Appearance
2.5 会话持久化:恢复、fork 与投影
本章概览:会话日志如何跨进程存活——持久化 seam 的契约、崩溃恢复语义("崩溃的轮次被关闭而非截断")、恢复/检查/增量读,以及服务 UI 的投影(projection)机制。
持久化是能力缝
ctx.sessionPersistence 是 Service Definition(session/session-persistence),后端是 Provider(session-persistence-jsonl、session-persistence-sqlite),消费方是 loop(resume)、工具与 hook 桥。持久化的单元就是已有的 SessionEvent——不存在平行的"持久化消息"类型;不可回放的元数据(格式版本、cwd、lineage、seed 边界、delegation depth)作为 SessionHeader 单独存放。
出处:
packages/session/session-persistence/README.md(deepseek-harness-src/packages/session/session-persistence/README.md)
服务 API 要点
| 方法 | 契约要点 |
|---|---|
create(meta) | 注册新会话元数据;可延迟物化(首个 append 才落盘) |
append(id, events) | 持久追加一批;只追加;首个事件 seq 必须等于存储的 next-seq |
load(id) | 返回不可变的平衡逻辑日志;活会话先 flush 快照、开着的轮次会拒绝;冷会话保留被中断的最终轮次并持久追加合成的关闭事件 |
inspect(id) | 升级、校验、深冻结的逻辑视图,不提交恢复也不发布 Session |
readFrom(id, fromSeq) | 只读 seq >= fromSeq 的后缀(checkpoint 消费者用) |
list(signal) | 轻量列出元数据,不解析全日志 |
每个后端必须遵守的不变式
- 只追加;崩溃的轮次被关闭,而非截断:已 flush 的事件绝不重写。崩溃可能留下未关闭的最终轮次(事件真实且可能很大);
load保留它们并持久追加合成关闭器(每个未应答的助手工具调用一个风险分类的错误tool/result,然后step/end?+turn/end {interrupted}),使重水合的日志平衡、历史合法。只有从未完整写入的撕裂尾部(torn tail)片段被丢弃。 - 连续 seq:
load拒绝日志中间的 seq 间隙/解析错误;append首 seq 必须等于存储 next-seq。 - JSON 可序列化:通过共享的"单遍无损 JSON"边界物化。
- 持久性:
append只在批次持久落盘后返回。
IMPORTANT
崩溃语义的精髓:绝不假装崩溃没发生。日志保留真实事件,追加合成的关闭事件让历史保持平衡——恢复后的历史仍然是一份合法的 provider transcript。
写协调器:共享生命周期
PersistenceCoordinator 拥有每 id 状态与串行化、每活会话一个有界写控制器、延迟物化、崩溃尾部修复、会话收养(adoption)与安静 dispose。JSONL 与 SQLite 都组合一个 coordinator,只实现小的 PersistenceBackend 存储钩子接口——两者共享生命周期正确性,各自保留不同的存储原语。
写批处理策略:每个 session/event 把事件拷入会话控制器;首个待处理事件开启一个固定批窗口,后续事件加入但不重置期限;writeBatchMaxDelayMs 约束这个有意的等待(不是事件循环/初始化/后端延迟);session/flush 取消等待,是共享的安静屏障。
出处:
packages/session/session-persistence/README.md与 shared-persistence-write-coordinator Agent Note
恢复(resume)与 fork
ctx.agents.resume({ resumeSessionId, ... }):经ctx.sessionPersistence加载持久化会话,在相同 id 下注册 agent,重建历史,然后对未发布的新 agent 作用域做 setup,再走回滚保护的发布序列;轮次编号与派生历史从加载的日志延续。resume需要持久化后端(无持久化时给出明确报错)。ctx.sessions.fork(source, boundary?, childSessionId?):以boundary(含)为 seed 前缀创建子会话;要求前缀边界不在开着的轮次内;携带 lineage 元数据(parentSession、seedLength、delegationDepth)。
出处:
packages/core/agent-loop/README.md与packages/core/session/README.md
投影(projection):为 UI 服务的状态折叠
ctx.sessionProjections(session/session-projection)是投影能力缝:框架驱动、领域计算。
- 领域注册一个
ProjectionDefinition:{ key, schema, init(), apply(state, event), view(state), stateVersion }——三个纯同步函数 + 声明; - 注册表订阅一次
session/event,每个已提交事件急切经过每个单元的apply; - 同一引用 = 无工作:
apply对不相关事件必须返回同一 state 引用(Object.is门控变更通知); - 整值事件规则:携带状态的日志事件必须携带完整的变更后状态(last-wins),绝不只是 delta;
- 消费方(
dsh-host-apiproxy的 history tail 页与session/projection推送帧)读snapshot()——同一 tick 的一致切面{ asOfSeq, values }。
投影单元的例子:plan 状态(plan/mode 事件折叠)、todo 状态(tool-todo)。session-projection-cache 把单元状态持久检查点(节流 + turn/end/detach 强制点),冷读走"缓存行 + 持久化尾部回放"阶梯,列表无需加载全日志。
出处:
packages/session/session-projection/README.md与packages/session/session-projection-cache/README.md
其它会话派生服务
| 服务 | 作用 |
|---|---|
ctx.sessionTitle | 从日志折叠最新标题;确定性回退 + 唯一异步提供方(首提示 LLM / 全部提示 LLM) |
ctx.sessionQuery | 会话检索:逻辑语料、有界读取、lineage、事件关系、语义过滤、SQLite 全文搜索 |
ctx.sessionReferenceResolver | 把有界的当前 surface 会话快照投影为持久化的不可信消息上下文(跨会话引用) |
ctx.sessionTelemetry | 采集、脱敏、把会话记录交给一个后端(OTel);输出离开进程 |
出处:
docs/capability-seams.md(deepseek-harness-src/docs/capability-seams.md)
小结
持久化把"事件日志"变成"跨进程真相":只追加 + 崩溃轮次关闭 + 连续 seq + 持久后返回,使恢复(resume)可以重建任何历史;投影与缓存则为 UI 提供廉价的实时读模型。下一部分进入模块精讲,从 3.1 Shell 执行能力族 开始。
参考资料
- packages/session/session-persistence/README.md — 持久化 seam 契约(
deepseek-harness-src/packages/session/session-persistence/README.md) - packages/session/session-projection/README.md — 投影能力缝(
deepseek-harness-src/packages/session/session-projection/README.md) - packages/core/agent-loop/README.md — resume 契约(
deepseek-harness-src/packages/core/agent-loop/README.md) - docs/subsystems/persistence.md — 持久化子系统页(
deepseek-harness-src/docs/subsystems/persistence.md)