底层机制:JSONL Session 解析如何驱动 Agent 路由
深入剖析 MadoHub 如何读取 Claude Code 的 session 日志,以提取干净、结构化的响应,用于智能路由。
Agent 路由里最难的问题,并不是给目标 Agent 生成提示词,而是从源 Agent 那里提取一段干净的响应。
终端输出很脏。ANSI 转义码、进度 spinner、换行折行、颜色序列 - 直接抓终端就像面对一堵噪音墙。你可以去掉控制码,但还是会留下格式化残留、半截行和混杂内容。
MadoHub 用的是另一种方式。我们不抓终端输出,而是读取 Claude Code 的结构化 session 日志 - 那些 JSONL 文件里保存着每一轮对话的干净语义表示。
这篇文章会解释它是怎么工作的。
终端抓取的问题
当 Claude Code 在终端里运行时,它的输出包括:
- ANSI 转义序列 - 用于颜色、光标定位和格式化。
- Unicode 标记 - 比如 ❯(提示符)、✽(思考)、⏺(工具使用)。
- 进度指示器 - 会覆盖前面的行。
- 权限对话框 - 带有交互元素。
- 多行代码块 - 带语法高亮转义码。
去掉 ANSI 码会让文本更干净,但还不够。进度指示会留下半截行。代码块会丢失结构。"Agent 输出" 和 "shell 噪音" 的边界也会变得模糊。
对于简单路由(比如“发送最后 10 行”),抓终端输出已经够用了。但对于智能路由 - 也就是 AI 模型需要理解 Agent 实际做了什么,并生成上下文提示词 - 你需要干净、结构化的数据。
Claude Code 如何存储 session
Claude Code 会用 JSONL 格式写结构化 session 日志(每一行一个 JSON 对象)。解析路径大概是:
tmux pane → ps process-tree BFS
→ agent PID (first descendant whose command matches claude/codex/amp/opencode)
→ ~/.claude/sessions/{PID}.json
→ sessionId field
→ ~/.claude/projects/*/{sessionId}.jsonl
→ conversation turns (user/assistant messages)每一轮对话都包含消息角色、内容和元数据 - 没有任何终端格式信息。这也是 Claude Code 内部用来维护对话上下文的数据。
解析链路
MadoHub 通过一个多步骤链路来解析 JSONL 文件:
第 1 步:获取 PTY 进程 ID
每个终端图块背后都是一个 tmux pane,MadoHub 会从终端创建那一刻起追踪该 pane 的 PID。这个 PID 是定位真正 Agent 进程的起点 —— 该进程通常是这个 pane shell 的子进程。
第 2 步:找到 Agent PID 及其 session 文件
Claude Code 在 ~/.claude/sessions/ 下维护一个 session 目录,但每个文件是以 Agent 的进程 ID 命名的 —— 而不是 session ID。为了找到这个 PID,MadoHub 会从 tmux pane 开始遍历进程树(用 ps -eo pid=,ppid=,comm= 对所有子孙进程做广度优先搜索),直到找到第一个命令名匹配已知 Agent(claude、codex、amp、opencode)的进程。然后 MadoHub 读取该 PID 对应的 ~/.claude/sessions/{PID}.json,其中包含一个指向实际对话日志的 sessionId 字段。
第 3 步:定位并读取 JSONL
MadoHub 拿到 session 文件里的 sessionId 后,会扫描 ~/.claude/projects/ 下的每个子目录,寻找名为 {sessionId}.jsonl 的文件 —— 那就是对话记录。MadoHub 解析它,并提取最后一条 assistant 消息 - 也就是 Claude Code 最近一次给出的响应。
这条消息是干净文本:没有 ANSI 码,没有终端残影,没有进度指示器。它就是 Claude Code “说”的内容,而且是它原本想呈现的格式。
第 4 步:解析失败时怎么办
如果进程树遍历找不到匹配的 Agent 进程,或者 session 文件已经消失(例如进程已退出),MadoHub 在这一轮就没有可用的 JSONL 路径。这时它会回退到终端输出提取 —— 和那些完全不产生 session 日志的 Agent 所用的 ANSI 去除方法一样。
干净响应能带来什么
有了干净、结构化的响应,路由 transform 就可以:
理解上下文:AI 路由模型拿到的是语义内容本身 - “我把认证中间件重构成了使用 JWT token,而不是 session cookie。改了 3 个文件:src/auth/middleware.ts、src/auth/tokens.ts 和 src/config/auth.ts” - 而不是一堆彩色终端输出。
生成精确提示词:"审查 src/auth/middleware.ts、src/auth/tokens.ts 和 src/config/auth.ts 中的 JWT token 迁移。检查 token 刷新流程是否正确处理了过期,并确认中间件正确验证了 token 签名。"
保留代码块:如果 Agent 的响应里包含代码片段,它们会保留原始格式 - 不会被终端换行和 ANSI 颜色搞坏。
缓存与性能
长会话里,JSONL 文件可能会变得很大。MadoHub 会按 tile ID 缓存最后一次解析出的响应,避免在每次路由触发时都重新解析整个文件。缓存会在检测到新的完成事件时失效。
解析链路也针对常见情况做了优化:进程树遍历和 session 文件读取都很快,而且绝大多数情况下都能解析成功。相比之下,扫描 ~/.claude/projects/ 寻找匹配的 {sessionId}.jsonl 文件是更耗时的一步,因为它得搜索每一个项目子目录。
局限性
基于 JSONL 的状态检测已经不再是 Claude Code 独有的了 —— Codex 现在也有自己的、基于 JSONL 的状态管线(task_complete → completed,turn_aborted/error → failed,user_message/task_started/agent_message → working,ri_message → stopped)。真正还有差异的地方是响应文本的提取:本文描述的这种干净、结构化的 session 日志格式 —— 直接从对话记录里读取最后一条 assistant 消息 —— 仍然是 Claude Code 独有的。对于 Codex 和 shell 命令,MadoHub 仍然会回退到终端输出提取并去除 ANSI 码,来获取实际的响应文本。
raw 和 full-output transform 类型适用于任何 Agent,因为它们直接使用终端输出。ai-routing 和 summary transform 在 Claude Code 上效果最好,因为它们能接收到干净的 JSONL 内容。
为什么这很重要
路由输出的质量,直接取决于提取出的响应质量。垃圾进,垃圾出。通过读取结构化 session 日志,而不是抓取终端输出,MadoHub 得到的是源 Agent 实际产出的最高保真版本。
这是一个会产生复利效应的设计决策。更好的提取 → 更好的 transform 提示词 → 更准确的目标 Agent 指令 → 更少的浪费轮次 → 更快的工作流收敛。对干净提取的投入,会在路由流水线的每一步都得到回报。