fix: stop tracking sessions that never become ready

When session readiness times out, immediately close the spawned pane and skip tracking to prevent stale mappings from causing reopen and close anomalies.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim 2026-02-17 03:40:55 +09:00
parent 17da22704e
commit 84a83922c3
4 changed files with 85 additions and 4 deletions

View File

@ -434,6 +434,53 @@ describe('TmuxSessionManager', () => {
})
describe('onSessionDeleted', () => {
test('does not track session when readiness timed out', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
let stateCallCount = 0
mockQueryWindowState.mockImplementation(async () => {
stateCallCount++
if (stateCallCount === 1) {
return createWindowState()
}
return createWindowState({
agentPanes: [
{
paneId: '%mock',
width: 40,
height: 44,
left: 100,
top: 0,
title: 'omo-subagent-Timeout Task',
isActive: false,
},
],
})
})
const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext({ sessionStatusResult: { data: {} } })
const config: TmuxConfig = {
enabled: true,
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_timeout', 'ses_parent', 'Timeout Task')
)
mockExecuteAction.mockClear()
// when
await manager.onSessionDeleted({ sessionID: 'ses_timeout' })
// then
expect(mockExecuteAction).toHaveBeenCalledTimes(0)
})
test('closes pane when tracked session is deleted', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
@ -521,8 +568,13 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true)
let callCount = 0
mockExecuteActions.mockImplementation(async () => {
mockExecuteActions.mockImplementation(async (actions) => {
callCount++
for (const action of actions) {
if (action.type === 'spawn') {
trackedSessions.add(action.sessionId)
}
}
return {
success: true,
spawnedPaneId: `%${callCount}`,

View File

@ -213,10 +213,17 @@ export class TmuxSessionManager {
const sessionReady = await this.waitForSessionReady(sessionId)
if (!sessionReady) {
log("[tmux-session-manager] session not ready after timeout, tracking anyway", {
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
sessionId,
paneId: result.spawnedPaneId,
})
await executeAction(
{ type: "close", paneId: result.spawnedPaneId, sessionId },
{ config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state }
)
return
}
const now = Date.now()

View File

@ -135,10 +135,21 @@ export async function handleSessionCreated(
const sessionReady = await deps.waitForSessionReady(sessionId)
if (!sessionReady) {
log("[tmux-session-manager] session not ready after timeout, tracking anyway", {
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
sessionId,
paneId: result.spawnedPaneId,
})
await executeActions(
[{ type: "close", paneId: result.spawnedPaneId, sessionId }],
{
config: deps.tmuxConfig,
serverUrl: deps.serverUrl,
windowState: state,
},
)
return
}
const now = Date.now()

View File

@ -129,10 +129,21 @@ export class SessionSpawner {
const sessionReady = await this.waitForSessionReady(sessionId)
if (!sessionReady) {
log("[tmux-session-manager] session not ready after timeout, tracking anyway", {
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
sessionId,
paneId: result.spawnedPaneId,
})
await executeActions(
[{ type: "close", paneId: result.spawnedPaneId, sessionId }],
{
config: this.tmuxConfig,
serverUrl: this.serverUrl,
windowState: state,
},
)
return
}
const now = Date.now()