雨天小六

读懂 Codex(8.27):Legacy Rollback Marker 的追加式回退

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

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

Legacy Rollback 不删除旧行,而是在 flush 后对“旧历史 + 新 marker”做一次完整重建,先更新热 Session,再把同一 marker 追加并刷新;分页模式在 App Server 入口明确拒绝。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 thread_rollback 的前置条件、重放、marker 耐久与外部副作用边界。输入:Legacy Thread、num_turns>=1 且当前无 active turn;状态所有者:Session handler + Rollout Reconstruction;成功结果:删除最近 N 个真实用户 Turn 的有效会话历史和追加 marker。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入Legacy Thread、num_turns>=1 且当前无 active turn
状态所有者Session handler + Rollout Reconstruction
成功产物删除最近 N 个真实用户 Turn 的有效会话历史和追加 marker
研究范围研究 thread_rollback 的前置条件、重放、marker 耐久与外部副作用边界

正常路径:先看顺序点

Legacy Rollback Marker 的追加式回退的正常路径时序图,展示入口拒绝 Paginated/零值/并发 rollback、确认无 active turn、Flush 并加载规范历史、临时追加 marker 重建 Session、持久化 marker 再 Flush/通知客户端
图 8.27-1:入口拒绝 Paginated/零值/并发 rollback → 确认无 active turn → Flush 并加载规范历史 → 临时追加 marker 重建 Session → 持久化 marker 再 Flush/通知客户端。这张图标出本节的实际顺序点与状态所有者。

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

  1. 入口拒绝 Paginated/零值/并发 rollback。
  2. 确认无 active turn。
  3. Flush 并加载规范历史。
  4. 临时追加 marker 重建 Session。
  5. 持久化 marker 再 Flush/通知客户端。

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

源码机制拆解

前置条件在副作用前检查

App Server 先检查 history_mode、numTurns 和 pending rollback;Core 再检查 active_turn、持久化句柄。任一失败都不改变 Context History。

先 Flush 再读避免漏掉尾部

record_conversation_items 的写入可能仍在 Recorder 队列。Rollback 在 load_history 前等待 flush,确保计算基于当前规范前缀。

同一 Marker 驱动热/冷结果

handler 创建 ThreadRolledBackEvent,把它临时接到 loaded items 末尾调用 apply_rollout_reconstruction;随后持久化相同 EventMsg。热 Session 和下一次 Resume 因此共享 reducer。

Marker 写失败不撤销内存回退

Session 已切换到回退后状态时,flush marker 若失败会发 Warning,并让 Recorder 保留 pending 继续重试。不能假装回退没发生,也不能重做外部工具。

回退单位是真实用户 Turn

drop/reconstruction 识别普通 user instruction 与 inter-agent assistant instruction 边界;无 user boundary 的维护任务不计数,超出历史则清到首个 user turn 前缀。

Python 风格伪代码

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

async def legacy_rollback(session, num_turns):
    require(num_turns >= 1)
    require(session.history_mode == LEGACY)
    require(session.active_turn is None)
    live = session.require_live_thread()

    await live.flush()
    stored = await live.load_history(include_archived=False)
    marker = ThreadRolledBack(num_turns=num_turns)

    reconstructed = reconstruct(stored.items + [marker])
    session.apply_reconstructed_state(reconstructed)
    session.recompute_token_usage()

    await session.persist(marker)
    try:
        await session.flush_rollout()
    except PersistenceError as error:
        session.warn('rollback active, marker pending retry', error)
    session.deliver_event(marker)

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

失败、取消与恢复

Legacy Rollback Marker 的追加式回退的失败路径图,区分num_turns=0、Turn 正在运行、初始 Flush/Load 失败、Marker 最终 Flush 失败
图 8.27-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
num_turns=0无明确状态变化InvalidRequest/ThreadRollbackFailed不写 marker
Turn 正在运行历史仍在追加拒绝 rollback先 interrupt/等待终态
初始 Flush/Load 失败无法证明当前规范前缀不改内存 history返回失败
Marker 最终 Flush 失败内存已经回退、pending 未耐久WarningRecorder 后续屏障继续重试

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

必须保持的不变量

  • Paginated Thread 不走 Legacy Rollback
  • 回退前先基于已 flush 的规范历史
  • 物理旧记录不被删除
  • 文件/Git/网络/进程副作用完全不在 Rollback 补偿范围

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

设计取舍

追加 marker 保住审计历史且易于重复 replay,但让物理日志继续增长,也无法提供工作区级 undo。

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

Mini Codex 复刻

event log 追加 Rollback(N),reducer 计算 effective history;API 明确命名 conversation rollback,并在文档中拒绝暗示文件恢复。

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

源码导航

相邻测试也很重要:

本节结论

Legacy Rollback 不删除旧行,而是在 flush 后对“旧历史 + 新 marker”做一次完整重建,先更新热 Session,再把同一 marker 追加并刷新;分页模式在 App Server 入口明确拒绝。

阅读导航

上一节:8.26 · 下一节:8.28

评论


← 返回文章列表