T3 Code 源码拆解
核心运行时

07 · SQLite、事件日志与读取投影

40 轮迁移背后的数据模型、写时投影、恢复策略和历史演化信号。

SQLite 承担的是权威存储

T3 Code 的 Server 本地运行,SQLite 很适合它的约束:单执行环境、低运维、事务强、可随 userdata 携带。 但“本地数据库”不代表数据模型简单。本快照包含从 001 到 040 的迁移,覆盖事件溯源、关系投影、Provider 运行态、 认证、checkpoint diff 和持续增加的 thread 产品字段。

三种数据,不应混为一张表

持久化的三层角色
orchestration_events 因果历史 / 重建 command_receipts 幂等结果 projection_* tables 快速产品查询 provider_session_runtime Provider 恢复信息 auth sessions / pairing 连接身份与权限 checkpoint_diff_blobs 大体积变化内容

事件日志

001_OrchestrationEvents.ts 建立追加式领域日志,sequence 提供全局顺序。事件 payload 保留发生了什么,供启动恢复、 调试因果与客户端增量同步使用。

事件并不是任意日志行。它必须是可 decode 的领域 Schema,并且 projector 要能在旧历史上持续工作。删除或改变旧事件语义 比给普通表改字段更危险。

Command receipts

002_OrchestrationCommandReceipts.ts 保存 idempotency key 对应的 accepted/rejected 结果。它与事件同事务提交, 让网络重试重放“结果”而不是重做“动作”。receipt 的生命周期要覆盖客户端可能重试的窗口,且 rejection 同样需要稳定。

关系投影

005_Projections.ts 建立 projects、threads、messages、activities、sessions、turns、pending approvals 等读模型表, 后续迁移逐步加入附件、activity sequence、runtime mode、interaction mode、proposed plan、archived、settled、snoozed、pinned、favicon 等。

这些表不是第二个真相源,而是事件的可查询产品形态。写路径在同一事务中同步推进它们,从而允许 RPC 快照做索引查询和分页, 不必每次 replay 全部事件。

迁移序列透露出的产品演化

迁移名本身是一份压缩历史:

范围主题透露的演化
001–005事件、回执、diff、runtime、投影先建立可恢复核心
006–012session/thread runtime 与 interaction mode多运行模式进入稳定模型
013–015proposed plan 与 turn 来源计划从临时 UI 变成领域对象
016、026model selection canonicalizationProvider 模型配置需要兼容清洗
017–019archive 与查询索引数据量增长后优化列表
020–022、031–032access、client metadata、scope、DPoP远程访问安全逐步加固
023–030shell summary、清理与排序索引大列表/历史线程性能与数据卫生
033–040settled、snoozed、pin、env mode、favicon控制面产品能力成熟

不要把这张表当发布时间线;快照没有 Git 历史。它只能证明 Schema 的演化顺序和当前兼容负担。

Persisted projection pipeline

编排引擎先用事件推进候选内存 model,再调用 persistence projection pipeline 更新关系表。两者使用同一事件语义但面向不同读者:

  • 内存 model 服务当前进程的快速决策与 PubSub;
  • SQLite projection 服务分页、重启恢复和大型集合查询。

最重要的不变量是:二者必须在已提交 sequence 上一致。若只更新其一,下一条 command 可能基于和客户端不同的世界做决定。

为什么不只存最终 Thread JSON

只存一个大 JSON 会让简单原型更快,但在 T3 Code 的需求下有明显问题:

  • 并发命令失败时难以解释部分状态;
  • 无法用 sequence 做可靠增量订阅;
  • 幂等重试无法证明此前做了什么;
  • 大线程每次更新都重写整个 blob;
  • 索引 archive/pin/settled/turn 时间会很困难;
  • Provider 失败后的意图与结果无法分开追踪。

事件 + 投影付出更多 Schema 和迁移成本,换来可解释因果与产品查询能力。

大内容为什么单独存 blob

Checkpoint diff 可能很大,若内嵌在每个 thread snapshot 或 event payload 中,会污染热路径。003_CheckpointDiffBlobs.ts 把它放进独立存储,通过引用按需读取。Git driver 对 diff 还有约 10 MB 上限,避免极端仓库把 Server 内存和网络占满。

类似策略也适用于附件:事件记录事实与 metadata,大 payload 通过专用路径读取。

索引是一种产品契约

后期迁移大量增加 archivedAt、thread detail ordering、shell archive、turn keyset、pin order 等索引。 这说明列表不再适合 offset + 全表排序。Keyset pagination 需要稳定的复合排序键,Server 和客户端都要把 cursor 当不透明边界。

索引设计不是“DBA 事后优化”:它由 UI 的排序、过滤和翻页语义决定。改变 pinned/snoozed 排序时,必须同时审查 query 与索引。

启动恢复

重启时,Server 需要恢复:

  1. 数据库 Schema 到当前版本;
  2. 持久化投影或由事件重建的 read model;
  3. Provider instance 配置;
  4. thread ↔ provider session runtime binding;
  5. 未结算 turn、pending approval 与 checkpoint 状态。

“恢复状态”不代表外部 Provider 一定仍活着。runtime binding 是尝试 resume 的证据,adapter 的 hasSession/readSession 才判断实际进程/SDK 状态;失败后需通过新事件结算,不可静默假装 session running。

Schema 演化原则

  • 历史事件优先保持可读;必要时在 decode/projector 做兼容,而非原地改旧 payload。
  • 关系投影可以迁移/回填,因为它可由事实重建,但迁移仍必须原子、可测试。
  • canonicalization 迁移应幂等,并对未知 Provider 值保守处理。
  • 新索引先匹配真实 query shape,避免为字段名盲目建索引。
  • 认证和权限迁移需要 fail closed;数据缺失不能默认提升权限。

源码锚点

可迁移经验

事件日志负责“为什么”,关系投影负责“怎样高效展示”,runtime 表负责“如何重新接上外部系统”。三者可以在一个 SQLite 中, 但不要把语义混成一张万能表。对本地优先应用,SQLite 足以承载严谨事件模型;规模小不应成为放弃一致性的理由。

On this page