diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 846924c31..d82a17083 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -863,6 +863,38 @@ describe('TmuxSessionManager', () => { logSpy.mockRestore() }) + test('#given an isolated session deferred after container spawn failure #when deferred attach retries #then it re-attempts isolated container creation before normal pane fallback', async () => { + // given + mockIsInsideTmux.mockReturnValue(true) + mockSpawnTmuxSession.mockImplementation(async () => ({ + success: false, + })) + + const { TmuxSessionManager } = await import('./manager') + const ctx = createMockContext() + const config = createTmuxConfig({ enabled: true, + isolation: 'session', + layout: 'main-vertical', + main_pane_size: 60, + main_pane_min_width: 80, + agent_pane_min_width: 40, }) + const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) + + await manager.onSessionCreated( + createSessionCreatedEvent('ses_isolated_retry', 'ses_parent', 'Isolated Retry Task') + ) + + mockExecuteActions.mockClear() + + // when + await Reflect.get(manager, 'tryAttachDeferredSession').call(manager) + + // then + expect(mockSpawnTmuxSession).toHaveBeenCalledTimes(2) + expect(mockExecuteActions).toHaveBeenCalledTimes(1) + expect(mockExecuteActions.mock.calls[0]?.[1]?.sourcePaneId).toBe('%0') + }) + test('#given queryWindowState returns null #when onSessionCreated fires #then session is enqueued in deferred queue', async () => { // given mockIsInsideTmux.mockReturnValue(true) diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 25c0c5e0e..bd5f8bf5d 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -27,6 +27,7 @@ interface DeferredSession { sessionId: string title: string queuedAt: Date + retryIsolatedContainer: boolean } export interface TmuxUtilDeps { @@ -366,8 +367,21 @@ export class TmuxSessionManager { } } - private enqueueDeferredSession(sessionId: string, title: string): void { - if (this.deferredSessions.has(sessionId)) return + private enqueueDeferredSession( + sessionId: string, + title: string, + retryIsolatedContainer = false, + ): void { + const existingDeferredSession = this.deferredSessions.get(sessionId) + if (existingDeferredSession) { + if (retryIsolatedContainer && !existingDeferredSession.retryIsolatedContainer) { + this.deferredSessions.set(sessionId, { + ...existingDeferredSession, + retryIsolatedContainer: true, + }) + } + return + } if (this.deferredQueue.length >= MAX_DEFERRED_QUEUE_SIZE) { log("[tmux-session-manager] deferred queue full, dropping session", { sessionId, @@ -380,6 +394,7 @@ export class TmuxSessionManager { sessionId, title, queuedAt: new Date(), + retryIsolatedContainer, }) this.deferredQueue.push(sessionId) log("[tmux-session-manager] deferred session queued", { @@ -430,8 +445,6 @@ export class TmuxSessionManager { } private async tryAttachDeferredSession(): Promise { - const effectiveSourcePaneId = this.getEffectiveSourcePaneId() - if (!effectiveSourcePaneId) return const sessionId = this.deferredQueue[0] if (!sessionId) { this.stopDeferredAttachLoop() @@ -459,6 +472,32 @@ export class TmuxSessionManager { return } + if (deferred.retryIsolatedContainer) { + const isolatedPaneId = await this.spawnInIsolatedContainer(sessionId, deferred.title) + if (isolatedPaneId) { + const sessionReady = await this.waitForSessionReady(sessionId) + this.sessions.set( + sessionId, + createTrackedSession({ + sessionId, + paneId: isolatedPaneId, + description: deferred.title, + }), + ) + this.removeDeferredSession(sessionId) + this.pollingManager.startPolling() + log("[tmux-session-manager] deferred session attached in isolated window", { + sessionId, + paneId: isolatedPaneId, + sessionReady, + }) + return + } + } + + const effectiveSourcePaneId = this.getEffectiveSourcePaneId() + if (!effectiveSourcePaneId) return + const state = await queryWindowState(effectiveSourcePaneId) if (!state) { this.nullStateCount += 1 @@ -623,7 +662,7 @@ export class TmuxSessionManager { if (this.isIsolated() && !this.isolatedWindowPaneId) { log("[tmux-session-manager] isolated container failed, deferring session for retry", { sessionId }) - this.enqueueDeferredSession(sessionId, title) + this.enqueueDeferredSession(sessionId, title, true) return } const sourcePaneId = this.getEffectiveSourcePaneId()