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 耐久与外部副作用边界 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 入口拒绝 Paginated/零值/并发 rollback。
- 确认无 active turn。
- Flush 并加载规范历史。
- 临时追加 marker 重建 Session。
- 持久化 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 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| num_turns=0 | 无明确状态变化 | InvalidRequest/ThreadRollbackFailed | 不写 marker |
| Turn 正在运行 | 历史仍在追加 | 拒绝 rollback | 先 interrupt/等待终态 |
| 初始 Flush/Load 失败 | 无法证明当前规范前缀 | 不改内存 history | 返回失败 |
| Marker 最终 Flush 失败 | 内存已经回退、pending 未耐久 | Warning | Recorder 后续屏障继续重试 |
这里没有统一的“回滚一切”。内存状态、日志行、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。
源码导航
- codex-rs/app-server/src/request_processors/thread_processor.rs:Paginated 拒绝与 pending rollback gate
- codex-rs/core/src/session/handlers.rs:Flush、Load、重建、持久化 marker 和 warning
- codex-rs/core/src/session/rollout_reconstruction.rs:Marker 的 segment skip 和前向 replay
相邻测试也很重要:
- codex-rs/app-server/tests/suite/v2/thread_rollback.rs:Paginated 拒绝、Legacy RPC 和 marker 落盘
- codex-rs/core/src/session/tests.rs:零值、active turn、累计 rollback、设置恢复和 marker 持久化
- codex-rs/core/src/session/rollout_reconstruction_tests.rs:完成/未完成/非用户/inter-agent Turn 计数
本节结论
Legacy Rollback 不删除旧行,而是在 flush 后对“旧历史 + 新 marker”做一次完整重建,先更新热 Session,再把同一 marker 追加并刷新;分页模式在 App Server 入口明确拒绝。
评论
登录后即可评论