fix(tmux): re-attempt isolated container on deferred session retry

This commit is contained in:
YeonGyu-Kim
2026-04-04 02:38:15 +09:00
parent cab84518de
commit 141881aa2d
2 changed files with 76 additions and 5 deletions
@@ -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)
+44 -5
View File
@@ -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<void> {
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()