mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Codex's local_thread_store_compression rewrites cold rollouts to *.jsonl.zst, which the store scan skipped without a trace. Each one is now counted in compressedFiles (complete: false), skipped only when its plain .jsonl sibling exists, as Codex's own discovery does, and a lookup that misses names the compressed file whose name carries the wanted id. A Zstandard file given to --file, or sitting in the store under a .jsonl name, is recognised by its magic number. Each selected rollout is parsed once into every view later needed of it, so a legacy copy's boundary proof reuses the parent's own read instead of charging it to --scan-bytes a second time (and counting its malformed or inherited records twice). A parent read that fails for any reason other than a budget now leaves the boundary unproven (cut at the last candidate, unverifiedPrefixes, complete: false) instead of failing a child whose own file is intact. A budget error still fails the read. --max-bytes is validated before the first file is read, next to the other budgets, and every budget flag accepts plain decimal integers only.
110 lines
5.0 KiB
TypeScript
110 lines
5.0 KiB
TypeScript
/**
|
|
* @fileoverview `codeman history read`: the command-line front of the offline
|
|
* Codex history reader (`local-history.ts`).
|
|
*
|
|
* Deliberately separate from `codeman agent` (`cli-agent.ts`): that command is
|
|
* a client of a running Codeman and refuses outside a managed session, while
|
|
* this one reads rollout files from disk and needs neither a server nor
|
|
* `CODEMAN_MUX`. It never writes, never inspects a live pane and never sends
|
|
* input.
|
|
*
|
|
* Every bound the reader enforces has a flag here with the reader's own
|
|
* maximum, so a large store is a matter of raising a number rather than an
|
|
* impossible lookup. Output is JSON unless `--format text` is given; `--json`
|
|
* is the JSON spelling and refuses to combine with `--format text`.
|
|
*
|
|
* @module cli-history
|
|
*/
|
|
import type { Command } from 'commander';
|
|
import {
|
|
formatLocalHistory,
|
|
HISTORY_DEFAULT_MAX_ENTRIES,
|
|
HISTORY_DEFAULT_METADATA_BYTES,
|
|
HISTORY_DEFAULT_OUTPUT_BYTES,
|
|
HISTORY_DEFAULT_SCAN_BYTES,
|
|
HISTORY_MAX_ENTRIES,
|
|
HISTORY_MAX_METADATA_BYTES,
|
|
HISTORY_MAX_OUTPUT_BYTES,
|
|
HISTORY_MAX_SCAN_BYTES,
|
|
readLocalHistory,
|
|
validateOutputBytes,
|
|
} from './local-history.js';
|
|
|
|
/**
|
|
* A budget flag's value as a number. Only plain decimal digits count, so `0x10`,
|
|
* `1e3` and ` 5 ` are refused rather than read as 16, 1000 and 5; anything else
|
|
* becomes NaN, which the reader's range check rejects with the flag's name.
|
|
*/
|
|
function budgetFlag(value: string | boolean | undefined): number {
|
|
return typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : NaN;
|
|
}
|
|
|
|
export function registerHistoryCommands(program: Command): Command {
|
|
const history = program
|
|
.command('history')
|
|
.description('Read local saved native transcripts without a running server');
|
|
history
|
|
.command('read')
|
|
.description('Read Codex history by file, native id, or root conversation and descendants')
|
|
.option('--file <path>', 'Explicit local JSONL transcript (regular file only)')
|
|
.option('--session-id <id>', 'Native Codex conversation UUID or unique prefix (at least 8 characters)')
|
|
.option('--root-session-id <uuid>', 'Native root UUID, including transitive child conversations')
|
|
.option(
|
|
'--sessions-root <path>',
|
|
'Local Codex sessions directory (default: $CODEX_HOME/sessions, else ~/.codex/sessions)'
|
|
)
|
|
.option('--topic <text>', 'Case-insensitive literal match, preserving whole matching turns')
|
|
.option('--after <date>', 'Inclusive YYYY-MM-DD lower bound, as a UTC date; preserves whole matching turns')
|
|
.option('--before <date>', 'Inclusive YYYY-MM-DD upper bound, as a UTC date; preserves whole matching turns')
|
|
.option(
|
|
'--max-bytes <n>',
|
|
`Maximum complete output bytes (1-${HISTORY_MAX_OUTPUT_BYTES})`,
|
|
String(HISTORY_DEFAULT_OUTPUT_BYTES)
|
|
)
|
|
.option(
|
|
'--scan-bytes <n>',
|
|
`Read budget for the selected transcripts (1-${HISTORY_MAX_SCAN_BYTES})`,
|
|
String(HISTORY_DEFAULT_SCAN_BYTES)
|
|
)
|
|
.option(
|
|
'--metadata-bytes <n>',
|
|
`Read budget for attributing every rollout in the store (1-${HISTORY_MAX_METADATA_BYTES})`,
|
|
String(HISTORY_DEFAULT_METADATA_BYTES)
|
|
)
|
|
.option(
|
|
'--max-entries <n>',
|
|
`Directory entries visited in the store (1-${HISTORY_MAX_ENTRIES})`,
|
|
String(HISTORY_DEFAULT_MAX_ENTRIES)
|
|
)
|
|
.option('--format <format>', 'json or text', 'json')
|
|
.option('--json', 'JSON output (the default; conflicts with --format text)')
|
|
.action(async (options: Record<string, string | boolean | undefined>, command: Command) => {
|
|
try {
|
|
if (options.format !== 'json' && options.format !== 'text') throw new Error('--format must be json or text');
|
|
if (options.json && options.format === 'text' && command.getOptionValueSource('format') === 'cli')
|
|
throw new Error('--json conflicts with --format text; pass one of them');
|
|
const format = options.format;
|
|
// Every flag is checked before the first file is read: --max-bytes would otherwise
|
|
// only be judged after the whole store had been scanned.
|
|
const maxBytes = budgetFlag(options.maxBytes);
|
|
validateOutputBytes(maxBytes);
|
|
const result = await readLocalHistory({
|
|
file: options.file as string | undefined,
|
|
sessionId: options.sessionId as string | undefined,
|
|
rootSessionId: options.rootSessionId as string | undefined,
|
|
sessionsRoot: options.sessionsRoot as string | undefined,
|
|
topic: options.topic as string | undefined,
|
|
after: options.after as string | undefined,
|
|
before: options.before as string | undefined,
|
|
scanBytes: budgetFlag(options.scanBytes),
|
|
metadataBytes: budgetFlag(options.metadataBytes),
|
|
maxEntries: budgetFlag(options.maxEntries),
|
|
});
|
|
program.configureOutput().writeOut?.(`${formatLocalHistory(result, format, maxBytes)}\n`);
|
|
} catch (error) {
|
|
program.error(error instanceof Error ? error.message : String(error), { exitCode: 1, code: 'history.read' });
|
|
}
|
|
});
|
|
return history;
|
|
}
|