mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
A forked or spawned thread's rollout carries its own session_meta followed by its parent's and a copy of the parent's history. A later session_meta for a declared ancestor now opens an inherited prefix that is dropped: by ordinal below subagent_history_start_ordinal in paginated history, and up to the thread_settings_applied event Codex appends after the copy in legacy history, checked against the parent's own messages when the copy holds more than one candidate. An unrelated id still refuses the file. Store attribution now has its own budget (--metadata-bytes) separate from the transcript budget (--scan-bytes), reads each head in 32 KiB steps, and --max-entries is a flag, so id lookups work on stores of thousands of rollouts. An unattributable store file or unreadable directory is counted instead of aborting the lookup, and a rollout that a running Codex appends to is read as a snapshot up to its last complete line. User records carry no phase, date bounds are UTC calendar dates, the injected-context rule is the shared one, a symlinked sessions root is followed as the Past Sessions scanner follows it, and --json with --format text is refused.
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
/**
|
|
* @fileoverview Codex rollout lines shaped like real ones, for the offline
|
|
* history tests. The structure (keys, their order, the event types around a
|
|
* fork boundary) was taken from real rollouts written by codex 0.120 to 0.163
|
|
* and from codex-rs `SessionMeta` (`protocol/src/protocol.rs`); no
|
|
* conversation content was copied.
|
|
*
|
|
* A forked or spawned thread's file, as `record_initial_history` writes it for
|
|
* `ForkPersistence::Copied`:
|
|
*
|
|
* own session_meta, parent session_meta, copied parent records,
|
|
* thread_settings_applied, the child's own records
|
|
*
|
|
* In paginated history every line also carries an `ordinal`, and the child's
|
|
* own `session_meta` declares `subagent_history_start_ordinal`.
|
|
*/
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
|
|
/** Codex's base instructions make every real session_meta line about 20 KiB. */
|
|
export const BASE_INSTRUCTIONS_BYTES = 19 * 1024;
|
|
|
|
export interface MetaOptions {
|
|
id: string;
|
|
/** Spawn parent: sets parent_thread_id, forked_from_id and source.subagent.thread_spawn. */
|
|
parent?: string;
|
|
/** Shared root (`session_id`); defaults to `parent ?? id`, as Codex writes it. */
|
|
root?: string;
|
|
mode?: 'legacy' | 'paginated';
|
|
historyStart?: number;
|
|
timestamp?: string;
|
|
baseInstructionsBytes?: number;
|
|
}
|
|
|
|
export function realMeta(options: MetaOptions): Record<string, unknown> {
|
|
const { id, parent, mode = 'legacy', timestamp = '2026-10-24T01:00:00.000Z' } = options;
|
|
const payload: Record<string, unknown> = {
|
|
session_id: options.root ?? parent ?? id,
|
|
id,
|
|
...(parent ? { forked_from_id: parent, parent_thread_id: parent } : {}),
|
|
timestamp,
|
|
cwd: '/work/repo',
|
|
originator: 'codex_exec',
|
|
cli_version: '0.163.0',
|
|
source: parent
|
|
? {
|
|
subagent: {
|
|
thread_spawn: {
|
|
parent_thread_id: parent,
|
|
depth: 1,
|
|
agent_path: null,
|
|
agent_nickname: 'Kepler',
|
|
agent_role: null,
|
|
},
|
|
},
|
|
}
|
|
: 'exec',
|
|
thread_source: parent ? 'subagent' : 'user',
|
|
...(parent ? { agent_nickname: 'Kepler' } : {}),
|
|
model_provider: 'openai',
|
|
base_instructions: { text: 'b'.repeat(options.baseInstructionsBytes ?? BASE_INSTRUCTIONS_BYTES) },
|
|
history_mode: mode,
|
|
...(options.historyStart !== undefined ? { subagent_history_start_ordinal: options.historyStart } : {}),
|
|
...(parent ? { multi_agent_version: 'v1' } : {}),
|
|
context_window: { window_id: `${id.slice(0, 8)}-window` },
|
|
};
|
|
return { timestamp, type: 'session_meta', payload };
|
|
}
|
|
|
|
export const taskStarted = () => ({
|
|
timestamp: '2026-10-24T01:00:00.000Z',
|
|
type: 'event_msg',
|
|
payload: { type: 'task_started' },
|
|
});
|
|
export const settingsApplied = () => ({
|
|
timestamp: '2026-10-24T01:00:00.000Z',
|
|
type: 'event_msg',
|
|
payload: { type: 'thread_settings_applied', thread_settings: { model: 'gpt-5.5', cwd: '/work/repo' } },
|
|
});
|
|
export const developer = () => ({
|
|
timestamp: '2026-10-24T01:00:00.000Z',
|
|
type: 'response_item',
|
|
payload: { type: 'message', role: 'developer', content: [{ type: 'input_text', text: 'policy' }] },
|
|
});
|
|
export const injected = (tag = 'environment_context') => ({
|
|
timestamp: '2026-10-24T01:00:00.000Z',
|
|
type: 'response_item',
|
|
payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: `<${tag}>\n ctx\n</${tag}>` }] },
|
|
});
|
|
/** A real user turn: the response_item Codex sent plus its user_message event twin. */
|
|
export const userTurn = (text: string, timestamp = '2026-10-24T01:00:00.000Z') => [
|
|
{
|
|
timestamp,
|
|
type: 'response_item',
|
|
payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text }] },
|
|
},
|
|
{ timestamp, type: 'event_msg', payload: { type: 'user_message', message: text, images: [] } },
|
|
];
|
|
/** An assistant message: the event and the response_item, both carrying Codex's phase. */
|
|
export const assistantTurn = (text: string, phase: 'commentary' | 'final_answer' = 'final_answer') => [
|
|
{
|
|
timestamp: '2026-10-24T01:00:01.000Z',
|
|
type: 'event_msg',
|
|
payload: { type: 'agent_message', message: text, phase },
|
|
},
|
|
{
|
|
timestamp: '2026-10-24T01:00:01.000Z',
|
|
type: 'response_item',
|
|
payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text }], phase },
|
|
},
|
|
];
|
|
|
|
/** Number every line, as paginated history does. */
|
|
export function withOrdinals(lines: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
return lines.map((line, ordinal) => {
|
|
const { timestamp, type, payload, ...rest } = line;
|
|
return { timestamp, ordinal, type, payload, ...rest };
|
|
});
|
|
}
|
|
|
|
export async function writeRollout(dir: string, name: string, lines: unknown[]): Promise<string> {
|
|
const path = join(dir, name);
|
|
await writeFile(path, lines.map((line) => JSON.stringify(line)).join('\n') + '\n');
|
|
return path;
|
|
}
|