Fold & 快照
fold 是将 EventLog 还原为任务状态的函数。它的输入被刻意保持最小:一个 EventLog、一个 ContentStore、一个 task id——仅此而已。没有时钟、没有随机性、没有外部 IO,它也从不重新调用 provider。这种纯粹性换来了一个具体能力:同一条日志,在任何进程、任何机器上 fold,产出字节级相同的状态。
正因如此,恢复根本没有专门的"加载状态"逻辑。要恢复一个挂起的 Task,就 fold 它;要在 Web UI 或 SSE 重连中展示 Task,就 fold 它;要事后审计,还是 fold 它。状态永远是一个计算得出的投影,而非一个必须与日志保持同步的单独存储副本——"副本与日志不同步"这一整类 bug 随之消失。
两条路径,一个结果
从顶部 fold 整条日志会使长任务越来越慢,因此 fold 保留了一条快照快速路径:
- 从顶路径 —— 从创世事件引导出空状态,重放一切。
- 快照路径 —— 从最新快照恢复状态,仅重放快照之后的尾部事件。快照本身就是一个普通事件,其正文存放在 ContentStore 中,在每次挂起前写入。
一条铁律凌驾于两者之上:两条路径必须 fold 出字节相等的状态。fold 保留了一个强制完整重放的开关,测试用它来交叉校验快照路径。这条规则将快照的地位锁定为"性能加速器",永远不是第二个事实来源:删除所有快照,行为不变,只是变慢。
同样的优先级处理版本漂移。当 fold 遇到一个早于新状态字段的旧快照时,它丢弃快照并回退到完整重放,直到新版本快照接管。宁可慢,不可错。
规范化渲染
"字节相等"需要一个后盾,这一层叫做 canonical(规范化):将任何类型化值渲染为稳定的字节形式——键排序、分隔符紧凑、全程 UTF-8。等价的对象因此渲染为完全相同的字节,同一内容的哈希在任何机器、任何时间都相同。内容寻址依赖 canonical 来去重,快照依赖 canonical 来交叉校验,整个事件溯源设计的可复现性就建立在这一薄层之上。(随着字段的增删,旧记录如何保持可 fold,在架构概览中有说明。)
fold 何时运行
每次唤醒、每次 SSE 重连、每次检查,以及每次 Engine 步骤的开始——fold 是挂起/恢复和多轮对话背后唯一的重建机制。它只向前 fold。