mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
The Past Sessions scanner and the offline history reader each parsed Codex rollouts with their own shape helpers, their own CODEX_HOME resolution and different injected-context rules. Move the store location, the three message shapes and the injected-context rule into codex-rollout-format.ts. The scanner's behaviour is unchanged; the offline reader adopts its rule in the next commit.
129 lines
5.3 KiB
TypeScript
129 lines
5.3 KiB
TypeScript
/**
|
|
* @fileoverview Shape helpers for Codex rollout JSONL lines, shared by the two
|
|
* readers of `~/.codex/sessions`: the Past Sessions scanner
|
|
* (`codex-transcript.ts`, previews only) and the offline history reader
|
|
* (`local-history.ts`, whole conversations).
|
|
*
|
|
* Both readers must agree on three facts, so they live here once:
|
|
*
|
|
* - where the store is (`$CODEX_HOME/sessions`, else `~/.codex/sessions`);
|
|
* - which line shapes carry a user or assistant message;
|
|
* - which `response_item` user rows are injected context rather than input.
|
|
*
|
|
* ## The message shapes
|
|
*
|
|
* Codex has written conversation text under three shapes:
|
|
*
|
|
* - `event_msg` / `user_message` and `agent_message`, written by every version
|
|
* seen so far. `agent_message` carries a `phase` (`commentary` or
|
|
* `final_answer`); `user_message` carries none.
|
|
* - `event_msg` / `item_completed` with an `item.type` of `UserMessage` or
|
|
* `AgentMessage`, written by newer versions (0.152.1 and later).
|
|
* - `response_item` rows with `role: 'user'` or `'assistant'`. These are what
|
|
* the model saw, so the user rows mix real input with injected context.
|
|
*
|
|
* ## The injected-context rule
|
|
*
|
|
* Codex injects context as `response_item` user rows that start with a Markdown
|
|
* heading (`# AGENTS.md instructions for ...`) or an XML-style tag
|
|
* (`<environment_context>`, `<recommended_plugins>`, `<subagent_notification>`,
|
|
* `<skill>`, `<turn_aborted>`, `<image ...>`, `<codex_internal_context ...>`).
|
|
* Measured on a store of 2,916 rollouts (codex 0.120 to 0.163), every
|
|
* `response_item` user row with either prefix and no event twin was injected;
|
|
* the real prompts that start with `#` all have a `user_message` or
|
|
* `UserMessage` event twin, which this rule never filters. A fixed list of tag
|
|
* names would leak every wrapper Codex adds later (`<recommended_plugins>`
|
|
* alone was 1,355 rows on that store), so the rule is the prefix.
|
|
*
|
|
* @module codex-rollout-format
|
|
*/
|
|
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
/** Codex's home directory: `$CODEX_HOME`, else `~/.codex`, exactly as codex resolves it. */
|
|
export function codexHomeDir(): string {
|
|
return process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
}
|
|
|
|
/** The rollout store under {@link codexHomeDir}. */
|
|
export function codexSessionsRoot(): string {
|
|
return join(codexHomeDir(), 'sessions');
|
|
}
|
|
|
|
/**
|
|
* Flatten Codex message content, which is a string or an array of blocks.
|
|
* Only blocks carrying a string `text` contribute (images and skills do not).
|
|
* Not trimmed: callers that want a preview trim.
|
|
*/
|
|
export function codexContentText(content: unknown): string {
|
|
if (typeof content === 'string') return content;
|
|
if (!Array.isArray(content)) return '';
|
|
return content
|
|
.filter(
|
|
(b): b is { text: string } => !!b && typeof b === 'object' && typeof (b as { text?: unknown }).text === 'string'
|
|
)
|
|
.map((b) => b.text)
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
}
|
|
|
|
/** Injected context rather than typed input. Applies to `response_item` user rows only. */
|
|
export function isCodexInjectedContext(text: string): boolean {
|
|
const start = text.trimStart();
|
|
return start.startsWith('#') || start.startsWith('<');
|
|
}
|
|
|
|
/** One rollout line's user or assistant message, whichever shape carried it. */
|
|
export interface CodexLineMessage {
|
|
role: 'user' | 'assistant';
|
|
/** Untrimmed text, never empty. */
|
|
text: string;
|
|
/** `event` = user_message/agent_message, `item` = item_completed, `response` = response_item. */
|
|
shape: 'event' | 'item' | 'response';
|
|
/** Codex's own phase (`commentary` / `final_answer`) when the line carries one. */
|
|
phase?: string;
|
|
/** A `response_item` user row: may be injected context, see {@link isCodexInjectedContext}. */
|
|
injectionProne: boolean;
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
}
|
|
|
|
function phaseOf(value: unknown): string | undefined {
|
|
return typeof value === 'string' && value ? value : undefined;
|
|
}
|
|
|
|
/** The message a parsed rollout line carries, or null for every other line. */
|
|
export function codexMessageFromLine(entry: unknown): CodexLineMessage | null {
|
|
const e = record(entry);
|
|
const p = record(e.payload);
|
|
let role: CodexLineMessage['role'] | undefined;
|
|
let text = '';
|
|
let shape: CodexLineMessage['shape'] = 'event';
|
|
let phase: string | undefined;
|
|
if (e.type === 'event_msg') {
|
|
if (p.type === 'user_message' || p.type === 'agent_message') {
|
|
role = p.type === 'user_message' ? 'user' : 'assistant';
|
|
text = codexContentText(p.message);
|
|
phase = phaseOf(p.phase);
|
|
} else if (p.type === 'item_completed') {
|
|
const item = record(p.item);
|
|
if (item.type === 'UserMessage' || item.type === 'AgentMessage') {
|
|
role = item.type === 'UserMessage' ? 'user' : 'assistant';
|
|
shape = 'item';
|
|
text = codexContentText(item.content);
|
|
phase = phaseOf(item.phase);
|
|
}
|
|
}
|
|
} else if (e.type === 'response_item' && (p.role === 'user' || p.role === 'assistant')) {
|
|
role = p.role;
|
|
shape = 'response';
|
|
text = codexContentText(p.content);
|
|
phase = phaseOf(p.phase);
|
|
}
|
|
if (!role || !text) return null;
|
|
return { role, text, shape, phase, injectionProne: shape === 'response' && role === 'user' };
|
|
}
|