解讀 Claude Code、Codex 與 Mistral Vibe 未有文件説明的工作階段日誌
# Reading the undocumented session logs of Claude Code, Codex and Mistral Vibe
作者拆解 Claude Code、Codex 與 Mistral Vibe 未有文件説明的工作階段日誌格式,並説明如何將記錄整理成共用事件時間線。Claude Code 與 Codex 每個工作階段或執行緒使用 JSONL 檔案,Mistral Vibe 自 2.26 起則按引擎採用兩種格式。
Oct 16, 2026 · @arthur
Why read the logs at all
Claude Code, Codex and Mistral Vibe all write everything they do to disk, as they do it, and none of them documents the format. If you run a few sessions that spawn sub-agents, those files are the only place that knows which agent is still working, which one is waiting on another, and which session is sitting on a prompt for you.
I build Agent Standup, an open-source tool that reads those files and plays each session back as a live call. This article is what I learned reading them: where each tool keeps its sessions, which records matter, how sub-agents show up, and how to keep a reader correct when a format you don't control changes under you. Everything here is read-only: the reader never writes in these folders and never calls a model.
The formats below were checked against real sessions in October 2026: Claude Code 2.1, Codex (desktop app and CLI), and Mistral Vibe 2.26.1. They are undocumented, so treat every field name as something that can change.
Claude Code: one JSONL file per session
Claude Code keeps each session in ~/.claude/projects/<folder>/<session-id>.jsonl. The folder is the project's path with every character that isn't a letter or a digit turned into -, so /Users/me/shop-api becomes -Users-me-shop-api.
Each line is one JSON record, and its type says what it is. user and assistant records carry the conversation. Others you'll meet: custom-title and ai-title (the session's name), system, attachment, queue-operation, pr-link and result.
An assistant message holds a list of content blocks: text, thinking and tool_use (a tool name, an id and its input). A trimmed example:
{"type":"assistant","timestamp":"2026-10-15T09:12:03.120Z",
"message":{"id":"msg_01…","model":"…","stop_reason":"tool_use",
"usage":{"input_tokens":12,"output_tokens":340,"cache_read_input_tokens":20480,"cache_creation_input_tokens":512},
"content":[{"type":"tool_use","id":"toolu_01…","name":"Bash","input":{"command":"npm test"}}]}}
The tool's answer comes back in a later user record, as a tool_result block with the same tool_use_id. Claude Code often writes a structured toolUseResult beside it too, which holds what the text doesn't: a published artifact's link and id, for example.
What tripped me up:
-
Token usage is repeated. The same assistant message is written once per content block, each time with identical
usagenumbers. Count usage once permessage.id, or a message with five blocks costs five times as much. -
The end of a turn is a mark, not a silence. The lead's last message of a turn has
stop_reason: "end_turn". Until that mark arrives, the lead is still working, even if nothing has been written for a minute. Pressing Esc shows up instead as a user message starting with[Request interrupted. -
Not everything in a user record was typed. User messages also carry
<system-reminder>blocks, slash-command and task-notification wrappers,isMetarecords and the summary written after a compaction. Filter them out before showing "what you said". -
Sub-agents have their own files. The lead starts one with the
Agenttool (Taskin older versions), and each sub-agent writes its own transcript in<session-id>/subagents/*.jsonl. Agents launched by a workflow go one folder deeper, insubagents/workflows/<run>/agent-*.jsonl. A sub-agent's first user message is its order. -
Sub-agents changed how they report. Since Claude Code 2.1.293 a sub-agent hands its report back through a
SubagentHandbacktool call, and often writes no final text of its own. A reader that only looks at text blocks silently loses every report.
Codex: one rollout file per thread
Codex, both the desktop app and the codex CLI, writes one "rollout" file per thread in ~/.codex/sessions/YYYY/MM/DD/rollout-<time>-<thread-id>.jsonl (or under $CODEX_HOME). Thread names live separately, in session_index.jsonl. You don't need anything else from the Codex folder.
The first line of every rollout is a session_meta record, and it carries the fields that tell you what the thread is: id, cwd, cli_version, originator, source and, for a child thread, parent_thread_id.
{"type":"session_meta","payload":{"id":"01a1…","cwd":"/Users/me/shop-api","cli_version":"…",
"source":{"subagent":{"other":"guardian"}},"parent_thread_id":"01a0…"}}
What I learned:
-
sourcesorts threads. A thread you started has a plain string ("vscode"in my sessions). A sub-agent has an object,{"subagent": {...}}, and its parent is inparent_thread_id. Link children to parents with that, never with file names or times. -
Most sub-agent threads aren't yours. On my machine, 34 of 57 rollouts were
{"subagent":{"other":"guardian"}}: the short automatic reviews Codex runs before some commands. Showing them as sessions floods the list, so I hide them. -
There is an official way to ask for the same lineage. Codex's App Server API (the JSON-RPC interface its IDE integrations use) can list a thread's children with
thread/listand the filterssourceKinds: ["subAgentThreadSpawn"]andparentThreadId, behind its experimental flag. It means running a Codex process, though; readingsession_metagets you the same link with no process at all. - A command appears when it finishes. Codex writes a command down once it's done, with its output and exit status, so a live view shows it after the fact, not while it runs.
- What maps cleanly to a common timeline: your prompts, the agent's messages, commands with their output, file changes, web searches, MCP calls, the questions it asks you, reasoning summaries, token counts and the end of each turn.
Mistral Vibe: two engines, two formats
Vibe keeps its sessions in ~/.vibe/logs/session (or under $VIBE_HOME), and since version 2.26 it writes them in two different ways depending on the engine. A reader has to handle both, because people still run the old one.
The current engine (the default in 2.26) writes logs/session/unified/<id>/, and it works like an event-sourced store:
-
CURRENTnames the latest snapshot, which Vibe calls a generation. - That generation's manifest lists the pages of history (
chunks/<sha>.json, each an array of entries) and the journal that follows them (journal/<n>.jsonl). - Each journal line is a
projection_deltathat appends, replaces or removes entries, until the next snapshot.
So reading a session means loading the snapshot's chunks, then replaying the journal in order. An entry is a message (yours or the agent's), a piece of reasoning, or an "effect": a tool call with its input, its state, its result, and the times it started and ended. Every event has its time, so a replay is exact.
Sub-agents leave no history of their own in this engine. All you know about one is what the lead did with it: subagent.spawn, .send_message, .wait (with the answer it got back) and .stop. You can show who was asked what and what they answered, but not each of their commands.
The earlier engine (vibe --legacy-harness, and older versions) writes <prefix>_<date>_<time>_<id>/ with a messages.jsonl (one message per line) and a meta.json. Each sub-agent is a session of its own in an agents/ folder inside it, so here you do see a sub-agent's commands. The catch: no message has a timestamp, and the file is saved at the end of each step. Followed live, times are right to within a step; read afterwards, you have to spread the events between the session's start and end.
Two more details:
- Vibe records when it's waiting for you, for an approval or an answer, so a reader can mark that session as "needs you" without guessing from silence.
-
The location can move:
session_logging.save_dirin Vibe's settings sends logs somewhere else, so let users point the reader at another folder.
One event list for three tools
[embedded content: How Agent Standup reads three tools: three formats, three adapters, one event list, four views]
Each adapter has a small job: turn its tool's records into the same handful of events (something said, something typed, tool calls, their results, token usage, the end of a turn, a compaction). Everything after that is shared: the live call, the board of sessions, the sub-agent tree and the replay. Supporting a fourth tool means writing one adapter, not four features.
The reader is incremental. Each file is parsed once, then only the bytes appended since the last read: the reader keeps an offset per file, plus the last bytes it read. If those bytes are no longer where they were, the file was rewritten rather than appended to, and it's read again from the start. A session is the merge of its main file and one file per sub-agent, each cached on its own, so following a busy session costs a few small reads every couple of seconds, not a full parse.
Staying correct when the formats change
Three tools, no documentation, and each ships often. What kept the reader honest:
- Write each adapter against real files, never against a guess or a blog post. Then keep a few anonymised samples as test fixtures, so a broken assumption fails a test instead of a user's screen.
- Diff the old and new versions on real sessions before every release. I run the previous release and the candidate side by side, each as its own process, on the roughly 120 sessions on my machine, and compare every API response. For the last release that was 76,943 events, all identical except for the changes I meant to make. The test suite didn't catch everything this catches.
- Isolate the readers. One odd Vibe session once took the whole session list down, for every tool, and only an adversarial review before the release caught it. Now each reader other than Claude Code's runs inside a guard: a file it can't read costs one session, not all of them.
-
Expect new fields to change meaning. The
SubagentHandbackcall (Claude Code 2.1.293) didn't break anything visibly; sub-agents just stopped reporting back in the call. Silent changes like that only show up when you compare against real sessions. - Never write in the tools' folders. A reader that only reads can't corrupt a session, and users trust it with their history.
Try it, and help fill the gaps
Agent Standup is free and MIT licensed. It's plain Node with no runtime dependencies, and it runs as a VS Code extension (also on Open VSX for Cursor and VSCodium), a terminal command, a desktop app or a Claude Code plugin. To try it with a built-in demo, no install needed (Node 18 or later):
npx @double_geste/agent-standup --open
The adapters are in the repo (codex.js, vibe.js, and the Claude Code reader in server.js), if you want to reuse the parsing for something else.
Three gaps where real sample files would help, with ids and paths redacted:
- a Codex rollout where Codex spawned a sub-agent for a task (just its
session_metaline); - a Vibe session from the current engine where a sub-agent's own steps are kept anywhere;
- sessions from Gemini CLI or OpenCode, the next tools worth reading.
Issues and samples are welcome at github.com/Atoll92/agent-standup.
Not affiliated with Anthropic, OpenAI or Mistral AI.
來源:DEV Community · dev.to
