CHAPTER 11 · STAGE 03 · 理解可靠性与扩展
持久化、恢复、Fork、Replay 与 Compaction
ctx.sessions 管理进程内的 Session 和 append-only event log。进程退出后还想继续,就需要 ctx.sessionPersistence 把同一份 SessionEvent 写入持久后端。内存 Session 与磁盘存储是两层能力
ctx.sessions 管理进程内的 Session 和 append-only event log。进程退出后还想继续,就需要 ctx.sessionPersistence 把同一份 SessionEvent 写入持久后端。
项目没有定义第二套“数据库事件”。JSONL 与 SQLite 后端都保存核心 SessionEvent,只是物理编码不同。这样内存语义、持久语义和 replay 不需要维护三套相似模型。
为什么持久化不阻塞每次 Session.append()
Session 追加事件时会同步发出 session/event,持久化插件把事件复制到每个 Session 的写入控制器。它不会让 AgentLoop 在每个 token chunk 上等待磁盘。
第一个待写事件开启固定 batching window,期间的其他事件加入同一批。到期后后端执行 durable append;写入期间到达的新事件形成下一批。
这种设计减少小写入次数,但意味着“事件已经在内存日志中”不等于“已经落盘”。需要观察持久状态的调用方必须使用 flush checkpoint。
Flush:明确的耐久性观察点
session/flush 取消等待窗口并持续写到当前队列收敛。调用成功后,调用方才能依赖存储已经包含此前事件。
普通 Turn 边界本身不必同步等待每个写入;检查点策略和真正读取存储的消费者负责选择 flush 时机。Agent idle 也不自动等同于“所有后端都已 durable”,需要按 API 约定观察。
如果后台写入失败,待写事件不会丢弃。控制器暂停自动重试,新的事件可以重新启动窗口,显式 flush 则立即重试并把失败暴露给调用方。
JSONL 与 SQLite 的差异
JSONL 后端通常为每个 Session 维护独立 artifact,支持定位与读取原始文本。实际文件可以用 checksummed Zstandard frames 或原始行编码。
SQLite 后端把多个 Session 放在同一数据库中,每个 SessionEvent 对应一行。它没有“每个 Session 一个文件”的位置,因此 locate 可以返回 undefined。
消费者依赖 SessionPersistence API,不应该假设所有后端都有可直接打开的 JSONL 文件。
Crash Recovery:保留已写事件,补齐中断边界
进程可能在 turn/start 之后、turn/end 之前崩溃。后端重新加载冷 Session 时不会截断整个开放 Turn,因为其中可能已有很多成功写入的 Step 和工具结果。
恢复逻辑保留已有事件,并在逻辑视图中补一个 turn/end,原因是 interrupted。这个原因由恢复产生,正常 Loop 不会主动写它。
这样日志重新获得平衡边界,同时不伪造“之前的工具没有执行”。已经 durable 的事实继续存在。
为什么只修复冷 Session
活跃 Session 可能正处于开放 Turn,这是正常运行态,不能因为另一个读者调用 load 就把它强行标记 interrupted。
对 live id 的 load 会先等待权威内存 snapshot 达到持久化观察点;如果 Turn 仍开放则拒绝,而不是修改正在运行的 Session。
冷 Session 没有活跃 writer,才可以把没有关闭的尾部解释为进程中断。
Session Header 与事件日志为什么分开
Session Header 保存格式版本、Session id、创建时间、cwd、父 Session、seed 长度、subagent 来源和 delegation depth 等存储 metadata。
这些字段描述整个 Session artifact,不是对话中发生的一条消息,也不应该进入 deriveMessages。因此它们与事件日志并列保存。
Header version 不匹配时,后端明确拒绝加载。预发布阶段不承诺旧格式迁移,静默猜测旧格式可能造成错误重建。
Resume、Replay 与 Fork 的区别
Resume
Resume 用同一个 persisted Session id 恢复活跃 Agent。历史、Turn 编号和 lineage 继续原 Session。
Replay
Replay 把一组已有 SessionEvent 作为 seed 创建内存 Session,用相同派生逻辑重现请求、UI 或测试输出。它可以使用 replay LLM Provider,不必再次调用真实模型。
Fork
Fork 在某个事件边界复制父 Session 前缀,创建新的 child Session id。Header 记录 parentSession 和 seedLength;子 Session 只追加自己的后续事件。
可以用版本控制类比帮助记忆,但正式区别是 identity 与 seed:Resume 继续同一 id,Replay 重放给定事件,Fork 创建带 lineage 的新 id。
Compaction 为什么不是删除旧日志
模型上下文窗口有限,长 Session 不能无限把所有内容发送给模型。Compaction 选择一段当前 surface,生成摘要,再追加 replacement 节点遮蔽原区域。
旧事件仍在 append-only 日志中,便于追踪摘要来源。当前 model surface 使用摘要节点,从而减少 token。
Compaction 不能随意切断 Tool Call 与 Tool Result。选择区间的两端必须保持配对平衡,否则摘要可能留下一个没有结果的调用或孤立结果。
自动压缩与手动压缩
自动压缩在 agent/pre-step 检查正常压力,也可以在 Provider 确认 context overflow 后通过 agent/request-error 尝试恢复。
手动压缩要求 Agent 处于适合的 idle 状态,并使用持久 start/end marker 表达压缩生命周期。取消、摘要失败或选区变化都必须留下可解释结果,不能半提交 replacement。
Tool Result Pruning
大型工具输出可能占据大量 context。可选 pruner 可以保留头部和尾部,缩短中间文本,并用新 surface replacement 取代完整结果。
Pruning 与语义摘要不同:它是确定性文本缩减;Compaction summary 通常需要模型或其他摘要实现。系统可以先 prune,再决定是否仍需要 summary。
Prepare、Publish 与 Rollback
从存储恢复 Session 时,系统先准备一个 unpublished Session,完成验证、修复、setup 和资源建立,再发布到 Session Store 与 Agent Registry。
如果 setup 失败,准备对象负责释放 Provider 状态,不让半初始化 Session 对外可见。成功发布后,所有权转移到活跃生命周期。
这类 prepare/commit/rollback 结构在复杂资源创建中非常重要:观察者不应该看到“Session 已注册但 Agent 还没准备好”的中间状态。
源码阅读顺序
docs/subsystems/persistence.zh.mdpackages/session/session-persistencepackages/session/session-persistence-jsonlpackages/session/session-persistence-sqlitedocs/subsystems/compaction.zh.md
本篇练习
- 解释 append 完成、flush 完成和 Agent idle 三个状态为什么不能混为一谈。
- 写出 Resume、Replay、Fork 各自的 Session identity 变化。
- 解释 crash recovery 为什么补 turn/end 而不是删除开放 Turn。
- 说明 Compaction replacement 怎样同时满足“减少模型历史”和“不修改旧事件”。
验收标准:能画出“内存 Session → 写入控制器 → Persistence Backend”的数据流,并指出至少两个失败恢复点。