DeepSeek Harness 源码解析 · 第 29 讲
DeepSeek Harness 源码解析系列 第 29 讲:Session 持久化(JSONL/SQLite) 基于 DeepSeek Harness 源码 · 2026-09-12 💡 本讲一句话: DeepSeek Harness 把每段会话存成"只追加的事件日志"——JSONL(zstd 帧)和 SQLite(WAL)两个后端共享同一个协调器,负责批量写、按 id 串行化、崩溃后修复撕裂尾部;读完你能说清一次 append 从内存到磁盘 durable 的完整路径。 一、为什么是"事件日志"而不是"状态快照" 前几讲我们反复看到 SessionEvent:turn/start、assistant/message、tool/call、tool/result……它们不只是 UI 回放的数据,更是 持久化的唯一事实来源 。本讲的三个包就是围绕这个事实展开的: 🔹 @deepseek-ai/dsh-session-persistence :后端无关的服务契约 + 协调器(缓冲、串行化、崩溃修复编排); 🔹 @deepseek-ai/dsh-session-persistence-jsonl :每会话一个 append-only 文件,默认 zstd 压缩帧; 🔹 @deepseek-ai/dsh-session-persistence-sqlite :单库三表(state/sessions/events),WAL 日志模式。 事件溯源带来三个直接收益: 崩溃可恢复 (任何时刻磁盘上的都是某个前缀,只需找到"最后一个完整 turn"); 追加即原子 (不需要锁整段会话,只保证单条记录完整落盘); 可审计 (原始字节就是历史本身)。代价是读路径必须容忍"撕裂尾部"——这正是本讲后半部分的重头戏。 二、服务契约:七个方法划清"后端"与"编排" SessionPersistence 抽象类是唯一的公共接口。注意它的 JSDoc 里反复出现两个词: durable(持久) 和 contiguous seq(连续序号) ——这是整个持久化层的地基。 📄 packages/session/session-persistence/src/index.ts (第 84-240 行) export abstract class SessionPersistence extends Service { // 服务基类,注册键固定为 'sessionPersistence' constructor(ctx: Context) { super(ctx, 'sessionPersistence') } // 所有后端共享同一个服务名,消费者无感切换 abstract locate(meta: SessionHeader): SessionLocation | undefined // 只算路径不碰磁盘;SQLite 没有单会话文件所以返回 undefined abstract readonly supportsRawArtifacts: boolean // 声明是否暴露"逐字节原文"读取(JSONL=true,SQLite=false) abstract create(meta: SessionHeader): Promise<void> // 登记元数据;允许懒物化——没 append 过的会话在 list 里不存在 abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void> // 持久化一批事件;resolve 之前必须已 durable,且 seq 连续 abstract load(id: SessionId): Promise<SessionInspection> // 冷恢复:保留完整中断 turn、补合成闭合事件,撕裂尾丢弃 abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection> // 只借不可变视图,不提交修复、不发布会话 abstract readFrom(id: SessionId, fromSeq: number, signal?: Ab…