你让 Codex 给 UserService.ts 加一个 validate() 方法。它返回一份漂亮的 diff——但 diff 里建了个 UserServiceV2.ts 把方法放进去了,原文件一个字都没改。结果:你的 import 还指向旧类,新文件是死代码,最后还是你自己手动把变更挪回去。
最快修复:删掉那个重复文件,然后用明确措辞重新发:就地编辑 src/UserService.ts,不要新建文件;如果补丁打不干净,就停下来解释原因。 这一句就能解决大部分情况。如果还是反复出现,根因几乎一定是 Codex 的 apply_patch 从 *** Update File: 退化成了 *** Add File:(补丁没打上,或沙箱禁止覆盖)。彻底的修复办法是一条 AGENTS.md 规则加上正确的沙箱模式,下面都会讲。
根因:apply_patch 的 Update→Add 回退
Codex 通过一个工具 apply_patch 来改文件,每个 hunk 都在补丁头里声明意图:
*** Begin Patch
*** Update File: src/UserService.ts ← 就地修改文件
*** Add File: src/UserServiceV2.ts ← 新建一个全新文件
*** Delete File: src/old.ts
*** End Patch
之所以冒出重复文件,是因为 Codex 本来想发 *** Update File:,结果发成了 *** Add File:。原因分两大类:模型主动选这么做(你的 prompt、不熟悉的文件,或者没有规则约束它),或者它被逼着这么做(Update 补丁打不上去,或沙箱禁止覆盖,于是它建了个兄弟文件)。下面这张表帮你把症状对应到正确的桶,对症下药。
| 桶 | 信号 | 最快修复 |
|---|---|---|
| Prompt 措辞 | prompt 里有 “new version”、“v2”、“improved”、“refactored” | 改成「就地编辑」重发 |
| 不熟悉的代码 | 新文件写法比原文件更朴素(裸函数 vs 带 decorator 的类) | 加一条 AGENTS.md 就地规则(Step 2) |
| 没规则 | AGENTS.md 没写文件创建策略 | 加一条 AGENTS.md 就地规则(Step 2) |
| 补丁失败 | chat 里在新文件之前出现 “patch failed” / “could not apply” | 先 commit,再让它打更小的定向补丁(Step 3) |
| 沙箱禁覆盖 | 对文件做 no-op edit 报权限/写入错误 | 修 sandbox_mode(Step 4) |
| 本来就该两份 | 任务确实要做 TS 移植或一个变种 | 不是 bug——按用途命名(Step 7) |
常见原因
按命中率从高到低:
1. Prompt 里用了「new version」「v2」这种词
Codex 对文件命名词非常字面。「写一个 utils.ts 的 new version」会产出 utils-new.ts;「做个 v2」就给你 utils.v2.ts。线索是你自己给的,不是 Codex 主动选的。
如何判断:在 prompt 里搜 “new”、“v2”、“improved”、“refactored”。改成「edit in place」重跑一遍,重复文件就消失。
2. 原文件里有 Codex 不愿动的写法
utils.ts 用了自定义 decorator 或不常见的泛型——模型对这类写法把握不大。它就对冲:原文件不动,新文件用它熟悉的写法重写。
如何判断:对比新旧文件的写法——新的明显更朴素(裸函数 vs 带 decorator 的类),这种不对称就是信号。
3. AGENTS.md 里没写文件创建规则
没明确规则时,Codex 在改动量大时偏向「新建」——因为 *** Add File: 可逆(删掉就行),而把原文件改坏的 *** Update File: 不一定可逆。
如何判断:grep -i "in place\|do not create" AGENTS.md——空就是没规则。注意 Codex 会从 git root 一路读到你当前目录(外加全局的 ~/.codex/AGENTS.md)并拼接起来,所以每一层都要检查。
4. Update 补丁太大、打不干净
Codex 试了 *** Update File: 编辑,补丁因为上下文行和磁盘上的文件对不上而失败,于是退化成整文件 *** Add File:。这个回退不一定会清楚地暴露出来。
如何判断:在 transcript 里搜 “patch failed”、“could not apply”、“context did not match”——紧挨在新文件出现之前就是这个原因。
5. 沙箱模式禁了覆盖
Codex 在沙箱里执行命令。默认是 workspace-write(读所有、在工作区内写)。但如果它实际上变成了只读——sandbox_mode 配错、编辑目标不在 sandbox_workspace_write.writable_roots 里,或者命中已知的 Windows 桌面应用 bug(apply_patch 能新建文件但无法更新现有文件,openai/codex issue #25860)——它就绕开限制建了个并行文件。
如何判断:对 utils.ts 做个 no-op edit(加个行尾空格)。报权限或写入错误就是沙箱拦了就地写入。在桌面应用上,留意那种 Add 成功、Update 却返回 exit code 1 且无输出的 apply_patch。
6. Task 本来就是要并行实现
「加一个 utils.js 的 TypeScript 版本」「做一个严格模式变种」——这两种本来就要两份文件。先确认你不是要「就地替换」。
如何判断:回看原 task。没出现「就地」「替换」这种词,重复可能是合规的。
最短修复路径
按收益从高到低,Step 1 一步覆盖大部分情况。
Step 1:删掉重复文件,用明确「就地」措辞重 prompt
删掉新建那个,保留原文件,发:
直接就地(IN PLACE)编辑 `src/utils.ts`。
- 用 apply_patch 的 *** Update File:,不要用 *** Add File:。
- 不要新建文件。
- 不要重命名文件。
- 直接改原文件。
- 如果补丁打不干净,停下来解释原因,不要建替代文件。
点名 *** Update File: 机制和「停下来解释」这两条都很关键:它们一起堵死了「新建重复文件」这个逃生口,换成一个你能据此处理的干净失败。
Step 2:把规则永久写进 AGENTS.md
在 repo 根目录加进 AGENTS.md。Codex 每次运行都会读它,从 git root 一路拼接到你的工作目录(想全局生效就放 ~/.codex/AGENTS.md),冲突时靠后的文件胜出;拼接后的指令链默认上限 32 KiB。
## 文件创建策略
- 永远就地编辑现有文件(apply_patch 用 `*** Update File:`)。
- 不允许的变种命名:`*-new.ts`、`*.v2.ts`、`*_copy.ts`、`*-improved.ts`、`*-refactored.ts`。
- 仅以下情况可以新建文件(`*** Add File:`):
- 用户明确说 "create a new file"
- 新功能确实需要新模块
- 给现有模块加测试文件(如 `utils.test.ts`)
- 就地 edit 风险大、或补丁打不上时,先停下来问,不要自作主张建新文件。
规则跨 session 生效。(提示:codex 的 /init 不会覆盖已存在的 AGENTS.md,所以直接改文件。)
Step 3:用 git 兜底,不要靠新建文件兜底
Codex 之所以「对冲」,是怕弄坏原文件。把这个担忧用 git 接走:
# 改动前打一个 checkpoint
git add -A && git commit -m "checkpoint before Codex edit"
# 让 Codex 就地改
# ... 如果改坏了:
git restore src/utils.ts # 一行回滚
在 prompt 里告诉它:
当前状态已经 commit,改坏了我可以 `git restore` 立即回滚。
请就地 edit——git 是安全网,不要靠建新文件兜底。
如果重复来自补丁失败(「补丁失败」那个桶),再让 Codex 做更小、更定向的改动:「只改 validate 方法,文件其余部分一字节都不要动。」更小的 hunk 更容易和文件上下文行对上,几乎不会触发 Add 回退。
Step 4:确认沙箱真的能就地写入
如果 no-op edit 失败(对应桶 5),别跟症状死磕,去查沙箱模式。默认的 workspace-write 应该允许 Codex 更新工作区内的文件。
# 查看当前模式和可写根目录
codex --help # 显示 --sandbox 和审批相关 flag
# 用能写工作区的模式跑一个 session:
codex --sandbox workspace-write
或者写进 ~/.codex/config.toml:
sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access
approval_policy = "on-request"
对于那个已知的 Windows 桌面应用 bug(apply_patch 能 add 不能 update),官方建议的临时绕法是把 session 切到完全访问(仅在你信任该任务时用):
codex --sandbox danger-full-access
确认你要编辑的目录被 sandbox_workspace_write.writable_roots 覆盖;编辑目标落在这些根目录之外会被当成只读,把 Codex 推向建兄弟文件。
Step 5:清理历史遗留的重复文件
repo 里之前 session 留下的 *-v2、*-new、*-copy 全部列出来:
find src -type f \( \
-name "*-new.*" -o \
-name "*.v2.*" -o \
-name "*-v2.*" -o \
-name "*_copy.*" -o \
-name "*-copy.*" -o \
-name "*-refactored.*" \
\) | sort
每一对决定保留哪个:
| 旧文件 | 新文件 | 处理 |
|---|---|---|
| utils.ts | utils-new.ts | utils-new 内容并回 utils.ts,删 utils-new |
| UserService.ts | UserServiceV2.ts | 如果 import 用 V2,rename V2 → 原名;删未被 import 的那个 |
然后用 IDE 的 “rename symbol” 批量改 import。
Step 6:在 PR review 阶段拦截变种文件名
CI 加一条:新增文件名匹配变种模式就 fail:
# .github/workflows/no-variant-files.yml
- name: Block variant filenames
run: |
if git diff --name-only --diff-filter=A origin/main...HEAD | grep -E '(\-new|\.v2\.|\-v2\.|_copy\.|\-copy\.|\-refactored)' ; then
echo "不允许变种文件名,请就地编辑。"
exit 1
fi
Step 7:真要并行实现,按用途命名而不是按版本
两种实现真要共存(老 API + 新 API),按功能命名,不要按版本:
auth/oauth.ts + auth/magic-link.ts ← 好
api/users-v1.ts + api/users-v2.ts ← 公开 API 版本可接受
api/utils.ts + api/utils-new.ts ← 永远不要
这个命名约定能让 Codex 不把「改」理解成「再来一个变种」。
如何确认已修好
- 在就地 prompt 和
AGENTS.md规则都到位后,重跑原任务。 - 看 diff:补丁头应该是
*** Update File: src/utils.ts,而不是*** Add File:。 - 确认没有新建兄弟文件:
git status --short显示M src/utils.ts,而不是一行新的A。 - 确认 import 仍然解析得到、测试通过——改动落在了你代码真正 import 的那个文件里。
预防建议
- 「就地编辑」写进
AGENTS.md一次就够——Codex 每次运行都读。 - 除非真要分文件,否则 prompt 里不要出现 “new version”、“v2”、“improved”、“refactored”。
- 改高风险代码前先 commit 一次,让 Codex 不必「防灾性新建」。
- 保持
sandbox_mode = "workspace-write"(或配好正确的writable_roots),别让就地更新被拦成兄弟文件。 - CI 加一条变种文件名检查——人工 review 漏掉的它能拦。
- 季度审一次现存变种文件——它们悄悄堆积、最后让 import target 一团乱。
- 真要并行实现,按功能命名(
oauth+magic-link),不要按版本。
常见问题
为什么 Codex 建了 UserServiceV2.ts 而不去改 UserService.ts?
因为它的 apply_patch 发出的是 *** Add File: 而不是 *** Update File:。触发原因有:带版本号味道的 prompt 措辞、不熟悉的原文件、AGENTS.md 里缺就地规则、补丁打不上、或沙箱禁止覆盖。用上面那张桶表对号入座。
怎么强制 Codex 永远就地编辑?
在 AGENTS.md 里加一段「文件创建策略」(Step 2),强制 *** Update File: 并禁止变种文件名,同时任务开头写「就地编辑,不要新建文件」。AGENTS.md 规则对每次运行都生效,prompt 那句管当前这次。
Codex 说补丁失败然后又建了个新文件,怎么办? 这就是 Update→Add 回退。先 commit 一个 checkpoint,再让它做更小、更定向的改动(「只改这个函数,其余不动」)。更小的 hunk 更容易和文件上下文行匹配,于是 update 能打上,不会退化成 Add。
在 Windows 上桌面应用一直只新建、不更新,是 bug 吗?
是——这是已知问题(openai/codex #25860):apply_patch 能新建文件但无法更新现有文件,常常返回 exit code 1 且无输出。用 --sandbox danger-full-access 跑 session 是官方记录的绕法;同样的流程在 WSL 下是正常的。
Codex 建第二个文件有没有合理的时候?
有。把 JS 文件真正移植成 TypeScript、确实是新模块、测试文件,或者两个公开 API 版本(users-v1 / users-v2),都该是独立文件。有问题的只是那种遮住你本想就地改的文件的版本后缀兄弟文件(-new、.v2、-copy)。
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #复制文件