Browse Source

fix duplicate multiplexer managers

Alvin Unreal 2 months ago
parent
commit
c9f3e4a4a0
2 changed files with 76 additions and 1 deletions
  1. 40 0
      src/multiplexer/session-manager.test.ts
  2. 36 1
      src/multiplexer/session-manager.ts

+ 40 - 0
src/multiplexer/session-manager.test.ts

@@ -59,6 +59,7 @@ function createDeferred<T>() {
 describe('MultiplexerSessionManager', () => {
   const realDateNow = Date.now;
   const originalChildEnv = process.env.OMOS_MULTIPLEXER_CHILD;
+  const originalTmuxPane = process.env.TMUX_PANE;
 
   beforeEach(() => {
     mockMultiplexer.spawnPane.mockReset();
@@ -72,6 +73,9 @@ describe('MultiplexerSessionManager', () => {
     mockMultiplexer.isInsideSession.mockReturnValue(true);
     Date.now = realDateNow;
     delete process.env.OMOS_MULTIPLEXER_CHILD;
+    process.env.TMUX_PANE = '%controller';
+    (MultiplexerSessionManager as any).activeControllerKey = null;
+    (MultiplexerSessionManager as any).hasActiveController = false;
   });
 
   afterEach(() => {
@@ -81,6 +85,13 @@ describe('MultiplexerSessionManager', () => {
     } else {
       process.env.OMOS_MULTIPLEXER_CHILD = originalChildEnv;
     }
+    if (originalTmuxPane === undefined) {
+      delete process.env.TMUX_PANE;
+    } else {
+      process.env.TMUX_PANE = originalTmuxPane;
+    }
+    (MultiplexerSessionManager as any).activeControllerKey = null;
+    (MultiplexerSessionManager as any).hasActiveController = false;
   });
 
   describe('constructor', () => {
@@ -108,6 +119,35 @@ describe('MultiplexerSessionManager', () => {
 
       expect(mockMultiplexer.spawnPane).not.toHaveBeenCalled();
     });
+
+    test('only one plugin instance owns multiplexer pane spawning per pane', async () => {
+      const ctx = createMockContext();
+      const first = new MultiplexerSessionManager(
+        ctx,
+        defaultMultiplexerConfig,
+      );
+      const second = new MultiplexerSessionManager(
+        ctx,
+        defaultMultiplexerConfig,
+      );
+
+      await first.onSessionCreated({
+        type: 'session.created',
+        properties: { info: { id: 'child-first', parentID: 'parent' } },
+      });
+      await second.onSessionCreated({
+        type: 'session.created',
+        properties: { info: { id: 'child-second', parentID: 'parent' } },
+      });
+
+      expect(mockMultiplexer.spawnPane).toHaveBeenCalledTimes(1);
+      expect(mockMultiplexer.spawnPane).toHaveBeenCalledWith(
+        'child-first',
+        'Subagent',
+        `http://localhost:${process.env.OPENCODE_PORT ?? '4096'}/`,
+        '/test/directory',
+      );
+    });
   });
 
   describe('onSessionCreated', () => {

+ 36 - 1
src/multiplexer/session-manager.ts

@@ -57,6 +57,9 @@ const SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
  * with polling kept as a fallback for reliability.
  */
 export class MultiplexerSessionManager {
+  private static activeControllerKey: string | null = null;
+  private static hasActiveController = false;
+
   private client: OpencodeClient;
   private serverUrl: string;
   private directory: string;
@@ -78,18 +81,45 @@ export class MultiplexerSessionManager {
       ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
 
     this.multiplexer = getMultiplexer(config);
+    const controllerKey = this.getControllerKey(config.type);
+    let isController = false;
+    if (controllerKey !== null) {
+      if (!MultiplexerSessionManager.hasActiveController) {
+        MultiplexerSessionManager.activeControllerKey = controllerKey;
+        MultiplexerSessionManager.hasActiveController = true;
+        isController = true;
+      } else {
+        isController = false;
+      }
+    }
+
     this.enabled =
       process.env.OMOS_MULTIPLEXER_CHILD !== '1' &&
       config.type !== 'none' &&
-      this.multiplexer?.isInsideSession() === true;
+      this.multiplexer?.isInsideSession() === true &&
+      isController;
 
     log('[multiplexer-session-manager] initialized', {
       enabled: this.enabled,
       type: config.type,
       serverUrl: this.serverUrl,
+      controllerKey,
+      activeControllerKey: MultiplexerSessionManager.activeControllerKey,
     });
   }
 
+  private getControllerKey(type: MultiplexerConfig['type']): string | null {
+    if (type === 'tmux') {
+      return process.env.TMUX_PANE ?? null;
+    }
+
+    if (type === 'zellij') {
+      return process.env.ZELLIJ_PANE_ID ?? process.env.ZELLIJ ?? null;
+    }
+
+    return null;
+  }
+
   async onSessionCreated(event: SessionEvent): Promise<void> {
     if (!this.enabled || !this.multiplexer) return;
     if (event.type !== 'session.created') return;
@@ -559,6 +589,11 @@ export class MultiplexerSessionManager {
     this.spawningSessions.clear();
     this.closingSessions.clear();
 
+    if (this.enabled) {
+      MultiplexerSessionManager.hasActiveController = false;
+      MultiplexerSessionManager.activeControllerKey = null;
+    }
+
     log('[multiplexer-session-manager] cleanup complete');
   }
 }