具体问题与边界
Patch 为什么必须先解析成操作、验证所有目标,再进入可产生副作用的写入阶段?
Mini PatchParser 支持 Add/Update/Delete 子集;PatchApplier 把语法操作转换成完整 PatchPlan;审批后才 stage/replace。Move、模糊 context 和流式 preview 未实现。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。
状态所有权
| 对象 | 所有者 | 生命周期 | 持久化 |
|---|---|---|---|
| patch text | ToolCall | 单次调用 | 随 Call 持久化 |
| PatchOperation/Hunk | PatchParser | 解析到计划完成 | 否 |
| PatchPlan writes/deletes | PatchApplier | 一次审批/提交 | 否 |
| temporary files | PatchApplier.apply | commit 期间 | 否 |
| workspace files | 文件系统 | 跨 Turn | 外部副作用,不在 Rollout 内存储内容 |
正常路径
- Parser 强制首尾 marker,并按 header 识别 Add File、Update File 和 Delete File。Add 内容行必须以
+开头;Update hunk 行必须以空格、+或-开头。 - 同一路径在一个 Patch 中只能出现一次。Parser 只建立领域操作,不读取文件系统。
- Planner 对每个路径先走 WorkspacePolicy.resolve,再验证存在性;Update 的 old hunk 必须在当前内容中恰好出现一次。
- 所有操作都在内存中规划成功后才请求 Approval。因此“后一个文件缺失”不会让前一个 Add 提前落盘。
- Commit 为每个 write 在目标目录 stage 临时文件并 fsync,再按顺序 os.replace,最后 delete;发生 OS 错误时报告 partial commit,不声称跨文件事务。
顺序为什么不能交换
Parser 强制首尾 marker,并按 header 识别 Add File、Update File 和 Delete File
→ 同一路径在一个 Patch 中只能出现一次
→ Planner 对每个路径先走 WorkspacePolicy.resolve,再验证存在性;Update 的 old hunk 必须在当前内容中恰好出现一次
→ 所有操作都在内存中规划成功后才请求 Approval
→ Commit 为每个 write 在目标目录 stage 临时文件并 fsync,再按顺序 os.replace,最后 delete;发生 OS 错误时报告 partial commit,不声称跨文件事务
箭头代表可见性与所有权转移,不是松散依赖。Policy、Approval、外部副作用、规范 Item 和 durability 各自有提交点,后一步不能替前一步作更强承诺。
Python 风格伪代码
operations = PatchParser.parse(patch_text)
def plan(operations):
writes, deletes = [], []
for op in operations:
path = workspace.resolve(op.path)
match op:
case Add(content):
require_not_exists(path)
writes.append((path, content))
case Update(hunks):
content = read_existing(path)
for hunk in hunks:
require(content.count(hunk.old) == 1)
content = content.replace(hunk.old, hunk.new, 1)
writes.append((path, content))
case Delete():
require_regular_file(path)
deletes.append(path)
return PatchPlan(writes, deletes) # no mutation yet
if await approval(plan.summary):
staged = [write_temp_and_fsync(target, text) for target, text in plan.writes]
for temp, target in staged: os.replace(temp, target)
for target in plan.deletes: target.unlink()
失败、取消与恢复
| 故障点 | 已留下的状态 | 处理 |
|---|---|---|
| marker/header 非法 | 没有领域 Operation | 错误 ToolResult,无写入 |
| hunk 0 次或多次匹配 | 目标位置不唯一 | 规划失败,无写入 |
| 路径逃出 workspace | resolve 拒绝 | 规划失败 |
| Approval deny | Plan 已存在但无文件副作用 | 模型看到 rejected |
| 第二个操作校验失败 | 第一个操作仍未提交 | 全计划预验证 |
| commit 中途 OS 错误 | 部分 replace 可能已完成 | 报告 partial commit,不伪造 rollback |
不变量
语法错误和规划错误必须发生在任何文件修改之前;commit 开始后的跨文件部分失败必须如实暴露。
设计思路与限制
官方测试明确覆盖 failure-after-partial-success;Mini 通过全计划预验证减少一类部分写,但 os.replace 序列仍不是全局事务。Mini 不支持 Move、End of File marker、宽松 shell 截取和 streaming parser。
测试与复现
cd examples/mini-codex
uv run pytest -q -k 'test_patch_parser_plans_all_files_before_commit or test_invalid_late_operation_does_not_apply_earlier_add or test_patch_changes_world_state_before_second_sample'
uv run mypy src
test_patch_parser_plans_all_files_before_committest_invalid_late_operation_does_not_apply_earlier_addtest_patch_changes_world_state_before_second_sample
官方源码导航
- codex-rs/apply-patch/src/parser.rs:Patch marker、Hunk、UpdateFileChunk 与 parse_patch
- codex-rs/apply-patch/src/streaming_parser.rs:参数增量期间的 Patch 状态机
- codex-rs/apply-patch/src/lib.rs:操作执行与文件写入顺序
- codex-rs/apply-patch/tests/suite/tool.rs:多操作、移动、错误和部分成功场景
- codex-rs/core/src/tools/handlers/apply_patch.rs:Tool handler 与审批/事件边界
Mini Codex 对照
src/mini_codex/tools/apply_patch.py:Parser、Plan、stage/commit 与 Toolsrc/mini_codex/policy/workspace.py:路径边界
本节结论
语法错误和规划错误必须发生在任何文件修改之前;commit 开始后的跨文件部分失败必须如实暴露。
评论
登录后即可评论