[控场AI]
· 12 分钟阅读· 6,287 字

解读Claude Code、Codex与Mistral Vibe未公开会话日志

解读Claude Code、Codex与Mistral Vibe未公开会话日志

逆向读取三款 AI 编码工具的未文档化日志,用适配器统一成事件列表并回放会话。

文章介绍开源工具 Agent Standup,它只读地解析 Claude Code、Codex 和 Mistral Vibe 实时写入磁盘但官方不提供文档的日志。三种格式差异巨大:Claude Code 每会话一个 JSONL,assistant 消息按内容块重复写入需按 message.id 去重 token,sub-agent 自 2.1.293 起改用 SubagentHandback 回传报告;Codex 每线程一个 rollout,靠 session_meta 的 source 与 parent_thread_id 判断父子线程;Vibe 2.26 起新旧引擎并存,当前引擎为事件溯源的快照加日志且不保留 sub-agent 自身历史。作者用适配器模式把三者统一成一份事件列表,并通过对着真实文件写解析、发版前在约 120 个会话上比对 76,943 个事件、隔离各读取器来应对无文档且频繁变更的格式。

为什么有人会去读这些没人写文档的日志

Claude Code、Codex 和 Mistral Vibe 会把运行过程中的几乎所有动作实时写到磁盘,但三家的官方文档都没有说明这些文件的格式。只要你跑过几轮会派生 sub-agent 的会话,就会发现磁盘上的这些文件是唯一知道"哪个 agent 还在干活、哪个在等另一个、哪个会话正卡在等你输入"的地方。

dev.to 作者 @arthur 为此做了个开源工具 Agent Standup,专门读取这些文件,把每个会话回放成一次实时通话。下面这些结论都来自他直接读日志的实践,覆盖 Claude Code 2.1、Codex(桌面端与 CLI)以及 Mistral Vibe 2.26.1,全部是只读操作——读取器不会往这些目录里写任何东西,也不会调用任何模型。

Claude Code:每个会话一个 JSONL 文件

Claude Code 把每个会话存放在 ~/.claude/projects/<folder>/<session-id>.jsonl。文件夹名是项目路径里所有非字母数字字符替换成 - 得到的结果,所以 /Users/me/shop-api 会变成 -Users-me-shop-api。

每行是一条 JSON 记录,用 type 字段区分类型:user 和 assistant 承载对话,此外还会遇到 custom-title、ai-title(会话名)、system、attachment、queue-operation、pr-link、result 等类型。assistant 消息里是一个内容块列表,包含 text、thinking 和 tool_use(工具名、id 和输入)。

工具的执行结果稍后回写进一条 user 记录,以 tool_result 块出现,用同一个 tool_use_id 关联。Claude Code 往往还会在旁边写一份结构化的 toolUseResult,里面装着文本里没有的信息,比如生成产物的链接和 id。

最容易踩的坑集中在几处。同一 assistant 消息会按内容块重复写入,每次带着完全相同的 usage 数字,所以统计 token 要按 message.id 去重,否则一条五个块的消息会算成五倍消耗。一轮对话的结束是一个明确的标记而非静默,lead 那条消息会带 stop_reason: "end_turn",在这个标记到达之前,哪怕一分钟没写入任何东西,它仍在工作;按 Esc 中断则会表现为一条以 [Request interrupted 开头的 user 消息。

user 记录里的内容也不全是用户输入的,还混杂着 <system-reminder> 块、斜杠命令和任务通知的包装、isMeta 记录以及压缩之后写入的摘要,展示"你说了什么"之前得先把它们过滤掉。

sub-agent 有自己的文件。lead 通过 Agent 工具(旧版本叫 Task)启动它们,每个 sub-agent 在自己的 <session-id>/subagents/*.jsonl 里写独立记录;由 workflow 启动的 agent 会再深一层,落在 subagents/workflows/<run>/agent-*.jsonl。sub-agent 的第一条 user 消息就是它收到的指令。需要注意的是,从 Claude Code 2.1.293 起,sub-agent 改用 SubagentHandback 工具调用回传报告,往往不再写自己的最终文本,只读 text 块的读者会悄无声息地丢掉每一份报告。

JSONL(JSON Lines)是一种每行一个独立 JSON 对象的文本格式,也常被称作 NDJSON。相比把整个数组序列化成一个 JSON 文件,它的关键优势是增量可读:追加一条记录只需在文件末尾写一行,读取方不必加载全部内容,只要记住字节偏移即可继续消费新增部分。这正是文中读取器采用 offset + 末尾字节重读策略的基础。

日志场景里 JSONL 还有一个隐含契约:每行自包含、字段可缺省,因此解析方必须对未知的 type 或新增字段保持容忍,不能因为出现没见过的记录就整段失败。Claude Code 频繁加入 custom-title、queue-operation 这类新类型,Codex 和 Vibe 也在改结构,正是这一契约让适配器可以逐条过滤而无须理解全部语义。

Codex:每个线程一个 rollout 文件

Codex 的桌面端和 codex CLI 都为每个线程写一个 rollout 文件,路径是 ~/.codex/sessions/YYYY/MM/DD/rollout-<time>-<thread-id>.jsonl(或 $CODEX_HOME 下)。线程名单独放在 session_index.jsonl,Codex 目录里其余的东西都不需要读。

每个 rollout 的第一行是 session_meta 记录,它带着判断线程身份的关键字段:id、cwd、cli_version、originator、source,以及子线程才有的 parent_thread_id。

source 字段用来给线程分类。你自己启动的线程是一个普通字符串(作者机器上是 "vscode"),sub-agent 则是一个对象 {"subagent": {...}},父线程记在 parent_thread_id 里。关联父子必须靠这两个字段,绝不能靠文件名或时间。作者机器上 57 个 rollout 里有 34 个是 {"subagent":{"other":"guardian"}},即 Codex 执行某些命令前自动跑的短审查,把它们当会话展示会淹没列表,所以他选择隐藏。

Codex 的 App Server API(IDE 集成使用的 JSON-RPC 接口)在实验开关后提供了官方途径查询同样的血缘关系:用 thread/list 配合过滤条件 sourceKinds: ["subAgentThreadSpawn"] 和 parentThreadId。但这需要跑一个 Codex 进程,而直接读 session_meta 不启动任何进程就能拿到同样的关联。

另一个限制是,Codex 在命令执行结束后才把它写下来,连同输出和退出码,所以实时视图里看到的命令是事后呈现,而不是执行中。能干净映射到通用时间线的包括:你的提示、agent 的消息、命令及其输出、文件变更、网络搜索、MCP 调用、它向你提出的问题、推理摘要、token 计数,以及每一轮的结束。

Mistral Vibe:两套引擎,两种格式

Vibe 把会话放在 ~/.vibe/logs/session(或 $VIBE_HOME 下),从 2.26 版起按引擎不同用两种写法,读取器必须同时支持,因为仍有人在跑旧引擎。

当前引擎(2.26 默认)写入 logs/session/unified/<id>/,结构类似事件溯源存储:CURRENT 指向最新快照(Vibe 称为 generation);该 generation 的 manifest 列出历史分页(chunks/<sha>.json,每个是条目数组)和紧随其后的日志(journal/<n>.jsonl);journal 每行是一个 projection_delta,追加、替换或删除条目,直到下一个快照。读一个会话就是先加载快照的 chunks,再按顺序重放 journal。条目可以是消息(你的或 agent 的)、一段推理,或者一个 "effect"(带输入、状态、结果以及起止时间的工具调用)。每个事件都带时间,所以重放是精确的。

但在这个引擎里,sub-agent 不留下自己的历史。你对它的全部了解就是 lead 对它做了什么:subagent.spawn、.send_message、.wait(带回来的答案)和 .stop。你能展示谁被要求做了什么、回答了什么,但看不到它执行的每条命令。

早期引擎(vibe --legacy-harness 和更早版本)写的是 <prefix>_<date>_<time>_<id>/,里面有 messages.jsonl(每行一条消息)和 meta.json。每个 sub-agent 是其中 agents/ 文件夹下的独立会话,所以这里能看到 sub-agent 的命令。代价是:没有一条消息带时间戳,文件在每一步结束时保存。实时跟随时时间精确到步,事后读取则只能在会话起止之间摊开事件。

还有两个细节:Vibe 会记录自己正在等你(等批准或等回答),所以读取器能把该会话标成 "needs you",不用靠沉默去猜;位置也可能移动,Vibe 设置里的 session_logging.save_dir 会把日志写到别处,读取器得允许用户指定其他文件夹。

事件溯源(event sourcing)是一种持久化范式:系统不直接保存最终状态,而是保存导致状态变化的全部事件序列,需要当前状态时从某个初始快照起按顺序重放事件得到。它有两大好处——审计完整、可回溯任意历史时点的状态;代价是读取需要重放,且要处理快照与后续日志的衔接。

Vibe 当前引擎正照此设计:chunks/<sha>.json 相当于快照(generation),journal/<n>.jsonl 里每一行 projection_delta 是追加、替换或删除条目的增量事件,重放完 journal 就得到最新状态。由于每个事件带时间戳,重放出的时间线是精确的。理解这一点才能明白为什么读取器必须"先加载 chunks,再按顺序重放 journal",以及为什么增量跟随时可以只消费 journal 尾部的新 delta。

把三种工具统一成一份事件列表

Agent Standup 如何读取三种工具:三种格式,三个适配器,一份事件列表,四种视图

每个适配器只做一件小事:把本工具的记录转成同一组事件(说了什么、输入了什么、工具调用、调用结果、token 用量、一轮结束、一次压缩)。之后的一切都是共享的:实时通话、会话看板、sub-agent 树和回放。支持第四种工具意味着写一个适配器,而不是四个功能。

读取器是增量的。每个文件只解析一次,之后只读取上次读取之后追加的字节:每文件维护一个 offset 和上次读到的最后几个字节。如果这些字节已经不在原位,说明文件是被重写而不是追加,就从头重读。一个会话是主文件和每个 sub-agent 各自文件的合并,分别缓存,所以跟踪一个繁忙会话每几秒只需几次小读取,而不是完整解析。

适配器模式(Adapter Pattern)是一种结构型设计模式,用于把某个类或数据源的接口转换成调用方期望的另一种接口,使接口不兼容的双方能协同工作。它在处理多来源异构数据时尤其常见:每个来源写一个适配器负责翻译,上层逻辑只面向统一接口编程。

本文中,Claude Code、Codex、Mistral Vibe 三种日志格式各异,每个适配器只负责把它们翻译成同一组事件(说了什么、输入、工具调用、结果、token 用量、一轮结束、压缩),之后实时通话、会话看板、sub-agent 树和回放全部共享,无须为每种工具各写一遍。其工程价值在于:新增一种工具只需增加一个适配器,而不是在四个功能里各改一遍,从而把"格式变化"的影响局部化——这正是应对零文档、频繁改版的三款工具的关键结构。

格式变了,怎么保证读取器不出错

三款工具、零文档、且都发版频繁。作者保持读取器正确性的做法包括:每个适配器都对着真实文件写,绝不靠猜测或博文,然后把若干脱敏样本留作测试 fixture,让错误假设在测试里失败而不是在用户屏幕上失败。每次发版前用真实会话做新旧版本 diff,把上一个发布版和候选版各跑一个进程,在机器上约 120 个会话上比对每一个 API 响应;上个版本比对的是 76,943 个事件,除了有意改动之外全部一致,这是测试套件抓不到的。

读取器还要互相隔离。曾经有一个异常的 Vibe 会话把整个会话列表拖垮,波及所有工具,靠发版前的一次对抗性审查才发现。现在除 Claude Code 外的每个读取器都跑在保护里:一个读不了的文件只损失一个会话,而不是全部。

还要预期新字段改变含义。SubagentHandback(Claude Code 2.1.293)没有破坏任何可见的东西,sub-agent 只是不再在调用里报告了,这类静默变化只有跟真实会话比对时才会浮现。最后一条底线:绝不在工具的目录里写入,只读的读取器不可能损坏会话,用户才愿意把历史交给它。

测试 fixture(测试夹具)指在测试前准备好、供多个用例复用的固定输入数据或环境状态。它的核心价值是可重复性:同一份输入每次都产生同样的结果,使回归测试能稳定地捕捉行为变化。在解析器这类程序中,fixture 通常是若干代表性输入文件及其期望输出。

作者把脱敏后的真实会话日志留作 fixture,让基于错误假设写出的解析逻辑在测试阶段失败,而不是等用户在使用中撞上。需要强调的是,fixture 只能覆盖已收集的样本形态,无法保证对未来未知格式仍然正确,因此还需要配合发版前对真实会话做新旧版本 diff(文中比对约 120 个会话、76,943 个事件),两者一静一动,才能较好地覆盖零文档、频繁变更场景下的正确性风险。

试用与待补的缺口

Agent Standup 免费且 MIT 许可,纯 Node 无运行时依赖,可以跑成 VS Code 扩展(Open VSX 上也有,支持 Cursor 和 VSCodium)、终端命令、桌面应用或 Claude Code 插件。想用内置 demo 试用,无需安装(要求 Node 18+):

npx @double_geste/agent-standup --open

适配器都在仓库里(codex.js、vibe.js,Claude Code 读取器在 server.js),可以复用解析逻辑做别的事情。作者还列出三个缺口,欢迎提供脱敏样本:Codex 派生 sub-agent 执行任务的 rollout(只要它的 session_meta 行)、当前引擎下保留了 sub-agent 自身步骤的 Vibe 会话、以及 Gemini CLI 或 OpenCode 的会话。工具与 Anthropic、OpenAI、Mistral AI 均无关联。

#Claude Code 会话日志#Codex rollout 文件#Mistral Vibe 日志格式#AI 编程工具 sub-agent#JSONL 解析#Agent Standup#AI agent 可观测性
分享:

相关推荐