fix(remote): never auto-revive a remote session after a clean agent exit - #355
Conversation
The COD-108 reconnect watcher treated any dead local pane as a dropped transport and re-ran the pane command — so a normal ctrl-c/ctrl-d on a remote claude/opencode/omp auto-spawned a FRESH agent (claude only looked correct because its '--session-id || --resume' fallback resumed, with a loud 'already in use' error first). Distinguish a transport drop from an intentional exit: only reconnect when the durable remote tmux session (codeman-ssh-*) is verifiably still alive on the remote host. A clean exit tears that session down; the watcher now probes it via ssh has-session and skips (remote-gone) when it is gone OR unknown (fail closed). The probe is cached per-session and fired async so the 5s tick never blocks on ssh. Tests: 3 new cases pinning remote-gone / unknown / alive decisions. Verified live: all remote CLIs stay dead after ctrl-c/ctrl-d.
Uh oh!
There was an error while loading. Please reload this page.
…t it once the pane is back #355 made the remote auto-reconnect watcher revive a dead pane only when the durable remote tmux session is verifiably still alive, which is the right rule: a clean Ctrl-C / Ctrl-D / exit tears that session down and must never relaunch a fresh agent. Its probe, though, read `has-session`'s stdout and treated an empty string as "gone". `tmux has-session` prints NOTHING on success (measured on a scratch socket: exit 0, empty stdout, the failure message goes to stderr), so every live remote session classified as gone and transport-drop reconnects were silently disabled along with the clean-exit revives. The probe now goes by exit status through a pure, unit-tested mapping (`classifyRemoteAliveExit`): 0 is alive; ssh's own 255, a timeout (`killed`, no numeric code) and a spawn failure are unknown, which the watcher already treats as do-not-revive; any other status is the remote command's and means gone (tmux's 1 for a missing session, 127 when tmux is not installed there). Two smaller things in the same area: - The cached answer was never invalidated, so after one successful reattach a stale `true` would have revived the NEXT clean exit (the original bug back after the first transport drop), and a cached `false` from a clean exit would have left a manually restarted session with auto-reconnect permanently off. The tick now forgets the cache entry whenever the pane is seen alive. - The fire-and-forget probe has a 15s timeout against a 5s tick, so an unreachable host stacked up to three ssh processes per dead session. An in-flight set caps it at one. The probe command is pinned as a literal string, and the reattach-then-clean-exit sequence is driven through the watcher in the tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
Ark0N
commented
Sep 4, 2026
Merged, thank you. The clean-exit rule is exactly right and it is what ships. One correction went in right behind it, so you know what changed: |
Problem
A normal
ctrl-c/ctrl-d/exitinside a remote CLI session (Claude, OpenCode, OMP, …) auto-respawns a fresh agent. The COD-108 remote-reconnect watcher polls every 5s and, for any remote session whose local pane is dead, emitsremoteSessionDropped— which reattaches by re-running the pane command.The watcher intended to recover from a transport drop (ssh drops, agent still running on the remote). But
isPaneDead()is true in BOTH cases: a transport drop AND a normal agent exit. A clean exit tears down the durable remote tmux session (codeman-ssh-*,remain-on-exit faileddestroys the session when its only pane exits), so the watcher cannot tell the two apart — and re-runs the command, launching a brand-new conversation.Claude only looked okay: its remote launch is
claude --session-id <id> || claude --resume <id>, so the fresh run resumed — but not before printing a loud "already in use" error. OpenCode / OMP started fresh every time.Fix
Only auto-reconnect when the durable remote tmux session is verifiably still alive on the remote host:
remoteTmuxSessionAlive()runstmux -L codeman-remote has-session -t codeman-ssh-<id>over ssh (exit 0 = alive).decideReconnect()gains aremote-goneskip: when the remote session is gone (clean exit → do not revive) OR the probe is unknown (unreachable host → fail closed, do not revive), the watcher does nothing.falseand the auto-revive stops.Transport-drop behavior is unchanged: remote tmux alive → reconnect as before.
Testing
remote-auto-reconnect.test.tspinning the decision: remote alive → emit; remote gone → skip; remote unknown → skip.ctrl-c/ctrl-don remote Claude, OpenCode and OMP sessions, the pane stays dead (no auto-respawn).This is intentionally independent of any CLI-specific resume logic — it fixes the watcher for every remote mode at once.
Note: I have a separate open PR (#353) for OMP backend support. This fix is deliberately scoped away from that branch; it applies to upstream
masterstandalone.