fix(omp,remote): pin remote conversations on respawn so ctrl-d/ctrl-c resumes instead of relaunching fresh

Two independent defects made ANY clean exit from a remote SSH session (user
ctrl-d or ctrl-c, or a dropped pane) relaunch the agent as a NEW conversation:

1. SSH-remote claude was launched as a bare `claude --dangerously-skip-permissions`,
   so the remote-respawn path (COD-108 reattachRemote re-running the idempotent
   launch command) started a fresh conversation every time. Pin it to the
   deterministic Codeman session id, mirroring the docker-claude shape
   (claudeDockerPaneCommand): `--session-id <id>` to create, with the
   `|| --resume <id>` fallback so the idempotent re-run resumes instead of
   erroring with "already in use". A per-host commands.claude override still wins.

2. OMP --resume pinning silently degraded to ambiguous `--continue` whenever a
   case path ended in a trailing slash (e.g. remote `remotePath` stored verbatim
   as `/home/user/dotfiles/`): mangleOmpWorkingDir produced `-dotfiles-` while
   omp persists sessions under `-dotfiles`, readdirSync returned null for an
   existing dir, and findLatestOmpSessionId/resolveAndClaimOmpSessionId never
   matched. Normalize the trailing slash before mangling (new exported
   stripTrailingSlash) and compare the session header cwd against the same
   normalized value.

Both were found live 2026-08-29 on a remote OMP/Claude node: ctrl-c and ctrl-d
behaved identically, both relaunching a fresh session.
This commit is contained in:
timkjr
2026-09-07 21:20:41 -05:00
parent a164c07f92
commit 0a5bc1ac2e
4 changed files with 67 additions and 7 deletions
+9
View File
@@ -41,6 +41,15 @@ describe('mangleOmpWorkingDir', () => {
const sibling = `${homedir()}-other/dev/foo`;
expect(mangleOmpWorkingDir(sibling)).toBe(sibling.replace(/\//g, '-'));
});
it('normalizes a trailing slash so a remote case path resolves to the same dir', () => {
// Regression (2026-08-29): remote case paths are stored verbatim with a
// trailing slash (e.g. `/home/user/dotfiles/`), but omp persists sessions
// under the slash-less mangle (`-dotfiles`). Before the fix this produced
// `-dotfiles-`, readdirSync returned null for an existing dir, and OMP
// respawn pinning silently degraded to the ambiguous `--continue`.
expect(mangleOmpWorkingDir(join(homedir(), 'dotfiles') + '/')).toBe('-dotfiles');
});
});
describe('findLatestOmpSessionId', () => {
+15
View File
@@ -172,6 +172,21 @@ describe('TmuxManager (unit)', () => {
expect(command).toContain('exec "${SHELL:-/bin/sh}" -i -l -c');
expect(command).toContain('claude --dangerously-skip-permissions');
});
it('pins SSH-remote claude to the Codeman session id so a respawn resumes the same conversation', () => {
// Regression (2026-08-29): remote claude was launched as a bare `claude …`,
// so every reattach/respawn after a pane death (user ctrl-d or ctrl-c exit)
// started a NEW conversation. The launch now mirrors the docker-claude shape:
// `--session-id <id>` to create, with a `|| --resume <id>` fallback so the
// idempotent re-run resumes instead of erroring ("already in use").
const command = buildRemoteLaunchCommand({
mode: 'claude',
remote: { hostId: 'gpu-box', label: 'GPU Box', host: '10.0.0.42', username: 'ubuntu', remotePath: '/w' },
sessionId: 'abc123def456',
});
expect(command).toContain('claude --dangerously-skip-permissions --session-id abc123def456');
expect(command).toContain('claude --dangerously-skip-permissions --resume abc123def456');
});
});
describe('remote kill command builder', () => {