第 1 卷
张嘉熙 · DeepSeek Harness 前沿工程实践



你好,我是张嘉熙。
上一讲把会话日志本身讲透了:只追加、进日志即冻结、消息历史是从它算出来的投影。但它留了两个口子,这本账记在什么纸上(怎么落盘),以及会话怎么续接。这一讲把两件一起接
上:先把纸铺好,再看能用它做些什么。
但要把这本账真正用起来,问题就一个接一个冒出来:
这本账自己的信息,又放哪儿?
进程退出后再起来,这本账还在吗?
能不能从历史中间岔出去另开一个会话,原来那个不受影响?
跑到一半进程死了,已经写进日志的那些还算吗?
关于环境:本讲新增两个包——@deepseek-ai/dsh-session-persistence(持久化 seam 的抽象定义)与 @deepseek-ai/dsh-session-persistence-jsonl(JSONL
后端实现),已加入本项目的 package.json,npm install --legacy-peer-deps 装上即可。
关于示例:npm run start:ch09:fork 跑 fork,npm run start:ch09:crash 跑崩溃与恢复。
SessionHeader:日志旁的元数据
第一个问题:这本账自己的信息,放哪儿?
一本账要能存下来、过后再打开,光有一串事件还不够:这本账自己还有一批信息要交代。
格式版本、工作目录(cwd)、从哪个会话分出来的、前多少条是继承来的,这些不是事件,所以不进上一讲那张事件类型表(SessionEventMap),也永远算不进消息历史
(deriveMessages())。它们跟事件流分开存,挂在 session.header 上:
复制代码 1interface SessionHeader {
2readonly version: number*// 落盘格式版本;本构建不认识就拒绝打开*readonly 3}
这些字段什么时候定下来?
建会话的那一刻,也就是下一节要讲到的“创建”(create(meta)),它的 meta 参数正是这份 header。所以是先后两步:建会话时先把“这本账是什么、从哪来、按什么格式记”写
定;此后 append 只管往事件流里加事件,一次都不会碰它。
“打不开”其实分两种:读不懂(SessionFormatUnsupportedError,数据好好的、只是
持久化:先把日志交到纸上
第二个问题:进程退出后再起来,这本账还在吗?
上一讲说过,dsh-session 只认那一串事件,落在哪里是另一层的事。这另一层是一个能力 seam:框架定义抽象服务,具体实现可换。整个契约只有六个动作——建、写、读、看、
续、起,本讲会挨个遇上:
复制代码 1abstract class SessionPersistence {
| 2 | //建:登记一个会话,但还不建文件——要等第一次写*abstract create(meta: SessionHeader): |
|---|---|
| 3 | //写:返回成功,就代表事件已经真的写到磁盘了*abstract append(id: SessionId, events: r |
| 4 | //读:读回来,顺带把没闭合的轮次配平——配平结果会写回磁盘*abstract load(id: SessionId): |
| 5 | //看:同样配平,但只在内存里做,一个字节都不写*abstract inspect(id: SessionId): Promi |
| 6 | //续:给投影续算用的——只读fromSeq之后那一段,不必翻整本*abstract readFrom(id: Sessio |
| 7 | //起:resume的正规入口,直接给你一个Session对象*abstract prepare(id: SessionId): |
| 8 | // ... |
| 9 | }</sessionpreparation></sessioninspection></sessioninspection></void></void> |
抽象得有人实现。官方给了两个,它们的名字上一讲在“介质可换”那里已经露过面,但只说了名字;这里我们补的是它们各自的磁盘布局:
后端磁盘布局
每个会话一份仅追加的日志文件。默认是压缩过的 .jsonl.zstd(Zstandard 格 JSONL 式,带校验和),也可配成不压缩的原始文本
每个事件一行,行字段 (session_id, seq, type, time, data, SQLite source_event_seqs, surface_op) 与事件 1:1 映射
JSONL 默认是压缩的,这一点你先在这里记一下:它决定了崩溃时磁盘上留下的是什么,一