fix(tmux): send Ctrl+C before kill-pane and respawn-pane to prevent orphaned processes (#1329)

* fix(tmux): send Ctrl+C before kill-pane and respawn-pane to prevent orphaned processes

* fix(tmux-subagent): prevent premature pane closure with stability detection

Implements stability detection pattern from background-agent to prevent
tmux panes from closing while agents are still working (issue #1330).

Problem: Session status 'idle' doesn't mean 'finished' - agent may still
be thinking/reasoning. Previous code closed panes immediately on idle.

Solution:
- Require MIN_STABILITY_TIME_MS (10s) before stability detection activates
- Track message count changes to detect ongoing activity
- Require STABLE_POLLS_REQUIRED (3) consecutive polls with same message count
- Double-check session status before closing

Changes:
- types.ts: Add lastMessageCount and stableIdlePolls to TrackedSession
- manager.ts: Implement stability detection in pollSessions()
- manager.test.ts: Add 4 tests for stability detection behavior

* test(tmux-subagent): improve stability detection tests to properly verify age gate

- First test now sets session age >10s and verifies 3 polls don't close
- Last test now does 5 polls to prove age gate prevents closure
- Added comments explaining what each poll does
This commit is contained in:
itsmylife44
2026-02-01 11:11:35 +01:00
committed by GitHub
parent c73314f643
commit 6389da3cd6
4 changed files with 315 additions and 2 deletions
+23
View File
@@ -173,6 +173,17 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
return false
}
// Send Ctrl+C to trigger graceful exit of opencode attach process
log("[closeTmuxPane] sending Ctrl+C for graceful shutdown", { paneId })
const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
stdout: "pipe",
stderr: "pipe",
})
await ctrlCProc.exited
// Brief delay for graceful shutdown
await new Promise((r) => setTimeout(r, 250))
log("[closeTmuxPane] killing pane", { paneId })
const proc = spawn([tmux, "kill-pane", "-t", paneId], {
@@ -214,6 +225,18 @@ export async function replaceTmuxPane(
return { success: false }
}
// Send Ctrl+C to trigger graceful exit of existing opencode attach process
// Note: No delay here - respawn-pane -k will handle any remaining process.
// We send Ctrl+C first to give the process a chance to exit gracefully,
// then immediately respawn. This prevents orphaned processes while avoiding
// the race condition where the pane closes before respawn-pane runs.
log("[replaceTmuxPane] sending Ctrl+C for graceful shutdown", { paneId })
const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
stdout: "pipe",
stderr: "pipe",
})
await ctrlCProc.exited
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`
const proc = spawn([tmux, "respawn-pane", "-k", "-t", paneId, opencodeCmd], {