mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user