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:
@@ -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], {
|
||||
|
||||
Reference in New Issue
Block a user