第 2 卷
“没有需要保持同步的并行持久化 schema”)。
纸选好了,接着看怎么往上写。这一步分前后两下:先是接口里的“建”(create),它只登记这个会话的元数据,一个字节都不写;真正把事件写到磁盘的,是后面的“写”
(append)。所以“建了但一次都没用过”的会话,磁盘上什么都不会留下。
“写”的规矩很硬:后端 append 返回成功,就意味着数据真的写到磁盘了,不是“收到”,而是“写完”(append resolves only after durability)。这条规矩直接决定崩溃时
能找回多少:返回之后进程才死,那条事件一定还在;返回之前就死,就当它没发生过。注意这是持久化层的 append,不是往会话里追加事件那个,后者攒够一批才会调用到它。
不过规矩归规矩,什么时候真的往磁盘写,由持久化插件说了算,每来一条就写一次太慢,所以真实的写路径是批处理的:
第一个待处理事件开一个固定窗口(默认 200ms),后续事件加入,但不重置窗口;
窗口到期,把这一批写下去;这一批写入期间新来的事件,自己另起一个窗口;
session/flush 取消等待、立刻排空。
这么看,flush 就不只是“催一下”了,它是持久化检查点:排空之后,磁盘上的日志才是完整的、能接得上的。少了它,最后那一批还压在窗口里,进程一死就什么都没留下。
循环在领下一轮之前会 flush 一次,既保证顺序,也在这一个点上观察写入错误。写失败时事件被保留、自动重试暂停,通过 agent/error(框架统一的报错通道)报出来,绝不会把
失败记成一条会话事件(那等于在账上写假话)。
等下 npm run start:ch09:crash 跑出来的第一段,磁盘 3 条,turn 没闭合,就是 flush 之后留下的。少了那句 flush,磁盘上什么都不会有。
分不分、从哪儿分,都是你说了算,这三件里,它是你主动去做的那一种。
为什么需要它?一个长任务跑到第十轮,你想试试“如果第六轮换个问法会怎样”;或者要派一个子 agent 去干一件事,又不想让它看见主线的全部历史,这些都需要从历史的某个点另开
一个会话,原来那个不受影响。
底层原语还是造会话的那个 create,只是多带两个参数:ctx.sessions.create(id, { seed, meta }),seed 是一串事件,造出来的会话一出生就带着这些历史。
日常用的是包在上面的一层策略 API:
复制代码 1fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): S
三个参数:从哪儿分(可以是会话对象,也可以是它的 id)、分到第几条(含)、新会话叫什么。分叉是这样一件事:

复制代码 1--- ① 父会话:两轮对话,8 条事件 ---
| 2 | seq0 | turn/start |
|---|---|---|
| 3 | seq1 | step/start |
| 4 | seq2 | user/message |
| 5 | seq3 | step/end |
| 6 | seq4 | turn/end |
| 7 | seq5 | turn/start |
| 8 | seq6 | user/message |
| 9 | seq7 | turn/end |
10 11--- ② fork 一份出去(不指定 boundary)---
| 12 | 子会话9条,seedLength = 8,parent = parent |
|---|---|
| 13 | 最后一条:seq8 session/end-seed ←种子边界 |
| 14 | 消息历史2条(全是继承来的) |
15 16--- ③ 只要第一轮(boundary = 4)---
17子会话 6 条,seedLength = 5 18消息历史 1 条
19 20--- ④ boundary 落在开放轮次内(seq 2 正卡在第一轮中间)---
21被拒:fork boundary 2 in session "parent" ends inside open turn 1
- 里有个多出来的一条。父会话 8 条,子会话却是 9 条,多的是 seq 8 那条
session/end-seed。
它的载荷是空的,不产生消息,位置本身就是它的全部意义:它之前的事件来自 seed,从它往后才是这个会话自己的写入。官方管它叫 firstLiveSeq 的持久投影,firstLiveSeq
是给拿着对象的消费方看的,这条事件是给只拿到磁盘字节的消费方看的,两者回答同一个问题:本次生命周期的写入从哪儿开始。
为什么要专门留这么一条?因为继承来的历史和本会话的实时工作,在日志里是逐字节相同的。
举个具体的例子:日志里有一个“开了没关”的标记(下一讲要讲的上下文压缩,用的就是一对