Skip to content

Provider adapters ​

These are the ingest side of the conversion sesh runs: adapters turn a vendor's on-disk session format into the canonical model: parseClaudeSession for Claude Code, parseCodexRollout for Codex. Both live under src/messages/adapters/ and return the same shape:

ts
type IngestResult = {
  thread: Thread;
  messages: Message[];
  children: IngestedThread[];  // sidechain / subagent threads (Claude only)
  report: { skipped: Record<string, number>; notes: string[] };
};

Nothing is ever silently dropped — anything an adapter can't map to a canonical field is either skipped-and-counted in report.skipped, or kept as a { type: "unknown" } block.

Claude Code session files ​

A session lives at ~/.claude/projects/<munged-cwd>/<sessionId>.jsonl — one JSON object per line. Structurally it's a tree, not a list:

  • Message lines (type: "user" | "assistant") carry an envelope: uuid, parentUuid (the tree edge), isSidechain, sessionId, timestamp, cwd, gitBranch, version, userType, plus assistant-only requestId and user-only promptId/promptSource/permissionMode. Tool-result lines also carry a rich parsed toolUseResult.
  • The nested message field is in Anthropic API shape: content blocks are text, thinking (with a signature), tool_use (id, name, input), or tool_result (tool_use_id, content, is_error). Assistant message.usage has input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, and more.
  • Everything else — attachment, ai-title, system (e.g. subtype: "turn_duration"), file-history-snapshot, mode, permission-mode, last-prompt, queue-operation, pr-link, … — is session/UI state, not conversation.

parseClaudeSession(text, options?) walks the tree, turns message lines into canonical Messages, and treats isSidechain runs as child threads (parentThreadId + originMessageId) rather than folding them into the parent. Pass { keepRaw: true } to retain the original line on each message as raw — useful since toolUseResult can be large and isn't kept by default.

Codex rollouts ​

A rollout lives at ~/.codex/sessions/**/rollout-*.jsonl — also one JSON object per line, but linear, { timestamp, type, payload }:

  • session_meta — payload.id (session id), cwd, originator (codex-tui), cli_version, base_instructions.
  • turn_context — per-turn turn_id, cwd, model, effort, sandbox/approval policy.
  • response_item — the model-facing conversation:
    • message — roles include developer; content parts input_text / output_text.
    • reasoning — encrypted_content (opaque) + summary (usually empty).
    • function_call / function_call_output — name (e.g. exec_command), call_id, arguments as a JSON string, paired output.
    • custom_tool_call(_output) — e.g. apply_patch, raw patch text as input.
    • web_search_call — action.query (+ action.queries), status; no call_id.
  • event_msg — UI telemetry: user_message / agent_message (duplicate content already in response_item), task_started / task_complete (turn boundaries), token_count (cumulative + per-step usage), patch_apply_end, web_search_end.

parseCodexRollout(text, options?) folds this into the same canonical shape: turn_context / task_started become canonical turnId boundaries, token_count becomes usage on the turn's latest assistant message, and duplicate/telemetry event_msg entries are skipped and counted.

Mapping table ​

Source→ Canonical
Claude assistant lineMessage{role:assistant}; blocks: text→text, thinking→reasoning, tool_use→tool_call
Claude user line with tool_resultMessage{role:tool}; tool_result block (toolUseResult into raw if keepRaw)
Claude user line (prompt)Message{role:user}, starts a new turn
Claude ai-titlethread title (last non-empty wins)
Claude system/attachment/mode/etc. linesskipped, counted in ingest report
Claude sidechain lines (isSidechain)child threads (parentThreadId + chain grouping)
Codex session_metathread metadata + provenance sessionId
Codex response_item:messageMessage (developer role → system)
Codex response_item:reasoningreasoning block (redacted when only encrypted_content; summary text kept when present)
Codex function_call(_output)tool_call / tool_result, callId = call_id, arguments JSON-parsed
Codex custom_tool_call apply_patchfile_change block (+ tool_result for the output)
Codex web_search_callweb_search block
Codex event_msg:token_countusage on the turn's latest assistant message
Codex turn_context/task_startedcanonical turnId boundaries
Codex event_msg othersskipped (duplicates/telemetry), counted in ingest report

Ingest reports ​

Every parse call returns a report: { skipped, notes } alongside the thread/messages/children — a count of skipped source lines/items keyed by their type, plus free-text notes. Nothing is dropped without it showing up here, which is what makes ingestion safe to run against real session files without silently losing history.

Emitters ​

Emitters run the mapping in reverse: canonical Thread/Message[] back out to a vendor's on-disk shape. Both live under src/messages/emitters/ and return an EmitReport ({ dropped: Record<string, number>; notes: string[] }) alongside the emitted lines — the same "reported, never silent" contract as ingestion.

DirectionFunction
Codex rollout → canonical → Claude Code sessionemitClaudeSession(thread, messages, options?)
Claude Code session → canonical → Codex rolloutemitCodexRollout(thread, messages, options?)

emitCodexRollout is the reverse handoff: parse a live Claude Code session with parseClaudeSession, then emit it as Codex rollout lines ready to drop into ~/.codex/sessions/<dateDir>/<fileName> for codex resume <sessionId>. It accepts a link option that gets stamped onto session_meta.payload.sesh (with an emittedLines count) so a later merge-back knows exactly where the handoff happened — see Session handoff for the full round-trip walkthrough. emitClaudeSession gained a matching initialParentUuid option, so emitted lines can be chained onto an existing session instead of always starting a new tree — what the merge-back direction needs to graft Codex's new turns back onto a copy of the original Claude session.

Block-mapping summary (canonical → Codex) ​

Canonical blockCodex response_item
text (assistant)message with role: "assistant", content part output_text
text (user)message with role: "user", content part input_text
tool_callfunction_call — name, call_id = callId, arguments JSON-stringified from the block's input
file_changefunction_call named apply_patch, arguments JSON-stringified { input: patch }
tool_result (role tool)function_call_output — call_id = callId, output as a plain string (non-string outputs flattened to their text parts, or JSON.stringifyd)
system messagemessage with role: "developer"
reasoningdropped, counted in report.dropped.reasoning / reasoning-redacted

See Session handoff for the round-trip: ingest a Codex rollout, then emit it back out as a Claude Code session — and the reverse, ingest a Claude Code session and emit it as a Codex rollout.