import { readFileSync } from "node:fs"; // Codex writes each session to a "rollout" JSONL file under // ~/.codex/sessions//.jsonl (archived copies live flat under // ~/.codex/archived_sessions/). Every line is a { type, payload } event. // Conversation turns are `response_item` events whose payload is a message; // user text arrives in input_text blocks, assistant text in output_text. export type Role = "user" | "assistant"; export interface Turn { role: Role; text: string; at?: string; } interface ContentBlock { type?: string; text?: string; } interface RolloutEvent { type?: string; timestamp?: string; payload?: { type?: string; role?: string; content?: string | ContentBlock[]; cwd?: string; id?: string; }; } const TEXT_BLOCK_TYPES = new Set(["input_text", "output_text", "text"]); // Codex injects its own context as user-role messages (environment, instruction // blobs, abort markers). These aren't things the user said, so they're dropped // from what we persist to Honcho. const CODEX_SYSTEM_TAGS = [ "environment_context", "turn_aborted", "user_instructions", "apps_instructions", "plugins_instructions", "skills_instructions", "collaboration_mode", ]; function isInjectedSystemTurn(text: string): boolean { const t = text.trimStart(); return CODEX_SYSTEM_TAGS.some((tag) => t.startsWith(`<${tag}>`) || t.startsWith(`<${tag} `)); } function collectText(content: string | ContentBlock[] | undefined): string { if (typeof content === "string") return content.trim(); if (!Array.isArray(content)) return ""; return content .filter((b) => b.type && TEXT_BLOCK_TYPES.has(b.type) && typeof b.text === "string") .map((b) => b.text as string) .join("\n") .trim(); } function* events(path: string): Generator { let raw: string; try { raw = readFileSync(path, "utf-8"); } catch { return; } for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed) continue; try { yield JSON.parse(trimmed) as RolloutEvent; } catch { // Skip malformed lines rather than failing the whole read. } } } // Read the full conversation, in order, dropping tool/reasoning/system noise. export function readRollout(path: string): Turn[] { const turns: Turn[] = []; for (const event of events(path)) { if (event.type !== "response_item") continue; const payload = event.payload; if (!payload || payload.type !== "message") continue; if (payload.role !== "user" && payload.role !== "assistant") continue; const text = collectText(payload.content); if (!text) continue; if (payload.role === "user" && isInjectedSystemTurn(text)) continue; turns.push({ role: payload.role, text, at: event.timestamp }); } return turns; } // The session_meta header line carries the working directory Codex launched in. export function readRolloutCwd(path: string): string | undefined { for (const event of events(path)) { if (event.type === "session_meta" && typeof event.payload?.cwd === "string") { return event.payload.cwd || undefined; } } return undefined; }