feat(tmux): sweep stale omo-agents-<pid> sessions on first spawn
Follow-up to PR #3507 addressing the Oracle-noted operational limitation: per-PID isolated session names (getIsolatedSessionName(process.pid)) mean that when an opencode process is SIGKILL'd (or the machine hard-reboots), the old omo-agents-<old-pid> tmux session survives forever because nothing is around to kill it. Added sweepStaleOmoAgentSessions() that: 1. Lists tmux sessions matching /^omo-agents-(\d+)$/ 2. For each, checks process.kill(pid, 0) to detect a dead PID 3. Skips our own PID 4. Calls killTmuxSessionIfExists for every session whose owner process is gone Wired into TmuxSessionManager.onSessionCreated() as a one-shot (guarded by staleSweepCompleted flag) so it runs lazily on the first subagent spawn when isolation="session". The flag is reset in cleanup() so subsequent process restarts re-run the sweep. 6 new tests cover: outside-tmux no-op, no matching sessions, multiple dead PIDs, current PID skip, live PID skip, list-sessions failure. Manual E2E verified on real tmux: - Created omo-agents-99999, sweep killed it - Spawned our own omo-agents-<pid>, closeTmuxPane returned true even after pane auto-destroy from Ctrl+C - Final tmux list-sessions shows zero omo-agents-* orphans
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
const STALE_SESSION_PATTERN = /^omo-agents-(\d+)$/
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
return true
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException
|
||||
return err?.code === "EPERM"
|
||||
}
|
||||
}
|
||||
|
||||
async function listOmoAgentSessions(tmux: string): Promise<string[]> {
|
||||
const { spawn } = await import("./spawn-process")
|
||||
const proc = spawn([tmux, "list-sessions", "-F", "#{session_name}"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stdout, , exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
|
||||
if (exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((name) => STALE_SESSION_PATTERN.test(name))
|
||||
}
|
||||
|
||||
export async function sweepStaleOmoAgentSessions(): Promise<number> {
|
||||
const [{ log }, { isInsideTmux }, { getTmuxPath }, { killTmuxSessionIfExists }] = await Promise.all([
|
||||
import("../../logger"),
|
||||
import("./environment"),
|
||||
import("../../../tools/interactive-bash/tmux-path-resolver"),
|
||||
import("./session-kill"),
|
||||
])
|
||||
|
||||
if (!isInsideTmux()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const tmux = await getTmuxPath()
|
||||
if (!tmux) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const candidateSessions = await listOmoAgentSessions(tmux)
|
||||
let killedCount = 0
|
||||
|
||||
for (const sessionName of candidateSessions) {
|
||||
const pidMatch = sessionName.match(STALE_SESSION_PATTERN)
|
||||
if (!pidMatch) continue
|
||||
|
||||
const pid = Number.parseInt(pidMatch[1], 10)
|
||||
if (!Number.isFinite(pid)) continue
|
||||
if (pid === process.pid) continue
|
||||
if (isProcessAlive(pid)) continue
|
||||
|
||||
log("[sweepStaleOmoAgentSessions] killing stale session", { sessionName, deadPid: pid })
|
||||
const killed = await killTmuxSessionIfExists(sessionName)
|
||||
if (killed) {
|
||||
killedCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return killedCount
|
||||
}
|
||||
Reference in New Issue
Block a user