Skip to content

Instantly share code, notes, and snippets.

@wlib
Created May 23, 2026 18:56
Show Gist options
  • Select an option

  • Save wlib/919faabee22755c80a3816c703ce672c to your computer and use it in GitHub Desktop.

Select an option

Save wlib/919faabee22755c80a3816c703ce672c to your computer and use it in GitHub Desktop.
Copy out relevant messages from codex
#!/usr/bin/env node
// codex-thread — print a single Codex thread as markdown to stdout.
//
// Usage:
// codex-thread <id-or-path> # print one thread
// codex-thread --list # list all threads (id, date, name)
// codex-thread --help
//
// <id-or-path> may be:
// - a full thread id (e.g. 019dea12-7596-7ac0-ac9a-893d790e3278)
// - a unique id prefix (e.g. 019dea12)
// - a path to a rollout-*.jsonl file
//
// Output: one thread, top-level agent only, in the same format as the
// batch export. Subagent transcripts (spawn_agent) are excluded — they
// live inside function_call_output records which this never reads.
// Assistant content is only the final_answer phase (no commentary/reasoning).
// User <environment_context> wrappers are stripped; empty user records dropped.
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
import os from "node:os";
const CODEX_DIR = path.join(os.homedir(), ".codex");
const SESSIONS_DIR = path.join(CODEX_DIR, "sessions");
const INDEX_PATH = path.join(CODEX_DIR, "session_index.jsonl");
function die(msg, code = 1) { process.stderr.write(msg + "\n"); process.exit(code); }
function loadIndex() {
if (!fs.existsSync(INDEX_PATH)) return new Map();
const map = new Map();
for (const line of fs.readFileSync(INDEX_PATH, "utf8").split("\n")) {
if (!line.trim()) continue;
try { const e = JSON.parse(line); map.set(e.id, e); } catch {}
}
return map;
}
function findRolloutFiles() {
const files = [];
if (!fs.existsSync(SESSIONS_DIR)) return files;
(function walk(dir) {
for (const name of fs.readdirSync(dir)) {
const full = path.join(dir, name);
const stat = fs.statSync(full);
if (stat.isDirectory()) walk(full);
else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) files.push(full);
}
})(SESSIONS_DIR);
return files;
}
function idFromFilename(name) {
const m = name.match(/rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f-]+)\.jsonl$/);
return m ? m[1] : null;
}
function resolveTarget(arg) {
if (arg.includes("/") || arg.endsWith(".jsonl")) {
if (!fs.existsSync(arg)) die(`File not found: ${arg}`);
return arg;
}
const all = findRolloutFiles();
const matches = all.filter((f) => {
const id = idFromFilename(path.basename(f));
return id && (id === arg || id.startsWith(arg));
});
if (matches.length === 0) die(`No thread matches id "${arg}". Try: codex-thread --list`);
if (matches.length > 1) {
process.stderr.write(`Ambiguous id "${arg}", matches:\n`);
for (const m of matches) process.stderr.write(` ${idFromFilename(path.basename(m))} ${m}\n`);
process.exit(1);
}
return matches[0];
}
async function readJsonl(file) {
const items = [];
const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
for await (const line of rl) {
if (!line.trim()) continue;
try { items.push(JSON.parse(line)); } catch {}
}
return items;
}
const joinContent = (content, type) =>
Array.isArray(content)
? content.filter((c) => c && c.type === type && typeof c.text === "string").map((c) => c.text).join("")
: "";
const stripEnvContext = (t) => t.replace(/^<environment_context>[\s\S]*?<\/environment_context>\n*/, "");
async function extractPairs(file) {
const items = await readJsonl(file);
const pairs = [];
let lastFinalAnswer = null;
for (const it of items) {
if (!it || it.type !== "response_item" || !it.payload || it.payload.type !== "message") continue;
const p = it.payload;
if (p.role === "assistant" && p.phase === "final_answer") {
lastFinalAnswer = joinContent(p.content, "output_text");
} else if (p.role === "user") {
const userText = stripEnvContext(joinContent(p.content, "input_text"));
if (userText.trim() === "") continue;
pairs.push({ assistant: lastFinalAnswer, user: userText });
lastFinalAnswer = null;
}
}
return pairs;
}
function renderThread(meta, file, pairs) {
const header = meta
? `## ${meta.thread_name} — ${meta.updated_at}`
: `## ${path.basename(file)}`;
const lines = [header, "", `_${path.basename(file)}_`, ""];
pairs.forEach((p, i) => {
lines.push(
`### Turn ${i + 1}`,
"",
`**Assistant (final_answer):**`,
"",
p.assistant == null ? "> _(no prior assistant message)_" : p.assistant,
"",
`**User:**`,
"",
p.user,
""
);
});
return lines.join("\n");
}
async function cmdList() {
const index = loadIndex();
const all = findRolloutFiles();
const rows = [];
for (const f of all) {
const id = idFromFilename(path.basename(f));
if (!id) continue;
const meta = index.get(id);
rows.push({
id,
updated_at: meta?.updated_at ?? "?",
name: meta?.thread_name ?? "(unknown)",
});
}
rows.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
for (const r of rows) process.stdout.write(`${r.id} ${r.updated_at} ${r.name}\n`);
}
async function cmdPrint(arg) {
const file = resolveTarget(arg);
const id = idFromFilename(path.basename(file));
const meta = id ? loadIndex().get(id) : null;
const pairs = await extractPairs(file);
process.stdout.write(renderThread(meta, file, pairs) + "\n");
process.stderr.write(`(${pairs.length} turns from ${path.basename(file)})\n`);
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
process.stderr.write(
"Usage: codex-thread <id-or-path>\n" +
" codex-thread --list\n" +
"\n" +
"Examples:\n" +
" codex-thread --list\n" +
" codex-thread 019dea12 > out.md\n" +
" codex-thread 019deb3a >> out.md\n"
);
process.exit(args.length === 0 ? 1 : 0);
}
if (args[0] === "--list" || args[0] === "-l") return cmdList();
return cmdPrint(args[0]);
}
main().catch((e) => die(String(e?.stack || e)));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment