Browse Source

Merge pull request #576 from andrewylies/fix/multiplexer-deferred-status-cleanup

fix(multiplexer): clear deferred idle close on explicit non-idle status
Alvin 1 month ago
parent
commit
175e3e2077
2 changed files with 69 additions and 2 deletions
  1. 62 0
      src/multiplexer/session-manager.test.ts
  2. 7 2
      src/multiplexer/session-manager.ts

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

@@ -662,6 +662,68 @@ describe('MultiplexerSessionManager', () => {
       expect(mockMultiplexer.closePane).not.toHaveBeenCalled();
     });
 
+    test('explicit non-idle status event clears stale deferred idle close', async () => {
+      const ctx = createMockContext();
+      const board = new BackgroundJobBoard();
+      board.registerLaunch({
+        taskID: 'retry-event-deferred',
+        parentSessionID: 'parent-1',
+        agent: 'explorer',
+      });
+      mockMultiplexer.spawnPane.mockResolvedValue({
+        success: true,
+        paneId: 'p-retry-event-deferred',
+      });
+      const manager = new MultiplexerSessionManager(
+        ctx,
+        defaultMultiplexerConfig,
+        board,
+      );
+      board.setTerminalStateListener((taskID) => {
+        void manager.retryDeferredIdleClose(taskID);
+      });
+
+      await manager.onSessionCreated({
+        type: 'session.created',
+        properties: {
+          info: { id: 'retry-event-deferred', parentID: 'parent-1' },
+        },
+      });
+      await manager.onSessionStatus({
+        type: 'session.status',
+        properties: {
+          sessionID: 'retry-event-deferred',
+          status: { type: 'idle' },
+        },
+      });
+      await manager.onSessionStatus({
+        type: 'session.status',
+        properties: {
+          sessionID: 'retry-event-deferred',
+          status: { type: 'retry' },
+        },
+      });
+
+      board.updateStatus({
+        taskID: 'retry-event-deferred',
+        state: 'completed',
+      });
+      await Promise.resolve();
+
+      expect(mockMultiplexer.closePane).not.toHaveBeenCalled();
+
+      await manager.onSessionStatus({
+        type: 'session.status',
+        properties: {
+          sessionID: 'retry-event-deferred',
+          status: { type: 'idle' },
+        },
+      });
+      expect(mockMultiplexer.closePane).toHaveBeenCalledWith(
+        'p-retry-event-deferred',
+      );
+    });
+
     test('explicit non-idle poll clears stale deferred idle close', async () => {
       const ctx = createMockContext();
       const board = new BackgroundJobBoard();

+ 7 - 2
src/multiplexer/session-manager.ts

@@ -264,7 +264,9 @@ export class MultiplexerSessionManager {
     const sessionId = event.properties?.sessionID;
     if (!sessionId) return;
 
-    if (event.properties?.status?.type === 'idle') {
+    const statusType = event.properties?.status?.type;
+
+    if (statusType === 'idle') {
       log('[multiplexer-session-manager] session status idle received', {
         instanceId: this.instanceId,
         sessionId,
@@ -277,8 +279,11 @@ export class MultiplexerSessionManager {
       return;
     }
 
-    if (event.properties?.status?.type === 'busy') {
+    if (statusType) {
       this.deferredIdleCloses.delete(sessionId);
+
+      if (statusType !== 'busy') return;
+
       log('[multiplexer-session-manager] session busy event received', {
         instanceId: this.instanceId,
         sessionId,