mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-13 18:29:32 +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.
158 lines
7.1 KiB
TypeScript
158 lines
7.1 KiB
TypeScript
/**
|
|
* @fileoverview The offline history reader against rollouts that change while
|
|
* it reads them: a running Codex appending to its own rollout (or a child's),
|
|
* an unrelated rollout rewritten mid-scan, and a selected rollout replaced
|
|
* outright. `node:fs/promises` is wrapped so a test can act at an exact point
|
|
* of the read (after the open, after a read call) without timing races.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { appendFile, mkdtemp, rename, rm, truncate, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
type Hook = (path: string, openCount: number, handle: import('node:fs/promises').FileHandle) => Promise<void> | void;
|
|
const hooks: { onOpen?: Hook } = {};
|
|
const opens = new Map<string, number>();
|
|
|
|
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
return {
|
|
...actual,
|
|
open: async (...args: Parameters<typeof actual.open>) => {
|
|
const handle = await actual.open(...args);
|
|
const path = String(args[0]);
|
|
const count = (opens.get(path) ?? 0) + 1;
|
|
opens.set(path, count);
|
|
await hooks.onOpen?.(path, count, handle);
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
const history = await import('../src/local-history.js');
|
|
const { assistantTurn, realMeta, settingsApplied, userTurn, writeRollout } =
|
|
await import('./fixtures/codex-rollout.js');
|
|
|
|
const ROOT = 'aaaaaaaa-1111-4111-8111-111111111111';
|
|
const CHILD = 'bbbbbbbb-1111-4111-8111-111111111111';
|
|
let dir: string;
|
|
const line = (value: unknown) => `${JSON.stringify(value)}\n`;
|
|
|
|
beforeEach(async () => {
|
|
dir = await mkdtemp(join(tmpdir(), 'local-history-live-'));
|
|
opens.clear();
|
|
hooks.onOpen = undefined;
|
|
});
|
|
afterEach(async () => {
|
|
hooks.onOpen = undefined;
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('rollouts that change while they are read', () => {
|
|
it('reads a snapshot of a rollout a running Codex appends to after attribution', async () => {
|
|
const path = await writeRollout(dir, 'rollout-root.jsonl', [realMeta({ id: ROOT }), ...userTurn('question')]);
|
|
hooks.onOpen = async (opened, count) => {
|
|
// The second open is the transcript read; Codex writes a whole record and starts the next.
|
|
if (opened === path && count === 2) {
|
|
await appendFile(path, assistantTurn('answer').map(line).join(''));
|
|
await appendFile(path, '{"timestamp":"2026-10-24T01:00:02.000Z","type":"event_');
|
|
}
|
|
};
|
|
const result = await history.readLocalHistory({ sessionId: ROOT, sessionsRoot: dir });
|
|
expect(result.records.map((r) => r.message)).toEqual(['question', 'answer']);
|
|
// The record cut mid-write is reported, not parsed and not fatal.
|
|
expect(result.scan.partialRecords).toBe(1);
|
|
expect(result.scan.malformedRecords).toBe(0);
|
|
expect(result.complete).toBe(false);
|
|
});
|
|
|
|
it('reads a family whose child is still running', async () => {
|
|
await writeRollout(dir, 'rollout-root.jsonl', [realMeta({ id: ROOT }), ...userTurn('root question')]);
|
|
const child = await writeRollout(dir, 'rollout-child.jsonl', [
|
|
realMeta({ id: CHILD, parent: ROOT, mode: 'legacy' }),
|
|
...userTurn('child task'),
|
|
]);
|
|
hooks.onOpen = async (opened, count) => {
|
|
// Grows during the metadata scan and again before its transcript read.
|
|
if (opened === child)
|
|
await appendFile(child, assistantTurn(`child progress ${count}`, 'commentary').map(line).join(''));
|
|
};
|
|
const result = await history.readLocalHistory({ rootSessionId: ROOT, sessionsRoot: dir });
|
|
expect(result.records.map((r) => r.message)).toEqual([
|
|
'root question',
|
|
'child task',
|
|
'child progress 1',
|
|
'child progress 2',
|
|
]);
|
|
expect(result.complete).toBe(true);
|
|
});
|
|
|
|
it('counts an unrelated rollout that shrinks during its head read as unattributed', async () => {
|
|
await writeRollout(dir, 'rollout-root.jsonl', [realMeta({ id: ROOT }), ...userTurn('question')]);
|
|
const other = await writeRollout(dir, 'rollout-other.jsonl', [realMeta({ id: CHILD }), ...userTurn('other')]);
|
|
hooks.onOpen = async (opened, count, handle) => {
|
|
if (opened !== other || count !== 1) return;
|
|
const read = handle.read.bind(handle) as (...args: unknown[]) => Promise<unknown>;
|
|
(handle as unknown as { read: (...args: unknown[]) => Promise<unknown> }).read = async (...args) => {
|
|
const result = await read(...args);
|
|
await truncate(other, 10);
|
|
return result;
|
|
};
|
|
};
|
|
const result = await history.readLocalHistory({ sessionId: ROOT, sessionsRoot: dir });
|
|
expect(result.records.map((r) => r.message)).toEqual(['question']);
|
|
expect(result.scan.unattributedFiles).toBe(1);
|
|
expect(result.complete).toBe(false);
|
|
});
|
|
|
|
it('still refuses a selected rollout replaced by another file after attribution', async () => {
|
|
const path = await writeRollout(dir, 'rollout-root.jsonl', [realMeta({ id: ROOT }), ...userTurn('question')]);
|
|
hooks.onOpen = async (opened, count) => {
|
|
if (opened !== path || count !== 1) return;
|
|
const replacement = join(dir, 'replacement.tmp');
|
|
await writeFile(replacement, [realMeta({ id: ROOT }), ...userTurn('rewritten')].map(line).join(''));
|
|
await rename(replacement, path);
|
|
};
|
|
await expect(history.readLocalHistory({ sessionId: ROOT, sessionsRoot: dir })).rejects.toThrow(
|
|
/changed after attribution/
|
|
);
|
|
});
|
|
|
|
it('still refuses a selected rollout truncated after attribution', async () => {
|
|
const path = await writeRollout(dir, 'rollout-root.jsonl', [realMeta({ id: ROOT }), ...userTurn('question')]);
|
|
hooks.onOpen = async (opened, count) => {
|
|
if (opened === path && count === 2) await truncate(path, 100);
|
|
};
|
|
await expect(history.readLocalHistory({ sessionId: ROOT, sessionsRoot: dir })).rejects.toThrow(
|
|
/changed after attribution/
|
|
);
|
|
});
|
|
it('leaves a child readable, with an unproven cut, when its parent is replaced after attribution', async () => {
|
|
const parentSettings = [settingsApplied(), ...userTurn('parent second question')];
|
|
const parent = await writeRollout(dir, 'rollout-root.jsonl', [
|
|
realMeta({ id: ROOT }),
|
|
...userTurn('parent question'),
|
|
...parentSettings,
|
|
]);
|
|
const child = await writeRollout(dir, 'rollout-child.jsonl', [
|
|
realMeta({ id: CHILD, parent: ROOT }),
|
|
realMeta({ id: ROOT }),
|
|
...userTurn('parent question'),
|
|
...parentSettings,
|
|
settingsApplied(),
|
|
...userTurn('child task'),
|
|
]);
|
|
hooks.onOpen = async (opened, count) => {
|
|
// The child's transcript read comes before the parent's proof read: replace the parent in between.
|
|
if (opened !== child || count !== 2) return;
|
|
const replacement = join(dir, 'replacement.tmp');
|
|
await writeFile(replacement, [realMeta({ id: ROOT }), ...userTurn('rewritten')].map(line).join(''));
|
|
await rename(replacement, parent);
|
|
};
|
|
const result = await history.readLocalHistory({ sessionId: CHILD, sessionsRoot: dir });
|
|
expect(result.records.map((r) => r.message)).toEqual(['child task']);
|
|
expect(result.scan.unverifiedPrefixes).toBe(1);
|
|
expect(result.complete).toBe(false);
|
|
});
|
|
});
|