fix(tmux-subagent): wait for session readiness before spawning attach pane

`opencode attach` was invoked inside a freshly-split tmux pane before the
child session appeared in the opencode server's status map.  The process
exited immediately (session not found), tmux auto-closed the pane, and the
subagent ran invisibly in the background — the race documented in #3505.

Fix: call `waitForSessionReady` *before* `executeActions` in
`session-created-handler.ts`, mirroring the guard already present in
`TmuxSessionManager.ensureSessionReadyBeforeSpawn()`.  If the session does
not become attachable within the timeout the handler returns early without
spawning a pane at all, eliminating the transient-pane and silent-close
failure modes.  The now-unreachable post-spawn readiness-check / pane-close
cleanup branch is removed.

Adds a regression test suite (session-created-handler.test.ts) covering:
- not-ready session → no pane spawned, no polling started
- duplicate session.created → idempotent
- non session.created event type → no action
- already-tracked session → idempotent

Closes #3505

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
PeterPonyu
2026-05-15 07:54:52 -04:00
parent 24f7e560ca
commit 0465562fa4
2 changed files with 194 additions and 20 deletions
@@ -102,6 +102,19 @@ export async function handleSessionCreated(
return
}
// Wait for the child session to be registered in the opencode server's status
// map BEFORE spawning the tmux pane. If we spawn first, `opencode attach`
// exits immediately (session not yet visible), tmux auto-closes the pane, and
// the subagent runs invisibly in the background — the bug described in #3505.
const sessionReady = await deps.waitForSessionReady(sessionId)
if (!sessionReady) {
log("[tmux-session-manager] session readiness failed before spawn", {
sessionId,
stage: "session.created",
})
return
}
const result = await executeActions(decision.actions, {
config: deps.tmuxConfig,
directory: deps.directory,
@@ -137,26 +150,6 @@ export async function handleSessionCreated(
return
}
const sessionReady = await deps.waitForSessionReady(sessionId)
if (!sessionReady) {
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,
directory: deps.directory,
serverUrl: deps.serverUrl,
windowState: state,
},
)
return
}
deps.sessions.set(
sessionId,
createTrackedSession({