fix(omp): resume by mode, not silently as claude, and support --continue

resumeHistorySession() never sent mode when recreating a session from a
history/session-manager row, so the server default silently opened a
plain Claude session for every non-claude row -- reproduced live: OMP
rows spawned Claude sessions on click. Thread the row's mode through
every call site (welcome list, session manager, mobile overview) and
only send the Claude-specific resumeSessionId for claude rows.

Codeman has no live PTY-reattach outside server boot, and it's moot for
OMP anyway (exiting it kills the pane's only process), so route the
non-claude relaunch through each CLI's own continue-most-recent flag
instead of a context-free fresh start. OMP never got one: buildOmpCommand
only implemented --model/--resume despite omp --help documenting
-c/--continue. Added continueSession to OmpConfig end-to-end (type,
schema, builder) mirroring the existing opencode/pi/grok/deepseek
fields, and wired resumeHistorySession to use it.

Verified live: told a real omp session a secret, exited it, closed the
tab without killing tmux, relaunched with --continue in the same
directory, and had it recall the secret.
This commit is contained in:
timkjr
2026-08-28 11:32:30 -05:00
parent 7ec48adcc8
commit 3e1a0e679f
9 changed files with 64 additions and 11 deletions
+8 -3
View File
@@ -914,9 +914,14 @@ function buildOmpCommand(config?: OmpConfig): string {
if (safeModel) parts.push('--model', safeModel); if (safeModel) parts.push('--model', safeModel);
} }
if (config?.resumeSessionId) { // --resume and --continue conflict; a valid explicit session id wins,
const safeId = /^[a-zA-Z0-9._-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined; // mirroring the sibling builders (grok/pi/opencode).
if (safeId) parts.push('--resume', safeId); const safeId =
config?.resumeSessionId && /^[a-zA-Z0-9._-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined;
if (safeId) {
parts.push('--resume', safeId);
} else if (config?.continueSession) {
parts.push('--continue');
} }
return parts.join(' '); return parts.join(' ');
+2
View File
@@ -350,6 +350,8 @@ export interface OmpConfig {
model?: string; model?: string;
/** Resume a previous conversation (passed via --resume). */ /** Resume a previous conversation (passed via --resume). */
resumeSessionId?: string; resumeSessionId?: string;
/** Continue the most recent session in this directory (passed via --continue). */
continueSession?: boolean;
} }
/** /**
+1 -1
View File
@@ -389,7 +389,7 @@ Object.assign(CodemanApp.prototype, {
async resumeMobileOverviewSession(sessionId) { async resumeMobileOverviewSession(sessionId) {
const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId); const row = (this._mobileOverviewPastRows || []).find((r) => r.id === sessionId);
if (!row || !row.workingDir) return; if (!row || !row.workingDir) return;
await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined); await this.resumeHistorySession(row.claudeSessionId || row.id, row.workingDir, row.name || undefined, row.mode);
}, },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
+1 -1
View File
@@ -670,7 +670,7 @@ Object.assign(CodemanApp.prototype, {
} else if (record.workingDir) { } else if (record.workingDir) {
// History rows are keyed by the Claude conversation UUID; resumed // History rows are keyed by the Claude conversation UUID; resumed
// sessions carry theirs separately as claudeSessionId. // sessions carry theirs separately as claudeSessionId.
void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir); void this.resumeHistorySession(s.claudeSessionId || s.sessionId, record.workingDir, undefined, s.mode);
} }
}, },
}); });
+1 -1
View File
@@ -692,7 +692,7 @@ Object.assign(CodemanApp.prototype, {
btn.append(...parts); btn.append(...parts);
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
this.resumeHistorySession(s.sessionId, s.workingDir, s.name); this.resumeHistorySession(s.sessionId, s.workingDir, s.name, s.mode);
}); });
container.appendChild(btn); container.appendChild(btn);
} }
+29 -4
View File
@@ -2193,7 +2193,7 @@ Object.assign(CodemanApp.prototype, {
if (isLive && this.sessions.has(s.sessionId)) { if (isLive && this.sessions.has(s.sessionId)) {
this.selectSession(s.sessionId); this.selectSession(s.sessionId);
} else { } else {
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name); this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode);
} }
}) })
); );
@@ -2436,7 +2436,7 @@ Object.assign(CodemanApp.prototype, {
} else { } else {
// Resume by the Claude conversation UUID when present (resumed sessions // Resume by the Claude conversation UUID when present (resumed sessions
// carry theirs separately from their Codeman id). // carry theirs separately from their Codeman id).
this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name); this.resumeHistorySession(s.claudeSessionId || s.sessionId, s.workingDir || '', s.name, s.mode);
} }
this.closeSessionManager?.(); this.closeSessionManager?.();
closeMenu(); closeMenu();
@@ -2904,7 +2904,7 @@ Object.assign(CodemanApp.prototype, {
return `w${startNumber}-${dirName}`; return `w${startNumber}-${dirName}`;
}, },
async resumeHistorySession(sessionId, workingDir, existingName) { async resumeHistorySession(sessionId, workingDir, existingName, mode) {
// Close the run mode menu if open // Close the run mode menu if open
document.getElementById('runModeMenu')?.classList.remove('active'); document.getElementById('runModeMenu')?.classList.remove('active');
// Close folder history modal if open // Close folder history modal if open
@@ -2925,13 +2925,38 @@ Object.assign(CodemanApp.prototype, {
const globalSettings = this.loadAppSettingsFromStorage(); const globalSettings = this.loadAppSettingsFromStorage();
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings); const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
const effort = this.getEffortSetting(globalSettings); const effort = this.getEffortSetting(globalSettings);
// `resumeSessionId` is a Claude conversation UUID (server reads it from
// ~/.claude/projects); an external-CLI row has no such thing, so sending
// it there gets silently ignored while the OMITTED `mode` field defaults
// the create to plain claude — reproducing whatever conversation THAT
// uuid happens to collide with instead of the row's own backend. Row mode
// wins here. Codeman has no cross-restart PTY-reattach outside server
// boot, so "resume" for a non-claude row means relaunching the CLI's own
// continue-most-recent flag (opencode/pi/grok/omp --continue, deepseek
// resumeSession) in the same directory — real conversation continuity,
// just not the literal old process.
const effectiveMode = mode || 'claude';
const modeConfigKey = {
opencode: 'openCodeConfig',
pi: 'piConfig',
grok: 'grokConfig',
omp: 'ompConfig',
}[effectiveMode];
const modeConfig =
modeConfigKey
? { [modeConfigKey]: { continueSession: true } }
: effectiveMode === 'deepseek'
? { deepSeekConfig: { resumeSession: true } }
: {};
const createRes = await fetch('/api/sessions', { const createRes = await fetch('/api/sessions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
workingDir, workingDir,
name, name,
resumeSessionId: sessionId, mode: effectiveMode,
...(effectiveMode === 'claude' ? { resumeSessionId: sessionId } : {}),
...modeConfig,
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}), ...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
...(effort ? { effort } : {}), ...(effort ? { effort } : {}),
}), }),
+1
View File
@@ -361,6 +361,7 @@ const OmpConfigSchema = z
.max(100) .max(100)
.regex(/^[a-zA-Z0-9._-]+$/) .regex(/^[a-zA-Z0-9._-]+$/)
.optional(), .optional(),
continueSession: z.boolean().optional(),
}) })
.optional(); .optional();
+1 -1
View File
@@ -401,7 +401,7 @@ describe('Session Manager unified list', () => {
const [historyRecord, , historyOptions] = app._buildHistoryItem.mock.calls[1]; const [historyRecord, , historyOptions] = app._buildHistoryItem.mock.calls[1];
expect(historyRecord).toMatchObject({ sessionId: 'conv-uuid-1', sizeBytes: 2048, firstPrompt: 'old prompt' }); expect(historyRecord).toMatchObject({ sessionId: 'conv-uuid-1', sizeBytes: 2048, firstPrompt: 'old prompt' });
historyOptions.onActivate(); historyOptions.onActivate();
expect(app.resumeHistorySession).toHaveBeenCalledWith('conv-uuid-1', '/repo/old'); expect(app.resumeHistorySession).toHaveBeenCalledWith('conv-uuid-1', '/repo/old', undefined, undefined);
}); });
it('surfaces an error message instead of an empty list when the endpoint fails', async () => { it('surfaces an error message instead of an empty list when the endpoint fails', async () => {
+20
View File
@@ -85,6 +85,26 @@ describe('OMP spawn command', () => {
).toBe('omp'); ).toBe('omp');
}); });
it('continues the most recent session when no explicit resume id is given', () => {
expect(
buildSpawnCommand({
mode: 'omp',
sessionId: 'abc12345',
ompConfig: { continueSession: true },
})
).toBe('omp --continue');
});
it('prefers an explicit --resume id over --continue', () => {
expect(
buildSpawnCommand({
mode: 'omp',
sessionId: 'abc12345',
ompConfig: { resumeSessionId: 'session-99', continueSession: true },
})
).toBe('omp --resume session-99');
});
it('drops unsafe model strings from the spawn command', () => { it('drops unsafe model strings from the spawn command', () => {
expect( expect(
buildSpawnCommand({ buildSpawnCommand({