diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 84c7bc441..e63f4bf4d 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -1932,6 +1932,47 @@ describe('TmuxSessionManager', () => { expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(0) }) + test('#given sweepStaleOmoAgentSessions throws on first onSessionCreated #when second onSessionCreated fires #then sweep is retried instead of skipped forever', async () => { + // given + mockSweepStaleOmoAgentSessions.mockClear() + mockSweepStaleOmoAgentSessions.mockImplementationOnce(async () => { + throw new Error('simulated sweep failure') + }) + mockIsInsideTmux.mockReturnValue(true) + const { TmuxSessionManager } = await import('./manager') + const manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ + enabled: true, + isolation: 'session', + }), mockTmuxDeps) + + // when + await manager.onSessionCreated(createSessionCreatedEvent('ses_first', 'ses_parent', 'First')) + await manager.onSessionCreated(createSessionCreatedEvent('ses_second', 'ses_parent', 'Second')) + + // then + expect(mockSweepStaleOmoAgentSessions).toHaveBeenCalledTimes(2) + }) + + test('#given sweepStaleOmoAgentSessions succeeds #when additional onSessionCreated events fire in same process #then sweep runs exactly once', async () => { + // given + mockSweepStaleOmoAgentSessions.mockClear() + mockSweepStaleOmoAgentSessions.mockImplementation(async () => 0) + mockIsInsideTmux.mockReturnValue(true) + const { TmuxSessionManager } = await import('./manager') + const manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ + enabled: true, + isolation: 'session', + }), mockTmuxDeps) + + // when + await manager.onSessionCreated(createSessionCreatedEvent('ses_a', 'ses_parent', 'A')) + await manager.onSessionCreated(createSessionCreatedEvent('ses_b', 'ses_parent', 'B')) + await manager.onSessionCreated(createSessionCreatedEvent('ses_c', 'ses_parent', 'C')) + + // then + expect(mockSweepStaleOmoAgentSessions).toHaveBeenCalledTimes(1) + }) + test('#given killTmuxSessionIfExists throws #when cleanup runs #then cleanup still completes without throwing', async () => { // given mockKillTmuxSessionIfExists.mockClear() diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 5734ff822..3340fb55d 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -67,6 +67,7 @@ export class TmuxSessionManager { private isolatedWindowPaneId: string | undefined private isolatedContainerNullStateCount = 0 private staleSweepCompleted = false + private staleSweepInProgress = false constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: TmuxUtilDeps = defaultTmuxDeps) { this.client = ctx.client this.tmuxConfig = tmuxConfig @@ -977,27 +978,32 @@ export class TmuxSessionManager { } this.staleSweepCompleted = false + this.staleSweepInProgress = false log("[tmux-session-manager] cleanup complete") } private async sweepStaleIsolatedSessionsOnce(): Promise { if (this.staleSweepCompleted) return + if (this.staleSweepInProgress) return if (this.tmuxConfig.isolation !== "session") { this.staleSweepCompleted = true return } - this.staleSweepCompleted = true + this.staleSweepInProgress = true try { const killed = await sweepStaleOmoAgentSessions() if (killed > 0) { log("[tmux-session-manager] stale isolated sessions swept", { killed }) } + this.staleSweepCompleted = true } catch (error) { log("[tmux-session-manager] stale sweep failed", { error: String(error), }) + } finally { + this.staleSweepInProgress = false } } }