Agent Harness 源码拆解(七):持久化与恢复——崩溃之后,怎么知道工具到底跑没跑
持久化是 harness 里最”安静”也最”贵”的模块。安静,因为正常情况下它不发声;贵,因为崩溃那一刻它会问你一个几乎无法回答的问题:这个工具,刚才到底跑没跑?
Maka 把这问题展开得很清楚——模型让 agent 把 config.json 的端口从 3000 改成 4000,工具开始写文件时应用恰好崩溃。重启后,缺失的结果至少有四种解释:工具根本没启动;启动了但没写;文件已改但结果没提交;文件改了又被别人改回来。总是重试会重复副作用,总是宣布成功会给模型一段虚假历史。
这一章看六家怎么回答这个问题。
那个地基:事件日志是事实,状态是投影
六家殊途同归,全部选择事件溯源:每一步(消息、工具调用、权限决策)落成 append-only 事件,状态从日志重放而来。这个地基第 04 章已经反复出现,这里只补一句它的崩溃语义:写一半只坏最后一行。append-only 文本天然崩溃安全,这就是为什么 pi 和 grok 都用 JSONL 而不是数据库当主存储——git 能 diff、grep 能搜、用户能手改。
还要补一个相关的判断:持久化要回答的不只是”崩溃后怎么办”,还有”跨会话怎么连续”。Anthropic 在跑长任务 agent 时发现一个反直觉的结论——光有 compaction 是不够的。即使顶级模型在循环里跑,给它一句高层指令(比如”造一个 claude.ai 克隆”),它也会要么试图一次性做完(上下文耗尽,把半成品留给下个会话),要么中途看进度不错就提前宣布完工。他们的解法是 initializer agent(首轮搭环境)+ 每个会话留下 claude-progress.txt 和 git 历史,让下一个”零记忆”的会话快速重建工作状态。这把”持久化”从崩溃恢复,扩展到了”跨上下文窗口的交接班”。
这里值得再拆一层:compaction 和 context reset 是两件事,解决的是不同的问题。compaction 是原地摘要、保留连续性,同一个 agent 拿着缩短的历史继续跑;context reset 是清空窗口、另起一个干净 agent,靠结构化的交接工件把状态传下去。Anthropic 在后续实验里明确指出,某些模型(如 Sonnet 4.5)会表现出 context anxiety(上下文焦虑)——感知到接近上下文上限就急着收尾——这种情况下 compaction 只能缓解、无法根治,reset 才提供真正的”干净起点”,代价是每次 reset 都要多付一层编排复杂度和 token 开销。六家里的 CodeWhale 检查点、grok 单 leader 的会话切分,本质上都在这个问题上做了各自的取舍。
pi:会话是一棵 JSONL 树,fork 零复制
pi 的会话文件是版本化的 JSONL,条目带 { type, id, parentId, timestamp }。这个 parentId 是点睛之笔:让”分支”只是叶子指针的移动。
于是 /tree 是在同一文件里切换活动叶子(不删离开的分支),/fork 是复制历史成新文件,编辑早先的 user message 自动产生新分支。fork 零复制、回滚不用合并算法——因为整棵历史就躺在同一个文件里。切分支时还有个贴心设计:收集旧叶子到共同祖先的条目,让 LLM 生成一个 branch summary 挂到新分支尖端,切分支不失忆。
喂给模型的永远是”活动分支这一条时间线”,树是给用户的历史管理结构——喂投影,存全量。
新一代 durable 层把存储升级成”原子提交三类记录”:不可变的 entries(transcript)、追加的 tasks(状态机)、可变的 documents(应用状态)。不变式写得很硬:一切可见进度皆耐久,没有”可见但不耐久”的发布路径。
代价:这套 durable 层约 2 万行,从旧实现整体重写而来,是 pi 复杂度最集中的地方。
Maka:Resume 不是 Retry,副作用用短事务包住
Maka 对崩溃恢复的贡献,是把”resume”拆成三个不能压缩成 resume=true 的问题:旧 Run 怎么关闭?每个工具操作处于什么状态?模型还能不能继续?
五条规则里最关键的两条:Resume 创建新执行(不复活旧 socket、Promise、JS 栈);缺失结果不是失败,也不证明工具没跑——证据缺失时停在”不知道”,而不是猜一个答案。
副作用怎么算账,靠 T1/T2 两个短事务:T1(dispatch 可能已开始)和 T2(结果已确定)两个 SQLite 事务,包住不可预测长的外部副作用。T1-有-T2-无 的区间被诚实建模为 indeterminate(未知是第三态),由工具自带的 recoveryMode 解释。这样每个副作用窗口都能回答”跑了没跑”。
代价:这套 T1/T2 + recoveryMode + 三段恢复(修复→解决→继续)是 Maka 里最重的一块,适合”agent 有钱、有副作用、要过夜”的场景。
opencode:事件溯源做成正式系统,投影是读模型
opencode 把事件溯源做成了一个小而完整的系统:所有会话状态按 (aggregate, seq) 提交,读模型是投影,客户端订阅是日志 tail。
它的一个技巧值得记:commit 钩子让事件和投影原子提交——durable 事件写入的事务里顺带执行投影更新,重放时跳过已有投影,于是投影和日志永远一致。
但它也诚实标注了成本:投影层的厚度。事件溯源的收益(重放/订阅/分页/恢复统一)伴随着版本化事件、owner claim、重放一致性、投影器维护的持续成本——“当前 V2 代码里最难读的部分全在投影层”。
CodeWhale:检查点 + 离线队列 + side-git 快照
CodeWhale 的恢复走”检查点 + 快照”路线:发用户输入前写检查点;动作型 turn 前后各一次 side-git 工作区快照;降级/离线时新 prompt 入内存队列并镜像到离线队列文件。
最有意思的是它把文件回滚和历史回滚拆成两个独立操作:/restore 从 side-git 快照恢复文件状态,但”restore file state without changing conversation history or the user’s .git“——快照留在自己的 .git 里,用户的仓库不受污染。这对照它的三分法:fork 新会话 / Esc-Esc 回溯转录 / /restore 只动文件。
还有一条少见的细节:模式持久化的写入发生在事件循环之外,失败时 toast 告警而不是下次启动静默回退,多个来源共享一个序列化写者——“a burst of Tab presses cannot end up persisting whichever write happened to finish last”。
grok-build:一台机器一个权威进程
grok-build 的持久化挂在它的进程拓扑上:单 leader,多客户端,一个权威。TUI、IDE 插件、headless 脚本全部挂同一个 Agent 进程,会话状态天然共享(IDE 里开着的会话,终端里一连就能接管)。
代价是 leader 的健壮性即全局健壮性,所以才有 crash-handler、名册合并、请求 ID 命名空间化。
它还有一个血泪教训:NFS 上 WAL 会 SIGBUS,所以 journal 模式按文件系统选型,每主机独立 DB,还对旧版本二进制做防御。harness 的持久化不能假设”我的盘是本地盘”。
如果你要设计持久化
- 个人工具、要可审计可 fork:学 pi,JSONL 树 + parentId 分支,喂投影存全量。
- 有副作用、要崩溃恢复:学 Maka,T1/T2 短事务包副作用窗口,未知是第三态。
- 要正式的事件溯源:学 opencode,但要认清投影层的厚度成本。
- 要文件回滚:学 CodeWhale,side-git 快照,文件回滚和历史回滚分离。
四个最容易踩的翻车点:
一是删除消息当压缩。丢了原始事实,回放审计时发现真相早没了。
二是总是重试或总是宣布成功。崩溃恢复里两害相权,唯一正确的是”停在不知道”。
三是把 resume 当 retry。复活旧进程状态,而不是从日志重建新执行。
四是假设盘是本地盘。NFS 上 WAL 直接 SIGBUS,持久化要按文件系统选型。
下一篇讲 多 Agent:什么时候拆子代理、怎么保证子代理不放大权限——CodeWhale 的 fleet、grok 的 subagents、Maka 的”子 Session 是操作员容器”。
本篇术语
- 事件溯源(event sourcing):append-only 事件日志是唯一事实,状态从日志重放
- 投影(projection):从日志现算出来的视图(模型上下文、UI、恢复状态)
- branch / fork:会话历史的分支;
parentId让分支只是叶子指针移动,fork 零复制 - T1/T2 事务:两个短事务包住不可预测的外部副作用,中间区间建模为”未知”
- indeterminate(未知第三态):副作用”可能已开始但结果未确定”的诚实建模
- side-git 快照:独立于用户仓库的 git 快照,用于文件回滚而不污染用户
.git - 单 leader:一台机器一个权威进程,多客户端共享会话状态
参考资料
参照系仓库(结论基于 2026-09 下旬至 10 月上旬各仓库 HEAD):
- earendil-works/pi ——
session-manager.tsJSONL 树与 pi-durable - anomalyco/opencode ——
event.tsEventV2 与投影 - xai-org/grok-build —— 单 leader 与 JSONL 追加
- zai-org/ZCode
- apache/maka ——
recovery-resolver.ts三段恢复 - Hmbown/CodeWhale —— 检查点与 side-git 快照
Harness 设计方法论文献:
- Anthropic,《Agent Harness Design: 3 Patterns for Harnessing Claude’s Intelligence》
- Anthropic,《Effective harnesses for long-running agents》(2025-11)——“compaction 不够用”、initializer + 进度文件跨会话交接的官方复盘
- Anthropic,《Harness design for long-running application development》——context reset 与 compaction 的区分、context anxiety 的官方论述