Remote tmux durability: survive SSH drop, discover/attach, collaborative sessions#156
Open
aakhter wants to merge 6 commits into
Open
Remote tmux durability: survive SSH drop, discover/attach, collaborative sessions#156aakhter wants to merge 6 commits into
aakhter wants to merge 6 commits into
Conversation
COD-104 wired checkRemoteTmuxAvailable into the remote-session create path,
but it does a real `ssh` via exec — so 2 remote-create tests in
session-routes.test.ts hit a ~10s ssh timeout and failed (422). Mirror
TmuxManager's IS_TEST_MODE no-op-shell-under-VITEST: short-circuit the live
probe to {ok:true} under vitest. Command construction stays covered by
buildRemoteTmuxCheckCommand unit tests. session-routes.test.ts now 61/61.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6ae2c0b8160090a1f0f6b32a3fe8496d402ac2c6)
…ill)
Phase 2 of the remote-tmux arc. Discover codeman-* tmux sessions already
running on a remote host (created by the remote's own Codeman or another
instance) and attach to one this Codeman didn't launch, with detach-not-kill
ownership for non-owned sessions.
- remote-hosts.ts: listRemoteCodemanSessions (ssh, VITEST-guarded, never throws)
+ pure parseRemoteSessionList + buildRemoteListSessionsCommand. Parser splits
on the LITERAL \t the remote tmux emits (next-3.7 does not expand \t) AND a
real tab. toAttachedSessionRemote builds a non-owned SessionRemote; toSessionRemote
now marks the COD-104 launch path owned:true.
- tmux-manager.ts: buildRemoteAttachCommand (sibling of buildRemoteLaunchCommand);
buildRemoteSessionCommand selects attach vs launch by ownership. killSession gains
a detach-not-kill early return for non-owned remote sessions: tears down only the
LOCAL pane (kills local ssh -> remote attach detaches), NEVER issues a remote
kill-session.
- types/session.ts: RemoteSessionInfo; SessionRemote.owned + remoteSessionName.
- schemas.ts: CreateSessionSchema.attachRemoteSession {hostId, remoteSessionName};
fixed a pre-existing no-useless-escape lint error in the jumpHost regex.
- case-routes.ts: GET /api/remote-hosts/:hostId/sessions (explicit discovery).
- session-routes.ts: attachRemoteSession create path -> non-owned session.
- UI (index.html/session-ui.js/styles.css): explicit "Discover existing sessions"
button + Attach action (owned:false). No auto-discover.
Verified on aa-desktop: discovered codeman-disco1, attached (attached=1, shared
view), killed local probe pane -> remote SURVIVED_DETACH (attached=0). Tests:
parse/attach-cmd/ownership unit + discovery route, session-routes + case-routes green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 55f5ada9db6d01518a4adf6b752e460b5df39524)
…to remote) Since COD-104 a remote session lives in a durable tmux server on the host and outlives the local pane, so killing a tab only DETACHED — even for sessions we own. Propagate `kill-session` to the remote for OWNED sessions in killSession's owned path (after COD-105's non-owned detach-only early-return); non-owned detach-only is untouched. Reconciled onto upstream PR Ark0N#145: Ark0N#145 already upstreamed this exact owned-kill propagation as `buildRemoteKillCommand({ remote, sessionId })` on the dedicated `-L codeman-remote` socket (matching buildRemoteLaunchCommand) and wired it into killSession (Strategy 3b, owned-only, fire-and-forget). The original COD-109 commit added a second `buildRemoteKillCommand(remote, name)` overload on the old `-L codeman` socket plus a duplicate kill block — a compile error AND a wrong socket post-Ark0N#145 (owned sessions no longer live on `codeman`). This commit keeps Ark0N#145's socket-correct implementation and drops the duplicate; the required test/remote-kill-command.test.ts is retargeted to Ark0N#145's `{ remote, sessionId }` signature and the `codeman-remote` socket. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continuous remote-only reconnect watcher closing the COD-104 durability arc: when a remote session's local ssh pane dies mid-run, re-establish it automatically instead of leaving a dead pane until the user pokes it. Design decisions (per cod108 design doc): - D1 event->owner: TmuxManager watcher DETECTS a dead remote pane and emits `remoteSessionDropped`; the session owner (server) reassembles the same RespawnPaneOptions and calls Session.reattachRemote() -> respawnPane, which re-runs the idempotent remote command (owned new-session -A / non-owned attach) and REJOINS the still-running durable remote tmux session. The watcher never reassembles options itself, and never routes through the Claude-idle respawn-controller. - D2 bounded backoff: per-session exponential backoff [5s,15s,45s,2m,5m,5m], reset on a successful reattach, `remoteReconnectExhausted` emitted once after the cap. Pure, unit-tested schedule + eligibility decision. - D3 always-on + kill-switch: `remoteAutoReconnect` app setting (default ON), read each tick; when false the watcher does nothing. Guards: killSession() (incl. the non-owned DETACH early-return) and shutdown add the session to an intentional-teardown guard set + clear its backoff BEFORE teardown, so a closed/killed tab is never auto-revived. Exactly one reconnect in flight per session (inFlight guard prevents stacked respawns). Per-session reconnect/guard state cleared on session removal. New: src/remote-reconnect.ts (pure backoff + decideReconnect), TmuxManager startRemoteReconnectWatcher/stop + runRemoteReconnectTick + noteRemoteReconnect + guardRemoteReconnect + clearRemoteReconnectState; Session.reattachRemote() (+ extracted _buildRespawnPaneOptions, shared with interactive start); server wiring + watcher start; 3 SSE events (sse-events.ts + constants.js in sync, broadcast + app.js exhausted "Reconnect" affordance); remoteAutoReconnect schema + settings-ui toggle. Tests: test/remote-auto-reconnect.test.ts (21) - pure schedule, eligibility (guarded never reconnects, non-remote/pane-alive/not-due skip, over-cap exhaust), and manager-level integration (dead remote pane -> dropped -> backoff -> exhausted; guarded emits nothing; reset-on-success; kill-switch off; state-cleared-on-remove). Verified real-remote against aa-desktop: drop local ssh pane -> watcher emitted -> respawnPane reattached the SAME remote session (remote pane_pid unchanged 3939->3939); test session cleaned up, the real host sessions left untouched. Checks: tsc, eslint, check:frontend-syntax, check:public-assets, prettier --check, build all green; tmux-manager/session-routes/session-manager/ sse-registry-parity suites pass. (cherry picked from commit d13d58b1994eb6594fd2eadea208104d36204f9d)
… + shared badge) Two Codeman clients attaching the same durable remote tmux session at different viewports would fight: tmux sizes a window to the SMALLEST attached client by default. Push `window-size latest` to the remote session config so the window tracks the most-recently-active client instead, letting concurrent clients coexist; surface the client count for a "shared · N" badge. Reconciled onto upstream PR Ark0N#145: Ark0N#145 moved the durable remote session onto the dedicated `-L codeman-remote` socket under a `codeman-ssh-` name and scoped every tmux set-option PER-SESSION (`set -t <name>`, never `-g`) so a shared remote tmux server's OTHER sessions keep their own prefix/mouse/sizing. The original COD-106 commit added `set -g window-size latest` (GLOBAL) on the old `-L codeman` socket — a regression against Ark0N#145's hardening. This commit layers the window-size feature onto Ark0N#145's structure as `set -t <name> window-size latest` (per-session, on the codeman-remote socket). Test assertions updated to the per-session form (remote-shared-sessions.test.ts) and the byte-identical launch-command test (remote-ssh-options.test.ts) extended with the window-size line — which supersedes the separate f09323c9 assertion fix (dropped: it targeted the global form and also carried unrelated CLAUDE.md doc changes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-sessions.md 55f5ada (COD-105) added Phase 2 of the remote-tmux arc: discover codeman-* sessions on a host and attach to non-owned ones, with detach-not-kill on close. - Data model: SessionRemote.owned/remoteSessionName + RemoteSessionInfo; toSessionRemote (owned:true) vs toAttachedSessionRemote (owned:false). - New Ownership section: discovery (listRemoteCodemanSessions, the literal-\t parse quirk, never-throws/VITEST), attach-vs-launch selection (buildRemoteSessionCommand), and the killSession detach-not-kill guarantee. - API: GET /api/remote-hosts/:hostId/sessions + the attachRemoteSession create path. - CLAUDE.md Remote Key Pattern notes discover/attach + detach-not-kill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit f321e1200a9a7c1e58c69ba936b680200fd53275)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a durability layer for remote SSH sessions, building on the remote-host SSH cases merged in #145. The agent runs inside
tmux -L codeman new-session -Aon the remote host so the session survives an SSH drop, and Codeman can discover and attach tocodeman-*sessions another client launched there.Scope (COD-104 to COD-109 in the downstream tracker; COD-107 already landed in #145):
codeman-*sessions on the host. Attached (owned:false) sessions detach on tab close, never kill.window-size latestso concurrent clients at different viewports do not clamp each other; adds a shared badge.All ssh command lines continue to flow through the single shell-safe
buildSshConnectionArgs()introduced with #145 (the COD-107 injection hardening), so no new command-injection surface is added.Testing
tsc --noEmit, lint, frontend-syntax, build: all clean.Design notes:
docs/remote-sessions.md.