Paginated 历史用逻辑 ordinal 与物理 byte offset 的组合定位前缀:前者约束顺序,后者冻结具体文件边界,二者共同支撑分页、Fork 和增量投影。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:说明 HistoryPosition 的生成、恢复和校验,不展开 lineage 拼接。输入:History Mode、history_base、当前文件尾和下一条记录;状态所有者:OrdinalState、ProjectionState 与 HistoryPosition;成功结果:end_ordinal_exclusive + end_byte_offset 的稳定边界。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | History Mode、history_base、当前文件尾和下一条记录 |
| 状态所有者 | OrdinalState、ProjectionState 与 HistoryPosition |
| 成功产物 | end_ordinal_exclusive + end_byte_offset 的稳定边界 |
| 研究范围 | 说明 HistoryPosition 的生成、恢复和校验,不展开 lineage 拼接 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 从 mode/base 或尾行恢复 next ordinal。
- 为新行附 ordinal。
- 写成功后记录字节尾。
- SQLite 原子推进 next position。
- Reader 用 exclusive boundary 截断。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Legacy 不写 ordinal
OrdinalState 在 Legacy 模式返回 None,保持旧日志线格式兼容。Paginated 从 history_base.end_ordinal_exclusive 或 0 推导下一序号,并为每条行记录写 Some(ordinal)。
Exclusive end 减少边界歧义
HistoryPosition 的 end_ordinal_exclusive 表示边界之后的第一个序号;Fork ThroughTurn 把该 Turn 的末 ordinal 加一,BeforeTurn 直接使用 start ordinal。
Byte offset 冻结物理前缀
同一个源文件以后仍可追加,因此只存 ordinal 不足以保护反向扫描边界。end_byte_offset 让 Reader 从固定字节末端开始,明确排除后来追加的内容。
恢复必须检查最后有效行
Resume 从 canonical meta 得到 mode,再反向找最后一条有效记录。Paginated 的最终有效记录必须携带 ordinal,否则无法安全确定下一个值。
投影同时检查两种连续性
SQLite apply_projection 在事务里核对 expected byte offset 与 expected ordinal。任何一个不吻合都说明另一个 writer、截断或陈旧 projector 介入,不能盲目覆盖进度。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
def initial_ordinal(mode, history_base):
if mode == LEGACY:
return None
return history_base.end_ordinal_exclusive if history_base else 0
async def append_paginated(state, item):
ordinal = state.next_ordinal
start = await state.file.position()
await state.file.write_line(encode(item, ordinal=ordinal))
end = await state.file.position()
state.next_ordinal = checked_add(ordinal, 1)
return HistoryPosition(
thread_id=state.thread_id,
end_ordinal_exclusive=state.next_ordinal,
end_byte_offset=end,
)
async def apply_projection(db, expected, rows, new_position):
async with db.begin_immediate() as tx:
assert await tx.position() == expected
assert rows_have_contiguous_ordinals(rows, expected.next_ordinal)
await tx.apply(rows, new_position)
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| ordinal 溢出 | 无法表示下一位置 | 追加返回错误 | checked_add 禁止回绕 |
| 尾行缺 ordinal | Paginated 文件不连续 | Resume 无法安全续写 | 拒绝打开而非猜号 |
| byte offset 超出文件 | 引用指向不存在字节 | Lineage/Fork 无效 | 边界校验直接报错 |
| 投影 expected position 不符 | 并发或陈旧 projector | 事务不应用 changes | 重新读取当前位置后重算 |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- Paginated 每个有效行都有单调 ordinal
- ordinal 只在行写成功后推进
- HistoryPosition 的 byte offset 不得超过物理文件
- SQLite next position 不能领先已经读取的完整 JSONL 前缀
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
双位置增加元数据和校验逻辑,但把逻辑分页与物理冻结同时表达,避免仅凭行号或仅凭字节带来的歧义。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
每次追加返回 Position;用 exclusive ordinal 和 byte offset 实现 freeze_prefix,并测试追加后旧 Position 仍读取同一前缀。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/protocol/src/protocol.rs:HistoryPosition 与 RolloutLine ordinal 字段
- codex-rs/rollout/src/ordinal.rs:Legacy/Paginated 的序号初始化与恢复
- codex-rs/thread-store/src/local/thread_history.rs:投影位置的事务校验
相邻测试也很重要:
- codex-rs/rollout/src/recorder_tests.rs:Paginated ordinal 写入与不安全尾部恢复
- codex-rs/thread-store/src/local/thread_history_materialization_tests.rs:byte offset/ordinal 的增量推进与 lineage 位置
本节结论
Paginated 历史用逻辑 ordinal 与物理 byte offset 的组合定位前缀:前者约束顺序,后者冻结具体文件边界,二者共同支撑分页、Fork 和增量投影。
评论
登录后即可评论