怎样读懂 Claude Code 的 JSONL 会话记录,而不是靠猜

Claude Code 的会话记录是有用的本地证据,但它不是一串长得一样的聊天消息。每一行都是一个独立的 JSON 对象,一行的含义由它的顶层类型和嵌套字段共同决定

先看信封

一次读一行。空行、残行或畸形行在本轮应当被忽略,而不是把整次扫描搞崩。记录可能正在被写的同时被读,所以一个不完整的末行在下一次文件变化时可能就变成合法的了

Agent Island 在分类 Claude 活动时用到的字段包括 typeuuidtimestampmessage.stop_reasonisSidechainisApiErrorMessagetoolEndsTurn。不是每一行都含有每一个字段,而某个字段缺失并不构成编造状态的许可

for each line:
  if blank: continue
  event = parse JSON
  if parsing fails: continue
  classify from the fields that are actually present

把用户活动和助手输出分开

一个用户事件是「之后有一个回合开始了」的证据。助手输出是「Agent 产出了工作」的证据,但它不自动等于整个会话已经结束。解析器必须保住事件顺序,好让之后的用户或开始活动能取代更早的完成标记

uuid 和时间戳帮助保住身份与排序。它们不该被文件修改时间取代:一个文件时间戳描述的是容器,而事件时间戳描述的是一条条记录

把 sidechain 当成另一个语境

isSidechain 标记标出的是不该被当成主要用户交接的活动。一个后台子 Agent 可能在父会话继续跑的时候结束。对两者发同一种闹钟,会在主任务还不需要人的时候就把用户拽回来

在产生前台的「需要你」状态之前先过滤掉 sidechain 事件。它们在诊断上仍然有用,但不等价于一次主线程交接

别把 API 错误变成完成

一个长得像助手输出的信封,可能装的是 API 或限流错误。所以 isApiErrorMessage 是一道闸门,不是装饰。错误输出应当进入对应的错误状态,绝不能被解读成一次成功完成的回合

同理,message.stop_reasontoolEndsTurn 需要上下文。一个终止标记可以描述某一次模型响应,但它证明不了之后不存在别的工具、用户或开始事件

重建状态,而不是 grep 一个词

稳健的读取器会把有序事件归约成一个会话状态:

  1. 解析合法记录,且不让整个文件失败
  2. 保住事件身份与语义时间戳
  3. 把 sidechain 交接排除在前台提醒之外
  4. 单独归类 API 错误
  5. 只有在之后没有活动取代它时,才接受一个完成候选

这就是为什么在记录里搜 stop_reasoncompleted 是不够的。一次词匹配没有回合身份,没有排序策略,也没有错误闸门

把隐私声称说窄一点

Agent Island 在本地读取这些会话记录来重建状态,不把会话内容上传到 Agent Island 的服务。实际发布的行为可以在开源的 v1.7.1 代码里审计;本地解析并不意味着所有可能的 Claude Code 记录版本都有完全一样的 schema