Skip to content

fix(remote): never auto-revive a remote session after a clean agent exit - #355

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
timkjr:pr/remote-exit
Sep 4, 2026
Merged

fix(remote): never auto-revive a remote session after a clean agent exit#355
Ark0N merged 1 commit into
Ark0N:masterfrom
timkjr:pr/remote-exit

Conversation

@timkjr

Copy link
Copy Markdown

Problem

A normal ctrl-c / ctrl-d / exit inside 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, emits remoteSessionDropped — 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 failed destroys 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() runs tmux -L codeman-remote has-session -t codeman-ssh-<id> over ssh (exit 0 = alive).
  • decideReconnect() gains a remote-gone skip: 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.
  • The probe is cached per-session and fired async, so the sync 5s tick never blocks on an ssh round-trip; a clean exit flips the cache to false and the auto-revive stops.

Transport-drop behavior is unchanged: remote tmux alive → reconnect as before.

Testing

  • 3 new unit cases in remote-auto-reconnect.test.ts pinning the decision: remote alive → emit; remote gone → skip; remote unknown → skip.
  • Verified live: after ctrl-c / ctrl-d on 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 master standalone.

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.
@Ark0N
Ark0N merged commit ee6a7af into Ark0N:masterSep 4, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Sep 4, 2026
…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

Ark0N commented Sep 4, 2026

Copy link
Copy Markdown
Owner

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: tmux has-session prints nothing on success (measured on a scratch socket: exit 0, empty stdout, the "can't find session" line goes to stderr), so stdout.trim().length > 0 read every live remote session as gone, and transport drops stopped reconnecting along with the clean exits. The follow-up (28b4423) classifies the probe by exit status instead (0 alive, ssh's 255 or a timeout unknown, anything else gone), forgets the cached answer whenever the pane is seen alive again so a stale true from one transport drop cannot revive the next clean exit, caps the probe at one in flight per session, and pins the command and the reattach-then-clean-exit sequence in tests. Ships in today's release.

@Ark0NArk0N mentioned this pull request Sep 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@timkjr@Ark0N