feat(omp): survive a full session kill by reading omp's own transcripts

Claude conversations survive "Kill Tmux & Claude" because Codeman reads
them back independently from ~/.claude/projects, not from its own
session bookkeeping. omp conversations had no equivalent: kill the
Codeman session and the conversation vanished from Past Sessions
entirely, even though omp itself never forgot it on disk.

Adds omp-transcript.ts, a scanner over omp's own
~/.omp/agent/sessions/<mangled-cwd>/<uuid>.jsonl files (the same shape
as Claude Code's own transcript scanner, but simpler -- these files are
small enough to read whole instead of doing head/tail windows). Each
file's own "session" header line carries the real cwd and session id
directly, so unlike Claude's mangled-directory-name decoding this
never has to guess. Wired into gatherUnifiedInputs() as a second
history source alongside the Claude scan, and HistoryInput/
mergeUnifiedSessions() now carry an optional `mode` so a non-claude
history-only row still gets a real mode badge.

Also fixes the ambiguity behind the "continue picks the wrong
conversation" report from this session's testing: omp mints its OWN
session uuid, unrelated to Codeman's, so a live/persisted row and its
own history-scan row would otherwise show up as two separate entries
for the same conversation the moment the id gets resolved. Reuses the
existing claudeSessionId alias field (mergeUnifiedSessions' fold-into-
owner mechanism) to point at the resolved omp id, threading it through
every place `_claudeSessionId` gets (re)computed -- the constructor,
_resolvedOmpRespawnConfig, and a new _maybeCaptureOmpSessionId() that
opportunistically resolves it the first time a brand-new omp session
(one that has never gone through a respawn) goes idle.

Also closes a THIRD instance of the "ompConfig never got wired in
here" gap this session kept finding: restoreMuxSessions() in server.ts
restores every sibling CLI's config from persisted state on boot except
omp's, so a boot-recovered omp session always lost its resolved resume
id and fell back to guessing again.

Verified live end-to-end: told a session a secret, killed it fully
(Kill Tmux equivalent, killMux=true -- the Codeman session AND its tmux
pane both gone), and the conversation still showed up in the unified
list as a history-sourced row with the real first prompt as its title
and an omp mode badge, keyed by omp's own session id.

Known remaining gap, not fixed here: the claudeSessionId alias doesn't
yet resolve reliably on every boot-recovery path for a session that
was never respawned while alive (e.g. a plain re-attach to a pane that
was never dead) -- worth a follow-up, but doesn't affect the two things
that matter most: the conversation surviving a kill, and continuation
correctness once an id has been resolved (which happens on the very
next respawn either way).
This commit is contained in:
timkjr
2026-08-28 11:32:30 -05:00
parent 4c332c6141
commit 54a930c80e
6 changed files with 387 additions and 9 deletions
+49 -2
View File
@@ -20,12 +20,14 @@ import {
type ApiResponse,
type SessionColor,
type SessionStatus,
type SessionMode,
type CodexConfig,
type GeminiConfig,
type AntigravityConfig,
type PiConfig,
type GrokConfig,
type DeepSeekConfig,
type OmpConfig,
} from '../../types.js';
import { Session, isAltScreenStripMode, isMuxAltScreenOnlyStripMode } from '../../session.js';
import { SseEvent } from '../sse-events.js';
@@ -136,6 +138,8 @@ import {
toSessionDocker,
} from '../../docker-hosts.js';
import { LRUMap } from '../../utils/lru-map.js';
import { findLatestOmpSessionId } from '../../utils/omp-session-resolver.js';
import { scanOmpSessionsHistory } from '../../omp-transcript.js';
import {
getLastTranscriptResponse,
isExternalCliTranscriptMode,
@@ -744,6 +748,31 @@ async function injectAgentSkill(casePath: string): Promise<void> {
// bypassing the `workspaceHooksEnabled` setting. Route handlers here resolve the
// setting through the ConfigPort (tests stub it) and pass it as the second arg.
/**
* A "Resume"/"continue" request for a NEW omp-mode session (the frontend's
* resumeHistorySession(), or anyone hitting the API directly) carries
* `continueSession: true` but no id — omp has none to give it, since Codeman
* has never tracked its own conversation UUID. Left as `--continue`, that
* picks whichever session file in the directory is newest, which silently
* drifts to the WRONG conversation the moment a second omp session (this
* one, a sibling worker, a stray manual run) has touched the same directory
* more recently. Resolve the real id up front instead, same as the
* dead-pane-respawn path in session.ts does, so even the FIRST relaunch of a
* resumed conversation is pinned rather than guessed.
*/
function resolveOmpConfigForCreate(
mode: SessionMode,
workingDir: string,
ompConfig: OmpConfig | undefined
): OmpConfig | undefined {
if (mode !== 'omp') return undefined;
if (!ompConfig || ompConfig.resumeSessionId || !ompConfig.continueSession) {
return ompConfig;
}
const resolvedId = findLatestOmpSessionId(workingDir);
return resolvedId ? { ...ompConfig, resumeSessionId: resolvedId } : ompConfig;
}
export function registerSessionRoutes(
app: FastifyInstance,
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort
@@ -1065,7 +1094,7 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? gatedPiConfig : undefined,
grokConfig: mode === 'grok' ? gatedGrokConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? gatedDeepSeekConfig : undefined,
ompConfig: mode === 'omp' ? body.ompConfig : undefined,
ompConfig: resolveOmpConfigForCreate(mode, workingDir, body.ompConfig),
resumeSessionId: validatedResumeId,
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
effort: body.effort,
@@ -3304,7 +3333,7 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? qsGatedPiConfig : undefined,
grokConfig: mode === 'grok' ? qsGatedGrokConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
ompConfig: mode === 'omp' ? ompConfig : undefined,
ompConfig: resolveOmpConfigForCreate(mode, resolvedCasePath, ompConfig),
envOverrides: qsGatedEnvOverrides,
effort,
remote,
@@ -4153,6 +4182,24 @@ export function registerSessionRoutes(
// Projects dir may not exist.
}
// OMP's own session files (~/.omp/agent/sessions) — the non-claude twin
// of the scan above; see omp-transcript.ts for why this exists at all.
try {
for (const h of scanOmpSessionsHistory()) {
history.push({
sessionId: h.sessionId,
workingDir: h.workingDir,
sizeBytes: h.sizeBytes,
lastModified: h.lastModified,
firstPrompt: h.firstPrompt,
lastPrompt: h.lastPrompt,
mode: 'omp',
});
}
} catch {
// Best-effort, same as the claude scan above.
}
// Mux process stats (best-effort; guard against mocks lacking the method).
let mux: MuxStatInput[] = [];
try {
+1
View File
@@ -2755,6 +2755,7 @@ export class WebServer extends EventEmitter {
piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined,
grokConfig: muxSession.mode === 'grok' ? savedState?.grokConfig : undefined,
deepSeekConfig: muxSession.mode === 'deepseek' ? savedState?.deepSeekConfig : undefined,
ompConfig: muxSession.mode === 'omp' ? savedState?.ompConfig : undefined,
envOverrides: savedEnvOverrides,
effort: savedState?.effort,
attachmentHistory: savedAttachmentHistory,