diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 6fc9dcd6..8e9456de 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -760,8 +760,11 @@ export function buildRemoteLaunchCommand(options: { sessionId: string; claudeMode?: ClaudeMode; allowedTools?: string; + /** OMP only — resume/continue overrides for the remote omp relaunch (dead-pane respawn). */ + ompConfig?: OmpConfig; + resumeSessionId?: string; }): string { - const { mode, remote, sessionId, claudeMode, allowedTools } = options; + const { mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId } = options; // §6.3: honor the session's EFFECTIVE claude permission mode on remote instead of // hardcoding --dangerously-skip-permissions, so a non-granted multi-user user's // downgraded 'auto' actually reaches the remote agent (the default command otherwise @@ -788,6 +791,30 @@ export function buildRemoteLaunchCommand(options: { modeCommand = remoteLoginShellCommand( `claude${permFlags} --session-id ${sessionId} || claude${permFlags} --resume ${sessionId}` ); + } else if (mode === 'omp') { + // Remote OMP respawn must RESUME the same conversation instead of + // relaunching fresh (found live 2026-08-29: remote ctrl-c/ctrl-d relaunched + // a brand-new omp session). The pinned id, when known, is passed as an + // explicit --resume; otherwise fall back to omp's own "most recent" + // --continue so a dead-pane respawn still lands back in the conversation. + // Rendered through the CLI registry (buildSpawnCommandFromRegistry), the + // SAME mode-agnostic path local/docker spawns use — not appendResumeFlag(), + // which would hand the id to the login shell as $0 after the quoted `-c + // 'omp'`, and not a raw buildOmpCommand() call, which the registry refactor + // (#347) deleted. Gives every registry CLI with a resume form this + // behaviour for free, and the flags can't drift from the local builder. + const ompEntry = getCli('omp'); + const ompCmd = ompEntry + ? (buildSpawnCommandFromRegistry(ompEntry, { + mode: 'omp', + sessionId, + ompConfig: { + ...ompConfig, + resumeSessionId: resumeSessionId || ompConfig?.resumeSessionId, + }, + }) ?? 'omp') + : 'omp'; + modeCommand = remoteLoginShellCommand(ompCmd); } else { modeCommand = defaultRemoteCommandForMode(mode); } @@ -1258,6 +1285,9 @@ function buildRemoteSessionCommand(options: { sessionId: string; claudeMode?: ClaudeMode; allowedTools?: string; + /** OMP only — resume/continue overrides for a remote omp relaunch. */ + ompConfig?: OmpConfig; + resumeSessionId?: string; }): string { const { remote, sessionId } = options; if (remote.owned === false) { @@ -1831,7 +1861,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { const fullCmd = docker ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId)) : remote - ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools }) + ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId }) : localFullCmd; // Create tmux session in three steps to handle cold-start (no server running) @@ -2082,7 +2112,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { const fullCmd = docker ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId)) : remote - ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools }) + ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId }) : localFullCmd; try { diff --git a/test/cli-registry-no-id-branching.test.ts b/test/cli-registry-no-id-branching.test.ts index 06cad742..8d1bc4de 100644 --- a/test/cli-registry-no-id-branching.test.ts +++ b/test/cli-registry-no-id-branching.test.ts @@ -93,6 +93,11 @@ const ALLOWED_BRANCHES: Record = { "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'": diff --git a/test/remote-shared-sessions.test.ts b/test/remote-shared-sessions.test.ts index 786c7b4f..7e2925dd 100644 --- a/test/remote-shared-sessions.test.ts +++ b/test/remote-shared-sessions.test.ts @@ -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')