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
+172
View File
@@ -0,0 +1,172 @@
/**
* @fileoverview Scan `~/.omp/agent/sessions/*&#47;*.jsonl` for Past Sessions rows,
* the omp analog of what `scanProjectDir()` (session-routes.ts) does for
* Claude's own `~/.claude/projects` transcripts.
*
* Without this, an omp conversation exists ONLY as a Codeman-level live/
* persisted session record — delete that (a "Kill Tmux" close, or any other
* cleanup) and the conversation vanishes from Past Sessions entirely, even
* though `omp` itself never forgot it. Claude conversations don't have that
* problem because Codeman already reads them back from Claude's own
* transcript files independent of its own session bookkeeping; this gives
* omp conversations the same treatment.
*
* Each omp session file's SECOND line is a `{"type":"session","id":...,
* "cwd":...}` header carrying the real (unmangled) working directory and the
* session's own id directly — no need to reverse-engineer the mangled
* directory name the way Claude Code's own scanner has to (see
* `decodeProjectKey()` in session-routes.ts and its "lossy" caveat). Prompt
* text comes from each `{"type":"message","message":{"role":"user",...}}`
* entry, giving a real first-message title instead of a bare case name.
*
* Unlike Claude's transcripts (which can run to tens of MB of tool-call
* output), an omp session file is the conversation only, so this reads each
* file whole rather than doing head/tail windows — bounded by a size cap so
* one unexpectedly huge file can't blow up memory.
*
* @module omp-transcript
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
function ompSessionsRoot(): string {
return join(homedir(), '.omp', 'agent', 'sessions');
}
/** Skip anything absurdly large rather than parsing it whole into memory. */
const MAX_OMP_SESSION_FILE_BYTES = 2 * 1024 * 1024;
/** Defensive cap on total files scanned across every directory, mirroring
* the Claude scanner's own instinct not to let one pathological tree stall
* a request — a real omp install has, at most, a few hundred of these. */
const MAX_OMP_SESSION_FILES = 2000;
export interface OmpHistorySession {
sessionId: string;
workingDir: string;
sizeBytes: number;
/** ISO timestamp, from the file's own mtime. */
lastModified: string;
firstPrompt?: string;
lastPrompt?: string;
}
function extractUserPromptText(message: unknown): string | undefined {
if (!message || typeof message !== 'object') return undefined;
const m = message as { role?: unknown; content?: unknown };
if (m.role !== 'user' || !Array.isArray(m.content)) return undefined;
const parts: string[] = [];
for (const block of m.content) {
if (block && typeof block === 'object' && (block as { type?: unknown }).type === 'text') {
const text = (block as { text?: unknown }).text;
if (typeof text === 'string') parts.push(text);
}
}
const joined = parts.join(' ').trim();
return joined || undefined;
}
/** Parse one omp session `.jsonl` file, or null when it's unreadable, empty, or has no session header. */
function parseOmpSessionFile(filePath: string): OmpHistorySession | null {
let stat: ReturnType<typeof statSync>;
try {
stat = statSync(filePath);
} catch {
return null;
}
if (stat.size === 0 || stat.size > MAX_OMP_SESSION_FILE_BYTES) return null;
let raw: string;
try {
raw = readFileSync(filePath, 'utf-8');
} catch {
return null;
}
let sessionId: string | undefined;
let workingDir: string | undefined;
let firstPrompt: string | undefined;
let lastPrompt: string | undefined;
for (const line of raw.split('\n')) {
if (!line) continue;
let entry: unknown;
try {
entry = JSON.parse(line);
} catch {
continue;
}
if (!entry || typeof entry !== 'object') continue;
const e = entry as Record<string, unknown>;
if (e.type === 'session' && typeof e.id === 'string' && typeof e.cwd === 'string') {
sessionId = e.id;
workingDir = e.cwd;
} else if (e.type === 'message') {
const prompt = extractUserPromptText(e.message);
if (prompt) {
if (!firstPrompt) firstPrompt = prompt;
lastPrompt = prompt;
}
}
}
if (!sessionId || !workingDir) return null;
return {
sessionId,
workingDir,
sizeBytes: stat.size,
lastModified: stat.mtime.toISOString(),
firstPrompt,
lastPrompt,
};
}
/**
* Scan every omp conversation on disk into Past-Sessions rows. Best-effort
* throughout: a missing `~/.omp` (never installed/used), an unreadable
* directory, or one corrupt file yields fewer rows rather than throwing —
* this feeds the same unified merge the Claude transcript scanner does, and
* one broken source must never blank the whole Past Sessions list.
*/
export function scanOmpSessionsHistory(): OmpHistorySession[] {
const root = ompSessionsRoot();
let dirEntries: string[];
try {
dirEntries = readdirSync(root);
} catch {
return [];
}
const out: OmpHistorySession[] = [];
for (const dirName of dirEntries) {
if (out.length >= MAX_OMP_SESSION_FILES) break;
const dirPath = join(root, dirName);
let dirStat: ReturnType<typeof statSync>;
try {
dirStat = statSync(dirPath);
} catch {
continue;
}
if (!dirStat.isDirectory()) continue;
let files: string[];
try {
files = readdirSync(dirPath);
} catch {
continue;
}
for (const file of files) {
if (out.length >= MAX_OMP_SESSION_FILES) break;
if (!file.endsWith('.jsonl')) continue;
try {
const parsed = parseOmpSessionFile(join(dirPath, file));
if (parsed) out.push(parsed);
} catch {
// One bad file must not sink the whole scan.
}
}
}
return out;
}
+11
View File
@@ -99,6 +99,13 @@ export type HistoryInput = {
gitBranch?: string; gitBranch?: string;
worktreeName?: string; worktreeName?: string;
worktreeRepo?: string; worktreeRepo?: string;
/**
* Set only by a non-claude transcript source (currently omp); the Claude
* scanner never stamps this; the meaningfulness floor below still counts a
* row with a `mode` as real, since that also signals "not claude" — see
* where it's read below for the isReal check this touches.
*/
mode?: string;
}; };
/** Mux process-stat view. */ /** Mux process-stat view. */
@@ -175,6 +182,10 @@ export function mergeUnifiedSessions(sources: UnifiedSources): UnifiedSessionIte
overwrite(item, 'gitBranch', h.gitBranch); overwrite(item, 'gitBranch', h.gitBranch);
overwrite(item, 'worktreeName', h.worktreeName); overwrite(item, 'worktreeName', h.worktreeName);
overwrite(item, 'worktreeRepo', h.worktreeRepo); overwrite(item, 'worktreeRepo', h.worktreeRepo);
// Claude rows never set this (they're implicitly claude); a non-claude
// transcript source (currently only omp) does, so a history-only row
// still gets a mode badge instead of reading as claude by default.
overwrite(item, 'mode', h.mode);
const ms = Date.parse(h.lastModified); const ms = Date.parse(h.lastModified);
if (!Number.isNaN(ms) && item.lastActivityAt === undefined) item.lastActivityAt = ms; if (!Number.isNaN(ms) && item.lastActivityAt === undefined) item.lastActivityAt = ms;
} }
+80 -7
View File
@@ -57,6 +57,7 @@ import {
type SessionRemote, type SessionRemote,
type SessionDocker, type SessionDocker,
} from './types.js'; } from './types.js';
import { findLatestOmpSessionId } from './utils/omp-session-resolver.js';
import { probeDockerCliVersion } from './docker-hosts.js'; import { probeDockerCliVersion } from './docker-hosts.js';
import { probeRemoteCliVersion } from './remote-hosts.js'; import { probeRemoteCliVersion } from './remote-hosts.js';
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js'; import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
@@ -690,7 +691,13 @@ export class Session extends EventEmitter {
this._wireActivityAt = config.lastActivityAt || Date.now(); this._wireActivityAt = config.lastActivityAt || Date.now();
this._wireActivitySettleUntil = config.lastActivityAt ? Date.now() + WIRE_ACTIVITY_SETTLE_MS : 0; this._wireActivitySettleUntil = config.lastActivityAt ? Date.now() + WIRE_ACTIVITY_SETTLE_MS : 0;
// Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one. // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
this._claudeSessionId = config.resumeSessionId || this.id; // For omp, `claudeSessionId` doubles as the generic "external transcript id"
// alias key mergeUnifiedSessions() folds a history row into its owning
// session by: omp mints its OWN uuid, unrelated to this Codeman id, so
// without this an omp conversation's Past-Sessions row (keyed by omp's
// id) would never merge with its own live/persisted row (keyed by this
// id) — it would just show up a second time.
this._claudeSessionId = config.resumeSessionId || config.ompConfig?.resumeSessionId || this.id;
// Restored from state.json on boot recovery. start() resets _claudeSessionId // Restored from state.json on boot recovery. start() resets _claudeSessionId
// to the launch id even when re-attaching to a mux session whose CLI has // to the launch id even when re-attaching to a mux session whose CLI has
// moved on (a `/clear` before the restart), so this anchor is what lets the // moved on (a `/clear` before the restart), so this anchor is what lets the
@@ -1633,10 +1640,18 @@ export class Session extends EventEmitter {
// Respawning a dead pane means the CLI process exited (crash, idle // Respawning a dead pane means the CLI process exited (crash, idle
// respawn, or the user's own /exit) but this is still the same // respawn, or the user's own /exit) but this is still the same
// conversation from the user's perspective — unlike a brand-new // conversation from the user's perspective — unlike a brand-new
// `createSession` call, defaulting to --continue here is the honest // `createSession` call, defaulting to continuation here is the honest
// behavior. Only when the session has no resume id of its own already // behavior. `--continue` alone is ambiguous the moment ANY other omp
// (an explicit resumeSessionId always wins in buildOmpCommand). // conversation has touched this directory more recently (ours resumed
ompConfig: this._ompConfig?.resumeSessionId ? this._ompConfig : { ...this._ompConfig, continueSession: true }, // elsewhere, a second Codeman session opened here, ...) since it just
// picks the newest session file — so resolve and PIN the exact id the
// pane that just died was writing to, once. The dead pane's file is
// already fully flushed at this point, so "newest file" here is
// unambiguous by construction; every later respawn then reuses the
// pinned id instead of re-guessing. Only when the session already
// carries an explicit resumeSessionId does this skip straight past it
// (that one always wins in buildOmpCommand regardless).
ompConfig: this._resolvedOmpRespawnConfig(),
resumeSessionId: this._resumeSessionId, resumeSessionId: this._resumeSessionId,
envOverrides: this._envOverrides, envOverrides: this._envOverrides,
effort: this._effort, effort: this._effort,
@@ -1647,6 +1662,33 @@ export class Session extends EventEmitter {
}; };
} }
/**
* OMP-only: resolve and PIN the exact conversation to continue when
* respawning a dead pane, so every later respawn reuses the same id
* instead of re-resolving (and re-risking picking up a DIFFERENT
* conversation that happened to touch this directory more recently). See
* the comment at the call site in {@link _buildRespawnPaneOptions} for why
* "newest file on disk" is safe here specifically. Non-omp modes and a
* session that already carries an explicit id pass through untouched.
*/
private _resolvedOmpRespawnConfig(): OmpConfig | undefined {
if (this.mode !== 'omp') return this._ompConfig;
if (this._ompConfig?.resumeSessionId) return this._ompConfig;
const resolvedId = findLatestOmpSessionId(this.workingDir);
if (resolvedId) {
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
// Alias omp's own session uuid to this Codeman id — see the
// constructor's claudeSessionId comment for why this field is the
// (generically-named) mechanism that folds a Past-Sessions row back
// into its live/persisted session instead of duplicating it.
this._claudeSessionId = resolvedId;
return this._ompConfig;
}
// Nothing on disk yet (the dying process never got far enough to write a
// session file) — fall back to the CLI's own "most recent" heuristic.
return { ...this._ompConfig, continueSession: true };
}
/** /**
* Remember whether the CLI currently wants to be told about mouse clicks. * Remember whether the CLI currently wants to be told about mouse clicks.
* *
@@ -1909,8 +1951,13 @@ export class Session extends EventEmitter {
spawnErrLabel: 'mux attachment', spawnErrLabel: 'mux attachment',
}); });
// Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one. // Set claudeSessionId — when resuming, the Claude conversation ID is the
this._claudeSessionId = this._resumeSessionId || this.id; // resumed one. `_resolvedOmpRespawnConfig()` (called above while building
// respawnPaneOptions) may have JUST aliased this to omp's own session
// uuid — that already-resolved id must win over the generic
// `this.id` fallback, or this line clobbers it back to the Codeman id
// on every single respawn.
this._claudeSessionId = this._resumeSessionId || this._ompConfig?.resumeSessionId || this.id;
// For NEW mux sessions: wait for readiness then clean buffer // For NEW mux sessions: wait for readiness then clean buffer
// For RESTORED mux sessions: don't do anything - client will fetch buffer on tab switch // For RESTORED mux sessions: don't do anything - client will fetch buffer on tab switch
@@ -2319,10 +2366,36 @@ export class Session extends EventEmitter {
this._isWorking = false; this._isWorking = false;
this._status = 'idle'; this._status = 'idle';
this._lastPromptTime = Date.now(); this._lastPromptTime = Date.now();
if (wasWorking) this._maybeCaptureOmpSessionId();
this.emit('idle'); this.emit('idle');
} }
} }
/**
* A brand-new omp session (never yet respawned, so
* {@link _resolvedOmpRespawnConfig} has never run) has no captured
* omp-native session id: `_claudeSessionId` still defaults to this
* session's OWN Codeman id from the constructor. Until something aliases
* it, the omp history scan's row for this exact conversation (keyed by
* omp's own uuid) merges with nothing and shows up a second time. The
* first turn going idle is the first moment omp has definitely written
* its session file, so resolve and alias it here — best-effort, and only
* once (skips once `_claudeSessionId` differs from `this.id`, whether from
* this capture or a resume/respawn that already resolved one).
*/
private _maybeCaptureOmpSessionId(): void {
if (this.mode !== 'omp' || this._claudeSessionId !== this.id) return;
try {
const resolvedId = findLatestOmpSessionId(this.workingDir);
if (resolvedId) {
this._claudeSessionId = resolvedId;
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
}
} catch {
// Best-effort: a failed capture just means the next respawn tries again.
}
}
/** /**
* Process expensive parsers (ANSI strip, Ralph, bash tool, token, CLI info, task descriptions). * Process expensive parsers (ANSI strip, Ralph, bash tool, token, CLI info, task descriptions).
* Called on a throttled schedule (every EXPENSIVE_PROCESS_INTERVAL_MS) instead of on every * Called on a throttled schedule (every EXPENSIVE_PROCESS_INTERVAL_MS) instead of on every
+74
View File
@@ -0,0 +1,74 @@
/**
* @fileoverview Resolve the real OMP session id for a working directory, so a
* relaunch can pass `--resume <id>` instead of the ambiguous `--continue`.
*
* `omp` persists each conversation as its own file under
* `~/.omp/agent/sessions/<mangled-workingDir>/<ISO-timestamp>_<session-uuid>.jsonl`
* (workingDir mangled the same way Claude Code mangles `~/.claude/projects/*`:
* every `/` replaced with `-`). `--continue` picks whichever file in that
* directory is newest, which silently drifts to the WRONG conversation the
* moment two Codeman sessions ever touch the same directory — exactly what a
* closed-then-resumed row plus a still-running duplicate produces. Resolving
* the id once and pinning it with `--resume` removes that ambiguity for every
* later relaunch of the same Codeman session.
*
* @module utils/omp-session-resolver
*/
import { readdirSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
/** A real OMP session file is `<ISO-ish-timestamp>_<uuid>.jsonl`; only the uuid matters here. */
const OMP_SESSION_FILE_PATTERN = /^.+_([a-zA-Z0-9-]+)\.jsonl$/;
/**
* Mirrors `omp`'s own directory mangling: every path separator becomes a
* dash. Pure so it's unit-testable without touching the filesystem.
*/
export function mangleOmpWorkingDir(workingDir: string): string {
return workingDir.replace(/\//g, '-');
}
/** `~/.omp` — no known env override exists (unlike DSH_HOME); revisit if omp adds one. */
function resolveOmpHome(): string {
return join(homedir(), '.omp');
}
/**
* Newest OMP session id for this working directory, or null when the
* directory doesn't exist yet (never launched) or holds no session files.
*
* Deliberately "newest file, full stop" rather than a time-windowed match:
* callers only invoke this at a moment where that's unambiguous by
* construction — right after the file that answers it was the only thing
* that could have just been written (a dead pane's process already exited,
* or a session being resumed has no live sibling in the same directory yet).
*/
export function findLatestOmpSessionId(workingDir: string): string | null {
const dir = join(resolveOmpHome(), 'agent', 'sessions', mangleOmpWorkingDir(workingDir));
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return null;
}
let newestMtime = -Infinity;
let newestId: string | null = null;
for (const entry of entries) {
const match = OMP_SESSION_FILE_PATTERN.exec(entry);
if (!match) continue;
let mtimeMs: number;
try {
mtimeMs = statSync(join(dir, entry)).mtimeMs;
} catch {
continue;
}
if (mtimeMs > newestMtime) {
newestMtime = mtimeMs;
newestId = match[1];
}
}
return newestId;
}
+49 -2
View File
@@ -20,12 +20,14 @@ import {
type ApiResponse, type ApiResponse,
type SessionColor, type SessionColor,
type SessionStatus, type SessionStatus,
type SessionMode,
type CodexConfig, type CodexConfig,
type GeminiConfig, type GeminiConfig,
type AntigravityConfig, type AntigravityConfig,
type PiConfig, type PiConfig,
type GrokConfig, type GrokConfig,
type DeepSeekConfig, type DeepSeekConfig,
type OmpConfig,
} from '../../types.js'; } from '../../types.js';
import { Session, isAltScreenStripMode, isMuxAltScreenOnlyStripMode } from '../../session.js'; import { Session, isAltScreenStripMode, isMuxAltScreenOnlyStripMode } from '../../session.js';
import { SseEvent } from '../sse-events.js'; import { SseEvent } from '../sse-events.js';
@@ -136,6 +138,8 @@ import {
toSessionDocker, toSessionDocker,
} from '../../docker-hosts.js'; } from '../../docker-hosts.js';
import { LRUMap } from '../../utils/lru-map.js'; import { LRUMap } from '../../utils/lru-map.js';
import { findLatestOmpSessionId } from '../../utils/omp-session-resolver.js';
import { scanOmpSessionsHistory } from '../../omp-transcript.js';
import { import {
getLastTranscriptResponse, getLastTranscriptResponse,
isExternalCliTranscriptMode, isExternalCliTranscriptMode,
@@ -744,6 +748,31 @@ async function injectAgentSkill(casePath: string): Promise<void> {
// bypassing the `workspaceHooksEnabled` setting. Route handlers here resolve the // bypassing the `workspaceHooksEnabled` setting. Route handlers here resolve the
// setting through the ConfigPort (tests stub it) and pass it as the second arg. // 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( export function registerSessionRoutes(
app: FastifyInstance, app: FastifyInstance,
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort & TabLayoutPort
@@ -1065,7 +1094,7 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? gatedPiConfig : undefined, piConfig: mode === 'pi' ? gatedPiConfig : undefined,
grokConfig: mode === 'grok' ? gatedGrokConfig : undefined, grokConfig: mode === 'grok' ? gatedGrokConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? gatedDeepSeekConfig : undefined, deepSeekConfig: mode === 'deepseek' ? gatedDeepSeekConfig : undefined,
ompConfig: mode === 'omp' ? body.ompConfig : undefined, ompConfig: resolveOmpConfigForCreate(mode, workingDir, body.ompConfig),
resumeSessionId: validatedResumeId, resumeSessionId: validatedResumeId,
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides), envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
effort: body.effort, effort: body.effort,
@@ -3304,7 +3333,7 @@ export function registerSessionRoutes(
piConfig: mode === 'pi' ? qsGatedPiConfig : undefined, piConfig: mode === 'pi' ? qsGatedPiConfig : undefined,
grokConfig: mode === 'grok' ? qsGatedGrokConfig : undefined, grokConfig: mode === 'grok' ? qsGatedGrokConfig : undefined,
deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined, deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
ompConfig: mode === 'omp' ? ompConfig : undefined, ompConfig: resolveOmpConfigForCreate(mode, resolvedCasePath, ompConfig),
envOverrides: qsGatedEnvOverrides, envOverrides: qsGatedEnvOverrides,
effort, effort,
remote, remote,
@@ -4153,6 +4182,24 @@ export function registerSessionRoutes(
// Projects dir may not exist. // 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). // Mux process stats (best-effort; guard against mocks lacking the method).
let mux: MuxStatInput[] = []; let mux: MuxStatInput[] = [];
try { try {
+1
View File
@@ -2755,6 +2755,7 @@ export class WebServer extends EventEmitter {
piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined, piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined,
grokConfig: muxSession.mode === 'grok' ? savedState?.grokConfig : undefined, grokConfig: muxSession.mode === 'grok' ? savedState?.grokConfig : undefined,
deepSeekConfig: muxSession.mode === 'deepseek' ? savedState?.deepSeekConfig : undefined, deepSeekConfig: muxSession.mode === 'deepseek' ? savedState?.deepSeekConfig : undefined,
ompConfig: muxSession.mode === 'omp' ? savedState?.ompConfig : undefined,
envOverrides: savedEnvOverrides, envOverrides: savedEnvOverrides,
effort: savedState?.effort, effort: savedState?.effort,
attachmentHistory: savedAttachmentHistory, attachmentHistory: savedAttachmentHistory,