Merge pull request #362 from timkjr/feat/omp-remote-continuation

fix(omp,remote): thread remote-omp resume/continue through respawn and reattach
This commit is contained in:
Ark0N
2026-09-12 05:14:24 +02:00
committed by GitHub
9 changed files with 329 additions and 35 deletions
+69 -8
View File
@@ -827,8 +827,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
@@ -837,11 +840,66 @@ export function buildRemoteLaunchCommand(options: {
// `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH entry that
// only an interactive login shell resolves (see that function's comment).
const override = remote.commands?.[mode];
const modeCommand = override
? override
: mode === 'claude'
? remoteLoginShellCommand(`claude${buildClaudePermissionFlags(claudeMode, allowedTools)}`)
: defaultRemoteCommandForMode(mode);
let modeCommand: string;
if (override) {
modeCommand = override;
} else if (mode === 'claude') {
// Deterministic conversation pinning for SSH-remote claude (mirrors the
// docker-claude shape in claudeDockerPaneCommand, INCLUDING the distinct
// resumeId branch it declares — this used to only mirror the same-id
// fallback shape, silently dropping an explicit resumeSessionId that
// differs from sessionId, e.g. a resume-from-history launch): the FIRST
// run creates the conversation under --session-id <sessionId>; a respawn
// / reattach re-runs the same idempotent command, --session-id exits
// non-zero ("already in use"), and the `||` fallback RESUMES that same
// conversation. Without a pinned id, every reattach relaunched a bare
// `claude` and started a NEW conversation (found live 2026-08-29: remote
// claude ctrl-d / ctrl-c relaunched a fresh session). A per-host
// `commands.claude` override stays authoritative (admin's explicit
// choice) and skips this entirely.
const permFlags = buildClaudePermissionFlags(claudeMode, allowedTools);
const cmd = `claude${permFlags}`;
// Defense in depth, mirroring claudeDockerPaneCommand's own belt-and-braces check:
// sessionId is server-minted and always safe in practice, but this command is built
// as a single shellescaped string and then executed as shell code on the remote
// host, so an unsafe value here is validated rather than trusted.
if (!RESUME_ID_SAFE.test(sessionId)) {
modeCommand = remoteLoginShellCommand(cmd);
} else {
const rid = resumeSessionId && RESUME_ID_SAFE.test(resumeSessionId) ? resumeSessionId : undefined;
modeCommand = remoteLoginShellCommand(
rid && rid !== sessionId
? `${cmd} --resume ${rid} || ${cmd} --session-id ${sessionId}`
: `${cmd} --session-id ${sessionId} || ${cmd} --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);
}
const remoteName = remoteTmuxSessionName(sessionId);
// Innermost: the command tmux runs in the new pane. Run via `/bin/sh -c` by
@@ -1309,6 +1367,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) {
@@ -1882,7 +1943,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)
@@ -2133,7 +2194,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 {