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

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 omp/opencode/claude 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.

Also thread ompConfig/resumeSessionId into the remote builders so a
dead-pane respawn of an omp session resumes (--resume <id>) or
continues (--continue) instead of launching bare omp.

Tests: 3 new cases pinning remote-gone / unknown / alive decisions;
remote omp resume + --continue fallback. Verified live: all three
remote CLIs stay dead after exit.
This commit is contained in:
timkjr
2026-09-07 21:30:31 -05:00
parent 0a5bc1ac2e
commit 88243e9ffa
3 changed files with 63 additions and 3 deletions
@@ -93,6 +93,11 @@ const ALLOWED_BRANCHES: Record<string, string> = {
"tmux-manager.ts::mode === 'claude'":
"claude's remote pane command carries per-session permission flags, and its docker form is " +
'`--session-id … || resume`; neither fits a static overlays.command string',
"tmux-manager.ts::mode === 'omp'":
'remote omp respawn needs the pinned/continue --resume override threaded through ' +
'(resumeSessionId/ompConfig), which the static overlays.remote.command string has no ' +
'room for; the command itself is still rendered through buildSpawnCommandFromRegistry, ' +
'the same mode-agnostic engine local/docker spawns use — only the BRANCH is per-mode',
// --- Per-CLI prose and launch handling not yet generalised ---
"web/session-wait-registry.ts::mode === 'deepseek'":
+25
View File
@@ -24,6 +24,31 @@ describe('COD-106 shared remote sessions', () => {
expect(cmd).toContain('new-session -A -s codeman-ssh-cod106aa');
});
it('remote omp relaunch resumes the pinned conversation instead of starting fresh (2026-08-29)', () => {
const cmd = buildRemoteLaunchCommand({
mode: 'omp',
remote,
sessionId: 'cod106aaa',
ompConfig: { model: 'llm-proxy/crof/glm-5.3-flash' },
resumeSessionId: '01a04eb1-d883-75f0-bdfa-74cc315b09ce',
});
// The remote pane command must carry the pinned omp session id so a
// dead-pane respawn lands back in the same conversation.
expect(cmd).toContain('omp --model llm-proxy/crof/glm-5.3-flash --resume 01a04eb1-d883-75f0-bdfa-74cc315b09ce');
// still a durable, idempotent remote tmux session
expect(cmd).toContain('new-session -A -s codeman-ssh-cod106aa');
});
it('remote omp relaunch falls back to --continue when no id is pinned', () => {
const cmd = buildRemoteLaunchCommand({
mode: 'omp',
remote,
sessionId: 'cod106aaa',
ompConfig: { continueSession: true },
});
expect(cmd).toContain('omp --continue');
});
it('parses session_attached as a CLIENT COUNT (>1 = shared)', () => {
const rows = parseRemoteSessionList(
['codeman-solo\\t1\\t100\\t1', 'codeman-shared\\t2\\t200\\t3', 'codeman-idle\\t0\\t300\\t1'].join('\n')