雨天小六

读懂 Codex(8.19):History 分段分页、Turn Lookup 和搜索

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#持久化#Rollout#软件架构

引用式 Fork 的逻辑历史横跨多个物理文件;分页、按 Turn 查找和全文搜索都必须先解析 lineage,再在可见 segment 范围内处理 cursor 与同 Turn shadowing。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 Paginated History 查询算法,不讨论元数据 Thread 列表搜索。输入:requested ThreadId、RolloutLineage、方向/排序/页大小/cursor/搜索词;状态所有者:thread_history read、segment_paging、turn_lookup 与 search;成功结果:逻辑可见的 TurnPage、ItemPage、单 Turn 或匹配片段。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入requested ThreadId、RolloutLineage、方向/排序/页大小/cursor/搜索词
状态所有者thread_history read、segment_paging、turn_lookup 与 search
成功产物逻辑可见的 TurnPage、ItemPage、单 Turn 或匹配片段
研究范围研究 Paginated History 查询算法,不讨论元数据 Thread 列表搜索

正常路径:先看顺序点

History 分段分页、Turn Lookup 和搜索的正常路径时序图,展示验证 Paginated/DB/参数、解析 lineage 与 cursor scope、按方向选择物理 segments、查询 page_size+1 并去除 shadowed ancestor、合并结果生成下一 cursor
图 8.19-1:验证 Paginated/DB/参数 → 解析 lineage 与 cursor scope → 按方向选择物理 segments → 查询 page_size+1 并去除 shadowed ancestor → 合并结果生成下一 cursor。这张图标出本节的实际顺序点与状态所有者。

这条路径可以压缩成五步:

  1. 验证 Paginated/DB/参数。
  2. 解析 lineage 与 cursor scope。
  3. 按方向选择物理 segments。
  4. 查询 page_size+1 并去除 shadowed ancestor。
  5. 合并结果生成下一 cursor。

图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。

源码机制拆解

Cursor 绑定查询作用域

Cursor 编码 requested thread、anchor ordinal、include_anchor 和 scope。把 list_turns 的 cursor 用到搜索,或把父 Thread cursor 用到 child,会被拒绝而不是返回难以解释的页。

Lineage segment 有明确区间

每个 segment 带 start_ordinal 和可选 frozen end。查询只读取该范围,祖先文件后来新增的 Turn 不会泄漏进已经 Fork 的 child。

新 segment 可以遮蔽祖先 Turn

同一 turn_id 若在更近的 segment 出现更新,分页会过滤旧 segment 的版本。Turn Lookup 的 visible 路径从新到旧找,source 路径则从旧到新找物理起点。

page_size+1 判定是否还有下一页

查询多取一个元素,截断为页面大小后用额外元素生成 cursor。segment 边界切换也被编码进遍历状态,客户端不需知道物理文件。

搜索输出客户端可用位置

只搜索 User Message 与 Final Agent Message;assistant Markdown 先去标记,匹配为字面量大小写不敏感。片段保留前 48/后 96 字符,并输出 UTF-16 offset 适配常见客户端。

Python 风格伪代码

下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:

async def page_visible_turns(thread_id, query, cursor):
    require(query.page_size > 0)
    lineage = await resolve_lineage(thread_id)
    anchor = validate_cursor(cursor, thread_id, scope=query.scope)
    collected = []

    for segment in ordered_segments(lineage, query.direction, anchor):
        rows = await db.query_turns(
            physical_thread=segment.thread_id,
            ordinal_range=segment.range,
            anchor=anchor.for_segment(segment),
            limit=query.page_size + 1,
        )
        for row in rows:
            if not shadowed_by_newer_segment(row.turn_id, segment, lineage):
                collected.append(row)
            if len(collected) > query.page_size:
                break

    page, extra = collected[:query.page_size], collected[query.page_size:]
    return Page(page, make_scoped_cursor(page[-1]) if extra else None)

阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

History 分段分页、Turn Lookup 和搜索的失败路径图,区分Legacy Thread 请求分页、Cursor 属于另一 Thread/scope、UpdatedAt 跨 lineage 无 watermark、祖先 Turn 被 child 更新遮蔽
图 8.19-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
Legacy Thread 请求分页没有结构化 projection contract返回 Unsupported使用全量历史兼容路径
Cursor 属于另一 Thread/scopeanchor 语义不匹配InvalidRequest客户端重启该查询
UpdatedAt 跨 lineage 无 watermark新写入改变全局排序翻页可能重复/漏项排序 cursor 携带 watermark
祖先 Turn 被 child 更新遮蔽物理 DB 有两行只返回逻辑可见的新版本segment-aware shadow filter

这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。

必须保持的不变量

  • 查询结果不越过每个 segment 的 frozen bounds
  • cursor 不能跨 scope 复用
  • 可见 TurnId 在逻辑页中至多出现一次
  • 搜索 offset 按客户端 UTF-16 语义返回

这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。

设计取舍

统一逻辑分页隐藏了物理 lineage,却让 cursor 与排序规则更复杂;这是引用式 Fork 节省复制成本后必须支付的查询成本。

源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。

Mini Codex 复刻

先把 lineage 展开成有界 segment 列表,再写 segment iterator;cursor 包含 thread/scope/anchor,测试跨段、遮蔽与错用 cursor。

复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。

源码导航

相邻测试也很重要:

本节结论

引用式 Fork 的逻辑历史横跨多个物理文件;分页、按 Turn 查找和全文搜索都必须先解析 lineage,再在可见 segment 范围内处理 cursor 与同 Turn shadowing。

阅读导航

上一节:8.18 · 下一节:8.20

评论


← 返回文章列表