Projection、Checkpoint 与冷启动
全量事件之外,系统保存了哪些可丢弃的加速状态
结论:事件日志掌握真相,Checkpoint 与 Projection 掌握时机和成本
DeepSeek Harness 没有在 Session 日志旁边再持久化第二套“对话真相”。它让 append-only SessionEvent 流和 SessionHeader 保持权威,再叠加三种不同机制:不可逆工作前的语义 flush barrier、快速读取当前视图的内存 projection cell,以及缩短部分 fold 的耐久 projection checkpoint。浏览器还会收到另一层派生数据——带 watermark 的 baseline 和非耐久 push frame——并让它们重新向日志收敛。
只有把这些层严格分开,架构的优势才成立。语义 checkpoint 证明更早的事实已经到达存储;projection checkpoint 只记住一次 fold;watermark 表示一个值观察到了哪里;baseline 是传输 cut;client row 则是可丢弃的收敛状态。把其中任何一层误当成日志本身,恰好会制造这套设计试图避免的恢复错误。
1. 六个名称对应六种不同合同
| 层 | 表示 | 权威性 | 能够证明什么 |
|---|---|---|---|
| Durable fact | SessionHeader + append-only SessionEvent | 事实源 | Backend 实际保存了哪些 metadata 与 committed events |
| Semantic checkpoint | 被 await 的 sessions.flush(session) | 耐久屏障,不是数据 | 已接纳前缀的所有参与 flush listener 均已 settle |
| Live projection cell | 每个 unit、每个 live Session 的 {state, observedSeq} | 可丢弃派生状态 | 最后一个通过该 unit fold 的事件 |
| Projection checkpoint | 每个 key 的 {ver, seq, val} | 仅为耐久 cache | 绑定到某一日志生命周期的版本化 fold shortcut |
| Host baseline | {asOfSeq, values} | 派生 wire snapshot | 一个可用来 seed client 的一致 cut |
| Client row | key -> {value, seq} | 可丢弃收敛状态 | 该 key 最新被接受的 Host value |
持久化单元就是既有的 Session event,而不是一套平行 persisted-message 模型;不可 replay 的 metadata 保留在 Header 中。Projection checkpoint 类型也明确声明:row 从不具有权威性,版本不适用或它声称的日志端点无法成立时必须丢弃。ARCH-LOGSESSION-FORMAT-HEADERPROJ-AUTHORITY-LAYERS
2. Semantic checkpoint 是 await barrier,不是存储中的 checkpoint object
把事实 append 到 live Session
↓
sessions.flush(session)
├─ persistence listener 排空 write-behind
├─ 其他 scoped durability listener settle
└─ 所有 listener settle 后才抛出第一个失败
↓
构造 model stream 或进入顶层 tool body
SessionStore.flush() 解析精确 live Session 的 scoped listeners,调用全部 listener,以 Promise.allSettled 等待,若存在拒绝再抛出第一个。它的返回值只表示是否有 durability listener 参与;这个 API 不会生成任何耐久的“checkpoint record”。
标准 policy 会在构造下游 model stream 前、顶层 tool body 运行前,以及每一个 agent/pre-step 边界放置 barrier。Model 与 tool 边界 fail-closed;嵌套 tool dispatch 复用已经耐久的外层 call。LOG-CHECKPOINT-POLICY
3. Write-behind 把热路径接纳与耐久静止点分开
接纳时复制
每个 committed event 通过 structuredClone 进入 persistence 自己拥有的队列。
固定 deadline 组批
第一个 pending event 启动一个有界 timer;热 append 路径不等待磁盘。
失败仍保留
失败 batch 按原顺序放回队首,并暂停自动重试。
加入同一 barrier
并发显式 flush caller 共用一次排空,直到队列静止。
显式 flush 会取消 batching timer、等待重叠工作、重试 retained events,并一直排空到 active 与 pending write 都不存在。按 Session ID 的 serialization 阻止同一日志的耐久 append 相互交错。LOG-WRITE-BEHIND
Persistence 与 checkpoint scheduling 是两个独立插件。只加载 backend 合法,但崩溃可能丢失仍处于 batching window 或 outstanding write 中的事件。发布的 base composition 会显式同时挂载 JSONL persistence provider 与 checkpoint policy。PROJ-SHIPPED-COMPOSITION
4. Projection unit 是对每个 committed event 的同步 fold
ProjectionDefinition<K, S> = {
key,
schema, // 校验 wire-facing view
init(): S,
apply(S, event): S, // pure 且同步
view(S): Value,
stateVersion
}
Registry 只订阅一次 session/event。每个事件都会通过每个已注册 unit 的 apply;不关心该事件的 unit 必须返回同一 state reference。只有 reference 改变才会触发 schema validation 与 change notification。缺失 cell 会延迟 fold 完整内存日志,而 observedSeq 即使在 state reference 未变化时也会前进。PROJ-REGISTRY-DRIVE
同步要求具有关键作用。Carrier 可以复制 Session 的 event snapshot,并在同一个 JavaScript turn 中立刻调用 snapshot(),得到共享 asOfSeq 恰为 session.seq - 1 的 values;异步 unit 会撕裂这个 cut。
5. 十三个生产 key 共用一个框架,却表达不同生命周期
| Key / version | Fold 语义 | 生产源码 |
|---|---|---|
todos v2 | 最新 whole todo/write;下一个 turn/start 清空 | packages/todo/tool-todo/src/index.ts:122-147 |
plan v1 | command/run(plan) 记录意图;plan/mode 提交 active 并清除 pending | packages/plan/plan-mode/src/index.ts:235-265 |
goal v4 | Durable goal/change 的 last-wins projection | packages/goal/goal/src/index.ts:201-212 |
title v1 | 最新 session/title | packages/session/session-title/src/index.ts:304-316 |
sessionStats v1 | 全日志 Turn、Step、model、tool、TTFT 与 decode totals | packages/session/session-stats/src/projection.ts:1-21,88-182 |
tokenUsage v1 | Usage sample 替换同一 Turn/Step 的前一份 sample | packages/llm/token-meter/src/usage-projection.ts:97-140 |
contextPressure v4 | Provider prompt sample 加 Surface signed movement 估计下一请求 | packages/llm/token-meter/src/usage-projection.ts:142-205 |
contextBreakdown v2 | 最新 request envelope 加 message-surface shadow pricing | packages/llm/token-meter/src/breakdown-projection.ts:31-69 |
subagentTiming v2 | Descriptor 之后已完成及当前活跃 Turn 的持续时间 | packages/subagent/subagent/src/projection.ts:36-85 |
subagent v2 | 最后一个有效耐久 mode/label descriptor,并包含其 seq | packages/subagent/subagent/src/projection.ts:131-155 |
permissions v1 | 三个 whole-value knob events,再相对 composition defaults 生成 view | packages/interaction/permission-presets/src/index.ts:227-251 |
sessionListMetadata v1 | 供 listing 使用的 blank-to-nonblank 状态与最新真人 prompt 时间 | packages/host/apiproxy/src/api-proxy.ts:1289-1300 |
imageLimits v1 | 每次启动固定的 attachment limits;constant state、live-service view、只走 baseline | packages/host/apiproxy/src/api-proxy.ts:1302-1323 |
这是固定 commit 上对生产注册的全仓库清点,并不承诺每种部署都激活全部十三个 key。注册取决于 composition;process-wide unit table 还意味着某个 preset 挂载的 key,可能出现在自身 preset 并不会产生有意义值的其他 Session snapshot 中。
6. Watermark 在观察时前进,Push 只在变化时发生
seq N 的 event 进入每个 unit
├─ apply 返回同一 reference
│ observedSeq = N
│ 不发送 session/projection frame
└─ apply 返回新 reference
observedSeq = N
schema.parse(view(state))
emit { key, value, seq: N }
这个区分解释了为何 imageLimits 从不发送 push,却仍会出现在 tail baseline 中;也解释了为何完整 snapshot 中一个安静的 value 可以合法地标记为日志当前 asOfSeq。Watermark 表示“已观察至此”,而不是“最后一次变化发生在此”。
Host 把 registry change 翻译为非持久的 session/projection mux frame。History tail——并且只有 tail——携带完整 baseline。Attached Session 在复制 events 与读取 projection snapshot 之间没有 await;detached Session 则 fold 被 inspect 的 events。PROJ-HOST-CARRIERLOG-HISTORY-CUT
7. Durable projection cache 是版本化 shortcut,从不是读取权威
| Guard | 为何存在 | 失败行为 |
|---|---|---|
stateVersion | State shape 或 fold semantics 可能变化 | 丢弃该 key 的 row 并重新 fold |
createdAt + cwd | Session ID 表示一个槽位,不会永远唯一指向同一生命周期 | 丢弃整个无关 record |
| Anchored floor | Cached row 可能声称超过缩短或修复后的 stored log | 检测 overreach 并从 seq 0 重读 |
| Lossless JSON boundary | Unit 内部状态必须可耐久序列化 | 让该 cache write 失败;不修改 Session log |
session_projcache domain 版本为 3。每个 Session record 把完整的 key-to-row checkpoint 绑定到一个 log identity。Domain version 不兼容时可以丢弃整个 cache medium,因为事件日志仍足以重建。PROJ-CACHE-SCHEMA
restoreFloor() 选择最低可用 watermark 前的一条事件。restore() 只有在 row version 匹配、且 seq 位于所提供 suffix 能够证明的边界中时才接受它。不安全的 partial restore 会 throw,让 caller 从零重读,而不是混合不兼容状态。PROJ-REGISTRY-RESTORE
8. 已实现的 cold-cache ladder 没有接入发布的 Cold History
已实现的 SessionProjectionCache.coldSnapshot(id)
cache rows → restoreFloor → persistence.readFrom(tail)
→ registry.restore → fail-soft refreshed write-back
发布的 ordinary cold History
persistence.inspect(full logical log)
→ registry.restore({}, all events, 0)
发布的 Session list
只调用 cachedSnapshot(header),零 log I/O
coldSnapshot() 会校验 log identity,从 anchored floor 做 tail fold,在 identity 或 endpoint 失败后回退到一次 full read,并在不影响读取的前提下写回 refreshed rows。PROJ-CACHE-LADDER
在固定 commit 上,生产源码搜索只找到 coldSnapshot 方法定义与生成的 API catalog,没有任何发布 caller。session.list 只使用 cachedSnapshot;ordinary 与 subagent cold History 已经读取 inspect 后的完整日志,并从零 fold;subagent listing 在 cached row 足以定论时使用它,否则执行完整 inspection。因此这个 cache 当前是发布的 list/metadata accelerator,而不是活跃的 cold-History tail-replay accelerator。PROJ-CACHE-INTEGRATION
9. Inspect、Prepare 与 Resume 构成冷启动协议,而不是三个别名
| 操作 | 逻辑恢复 | 耐久突变 | Ownership / publication |
|---|---|---|---|
inspect(id) | 校验并在内存加入确定性 closers | 不提交 repair | 无 Agent;可保留 exact prepared graph 供复用 |
readFrom(id, seq) | 无;返回有效 physical stored suffix | 无 | 不经过 preparation cache,也不发布 |
prepare(id) | 构造平衡的 exact Session | 必要时提交 repair,再重新加载 | 独占预留 unpublished Session |
Agent resume | 消费 preparation | 后续 live suffix 按正常路径持久化 | 运行 setup,发布 exact Session + Agent,或 rollback |
Cold inspection 可以呈现平衡的 logical Turn,同时不触碰 torn 或 interrupted physical tail。Preparation 使用 loading、ready、committing、reserved 四个 phase;同 ID inspector 共享一次 load,只有被独占预留的 exact Session 能够 publish。LOG-PERSISTENCE-READ-FACESLOG-PREPARED-COMMIT
Agent factory 把 caller、owner fiber 与 factory cancellation 融合到 persistence.prepare 周围。Setup 在 publication 前运行;失败会 dispose prepared Agent 并释放 reservation。SessionStore.enter 是最终同 ID collision boundary。PROJ-COLD-RESUME
10. 浏览器 Cold Open 被刻意设计为不同于 Agent Resume
恢复选择
浏览器记住当前 Session address,并在 list 到达后选择它。
打开 History
Session.open() 请求 tail page;它不请求 Agent。
Inspect 冷存储
Host 优先使用 attached Session,否则使用 persistence inspection。
仅按需 Resume
Prompt、rename、model、command 等需要 Agent 的操作才进入共享 resolver。
Cold Session 必须能被 persistence list 找到,并携带 project cwd,才可由 Web 服务。若日志记录的 preset 已无法提供 standing presenter scope,History 仍可降级为 generic tool card。真正 resume 更严格:它必须通过 format、continuity、ownership、preset composition、setup 与最终 publication 检查。
共享 resolver 复用 live Agent,对同 ID resume 去重,inspect ownership 与 composition,再调用 agents.resume。Not-found 与 subagent ownership 得到 typed error;其他真正的 resume failure 仍是 error。只有显式 create/adopt 路径在 persistence 确认 ID 缺失后才创建 fresh Session。PROJ-HOST-COLD-OPENPROJ-COLD-RESUME
11. Client Convergence 组合完整 Baseline、稀疏 Push 与 Generation Repair
history tail baseline { asOfSeq: N, values: all registered keys }
↓ seed
resident ProjectionValueStore
↑ 仅 frame.seq 更大时 apply
session/projection { key, value, seq }
新 mux generation:
session/subscribed { lastSeq: D }
→ 删除 client 中 seq > D 的 rows
→ 重新拉 list,并 resync 已打开的 History windows
即使 Session object 尚不存在,manager 也为每个 Session 拥有一个 projection store。Baseline 与 push frame 都使用严格 higher-seq-wins。完整 History baseline 还会清理 cut 及更早时刻未携带的旧 key;局部 session.list block 只 apply 自己携带的 key,刻意不清理其他 key。PROJ-CLIENT-STORE
session/subscribed.lastSeq 会删除声称超过 Host durable endpoint 的 row,让 crash 后重建出的较低 seq 真相能够落地。随后 History installation 排空 buffered live events,丢弃 overlap,并在检测到 seq hole 时重新拉 tail。PROJ-CLIENT-MANAGERLOG-CLIENT-STITCH
Mux 与 host feed 是两条独立 stream,没有共享 total-order 声明;v1 的 events.mux({since}) 也会忽略 since。被支持的收敛方式是重开 stream、刷新 list、重拉 History,并按各 domain 规则 repair,而不是从一个全局线性 stream offset 继续。
12. Rebuild 与 Invalidation 发生在四种不同 Scope
| 变化 | 被失效的状态 | 真相如何回来 |
|---|---|---|
| 追加另一个 event | Live cell eager 前进 | 变化 unit 发 push;完整 baseline 之后把每个 value 标记到新 cut |
| Unit state 或 fold semantics 变化 | 旧 stateVersion 的 persisted row | 丢弃 row,并从可证明的日志前缀 fold |
| Session ID 被重建或 storage root 变化 | Identity 不匹配的整个 checkpoint record | 丢弃该无关生命周期的全部 rows |
| 最后一个 registrant 卸载 | 该 key 的 registration 与全部 live WeakMap cells | 后续 snapshot 省略 key;重新注册后延迟 fold 内存日志 |
| Projection-cache domain format 变化 | 整个 derived medium | 全部丢弃;Session log 仍足以重建 |
没有任何 live registry API 会从 persisted checkpoint hydrate 自己的 WeakMap cells。Cache 的 detached restore() 路径会返回 snapshot 与 refreshed checkpoint,但 resumed live Session 在 registry 第一次访问时仍会 fold 内存 event array。Cache 也没有 eviction 或 retention surface;record 会一直累积,直到 out-of-band maintenance 删除。
13. 四项文档声明已经偏离当前生产控制流
| 文档声明 | 当前实现 | 证据 |
|---|---|---|
| 重复 projection key 会 throw | 同 version 注册共享一个 ref-counted unit;只有 version mismatch 才 throw | session-projection/README.md:11 对比 src/index.ts:194-220 |
Title 不进入 session.list | List row 携带 projection block;cold cached title 会在 open 前 seed manager | host/apiproxy/README.md:33 对比 api-proxy.ts:1725-1764 |
| History 会 resume unattached Session,且没有 persistence-only read path | Cold History 使用 inspect(),并明确不发布 Agent | client/connection/README.md:23-26 对比 api/sessions.ts:264-283 |
skill.list 可从 Header 解析 cold Session | Handler 首先要求 ctx.sessions.get(sessionId);unattached Session 返回 not found | host/apiproxy/README.md:59 对比 api-proxy.ts:3205-3245 |
14. 源码级风险:Detach 可能短暂让 Projection Cache 领先日志
Session detach
1. 从 SessionStore 移除 Session
2. emit 被 contain、未 await 的 session/disposed observers
├─ persistence 启动异步 retirement flush
└─ projection cache 取 snapshot 并写入
live-entry 检查此时失败 → 不调用 sessions.flush()
Cache 文档声明日志永远领先 cache。普通 live write 确实执行这个保证,但 detach 时并不严格成立:Session 已经离开 store,因此 cache 跳过显式 flush,只依赖 persistence listener 的另一条 retirement drain。Cache 源码自身也承认可能存在 residual overreach。若进程在两次耐久写之间崩溃,就可能留下 seq 领先 stored log 的 checkpoint。PROJ-CACHE-DETACH-WINDOW
coldSnapshot() 原本可通过 anchored floor 检出 overreach,但发布路径没有 caller 使用它;zero-I/O list read 也不校验 stored log endpoint。因此 phantom list projection 可能进入 client;后续较低 seq 的 cold-History full-fold baseline 会在 higher-seq-wins 下失败,直到真正 resume 发出 session/subscribed 并 truncate row,或日志前进超过它。
15. 源码级风险:Equal-seq Replacement 无法跨越 Host Generation
Client 会拒绝 seq 小于或等于 resident row 的所有 update,而 generation truncation 只删除严格超过新 Host durable lastSeq 的 rows。如果 Host 在相同日志端点上重启、但在该端点计算出不同 value,旧 row 会保留,新 equal-seq baseline 无法替换它。PROJ-CLIENT-EQUAL-SEQ
imageLimits 让场景变得具体:它的 state 永远是 null,view 读取当前 boot 的 attachment config,并且刻意从不 push。浏览器若跨 Host restart 保持存活,就可能在 seq N 保留旧 limits;subscribed 不会删除它,因为它没有超过 N,而新的 N baseline 会因 equal 被拒绝。只有另一个 event 推进 baseline、client 完整重置,或协议携带 generation/version 维度后,value 才会收敛。
同一个边界也适用于 unit view semantics 在日志 watermark 不变时改变,以及同 key 的 unload/reload。stateVersion 能保护 Host persisted cache,却没有出现在 client wire row 中,因此无法解决这个 client-generation 歧义。PROJ-IMAGE-LIMITS
16. 源码级风险:一个 Optional Unit 可以让 Ordinary History 不可用
Detached History baseline 调用 registry.restore({}, events, 0),会 fold 每一个已注册 unit。如果任何 unit 的 apply、view 或 schema 拒绝日志,ordinary session.history 会返回 internal,而不是在没有 projections 的情况下继续提供 transcript。Session listing 明确按 row contain projection failure,subagent History 也会降级为不带 block;ordinary History 没有这样做。PROJ-HISTORY-FAILURE
这不只是 failing domain 内部的 corrupt state:process-wide registry 的 all-unit fold 允许一个无关 projection 阻断基础 transcript read。Fail-loud 是否符合预期需要明确产品决策;当前 carrier 行为并不一致。
注销最后一个 unit 只会删除 registration 与 cells,不会发送 invalidation frame。已连接 client 会保留该 key,直到后续完整 tail baseline 省略它;如果相同 key 在同一 log seq 重新加载,即使 baseline 到达,严格 equal-seq 规则仍可能保留旧 value。PROJ-REGISTRY-REGISTRATIONPROJ-CLIENT-STORE
17. 设计评价与验证状态
| 选择 | 收益 | 代价 |
|---|---|---|
| 唯一权威事件日志 | 每个派生层都可丢弃并重建 | 加速路径未接线时,cold full fold 仍昂贵 |
| 同步、whole-value projection units | 原子 cut 与简单 client replacement 语义 | 每个 event 都触碰每个 unit;大 value 随每个 tail baseline 传输 |
| 版本化 persisted fold shortcuts | 语义变化无需 cache migration | Version discipline 依赖人工,cache retention 无界 |
| Inspect 与 Resume 分离 | Cold browsing 不激活 Agent,也不突变 durable recovery state | 读取与执行路径会暴露不同的 logical/physical 时刻 |
| Client higher-seq-wins | Stale baseline 与 replayed frame 无法回退当前状态 | 单独 seq 无法区分 equal-watermark Host generation |
本章在固定 upstream commit 上追踪了 Session flush dispatch、checkpoint policy、persistence write-behind、projection registry 与全部生产注册、projection-cache schema 与读写路径、Host History/list carrier、cold inspection 与 exact preparation、Agent resume,以及浏览器 projection/gap-repair store;同时阅读了相关合同测试源码。依赖未安装,本地没有执行测试、crash injection 或真实 reconnect 实验,因此上面的每个风险段都被标记为静态推导,而非已复现行为。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。