上周我让Codex改一个API接口的参数校验逻辑,它一顿操作改了好几个文件,改完我发现方向完全跑偏了。双击Esc回退三轮对话,上下文确实回去了。然后我习惯性地打开编辑器想看看"之前的代码长什么样",发现文件根本没动,还是被改过之后的状态。 当时我愣了一下,心想:这不对吧? 然后更离谱的事来了。我接着在回退后的对话里问它"你刚才那个参数校验改完之后,类型定义还一致吗",它居然煞有介事地分析了一遍。问题是它分析的是被覆盖后的文件,而不是回退前的。它拿着新文件假装在看旧代码,我在旁边看得直冒冷汗。 这不是个小bug。模型拿到的是回退后的旧对话,看到的却是磁盘上的最新文件,两边对不上,它就在一本正经地胡说八道。这个问题在Codex的issue tracker上挂了大半年,#22100里写得明明白白。#9203要求把 /undo 加回来,#11626要求"对话和代码一起回滚",截至本月两个issue都还开着。
官方做过两次,两次都死在同一个地方
这事最让我意外的不是"没做",而是"做了两次都没做成"。
第一次用的是 Feature::GhostCommit,配置键叫 undo。思路很直觉:在用户的git仓库里写游离commit,回退时checkout到之前的状态。听着没毛病对吧?结果在 #8214 里直接造成了数据丢失------恢复路径动了用户的index,有些场景下文件直接被覆盖了。修复之后两天,整个功能在 PR #8424 里被整体下线。 我第一次看到这段历史的时候,觉得大概是"工程师不够小心"。但仔细看完技术分析,发现不是这么回事。 问题的根子在于:为了安全,每次快照要跑一次 git status --untracked-files=all,外加一个一次性的临时index。临时index是故意的安全设计------为了不碰用户自己的index。但代价是git的stat cache完全失效,每次快照都要从头哈希所有改动过的文件。 git status 之所以快,靠的就是stat cache。为了安全丢掉它,快照就慢下来了;慢下来之后,被迫加异步执行、加一个240秒的watchdog、加一堆硬编码排除列表(超10MB跳过、超200条目目录跳过、node_modules跳过......)。一条很清楚的因果链:为了安全避开用户的index → 失去stat cache → 慢 → 复杂度一层层堆上去。
第二次尝试换了个姿势,但犯了同一个错误------还是往用户的仓库里写东西。Codex Desktop现在还在用户的仓库里、refs/codex/turn-diffs/ 下面保存整棵树的git对象检查点。这次没造成数据丢失,但造成了另一个问题:一个5.7GB的项目里长出了102GB的孤儿对象(#29388),没有GC。而且这些非标准refs还让基于libgit2的第三方客户端出了兼容性故障(#28241)。
两次实现,一个丢数据,一个撑爆磁盘。 看到这儿我其实有点哭笑不得。两拨人都很聪明,都做了安全设计,结果安全设计本身成了问题的根源。这就像你为了防贼在门口装了十道锁,结果每次回家自己也得花半小时开门------最后你不是被贼烦死的,是被自己的安全措施折腾死的。 结论其实就一句话:用户的仓库不是Codex该往里写东西的地方。
那怎么办?有人换了个思路
知道这个问题挂了大半年没解决,我中间一度认命了------自己每轮手动commit呗,还能咋地。直到最近在GitHub上翻到一个叫 codex-rewind 的项目,仔细看了看它的设计,觉得"嗯,这个思路确实不一样"。
python
CODEX_HOME/file_snapshots/
blobs/<xx>/<hash> # 内容寻址的文件内容,天然去重
manifests/<manifest-id> # 每个检查点一份:[(path, mode, size, mtime, hash)]
refs/<thread-id> # thread → (turn_id, manifest-id) 列表
turns/<turn-id> # turn → manifest-id
这个选择直接绕开了前两次翻车的整个地雷区。没有孤儿对象------因为不往git仓库写任何东西。没有index被破坏------因为根本不碰.git。没有被第三方客户端误读------因为那些检查点待在Codex自己的目录里,git工具链根本看不见。 但它引入了一个新问题:磁盘占用没有现成工具帮你回收。git gc不会管 CODEX_HOME 下面的东西。所以这个子系统必须自己实现GC------引用计数加mark-and-sweep,生命周期绑定到会话上。 安装和使用倒是简单:
css
npm i -g codex-rewind
codexr --enable file_snapshots
codexr 和原版 codex 命令并存,不替换。开启文件快照后开始新会话,文件快照在后台随每个turn自动记录。回退的时候输入 /rewind,选一条之前的提示词,对话和文件一起回去。改主意了输入 /redo 走回来。 不过有个限制得提前知道:这个开关是会话级的,不是每次调用级的。新会话开了就跟踪,已经跑到一半的旧会话中途开不了------不能补录前面的快照。
跟踪哪些文件------这个设计值得细看
不遍历整棵目录树,这是codex-rewind和前两次尝试最本质的区别。它用三个"有界分区"的并集来确定跟踪范围:
| 分区 | 来源 | 边界 | 在Codex仓库上的实测 |
|---|---|---|---|
| git-tracked | 直接读 .git/index(用gix库,不fork git命令) |
项目边界 | 5,996文件 / 56.4MB / 265ms |
| edit-touched | 本次会话中Agent工具实际写过的路径 | 会话动作边界 | 与改动量成正比 |
| recently modified | 前两者之外,按mtime排序的补充 | 硬上限:最多100个额外文件,单文件16MB | 100文件 / 2.4MB / 160ms |
关键在"有界"两个字。同一个checkout上,完整子树遍历是 70,609个文件 / 116GB------几乎全是构建产物。三个分区的并集约 6,096个文件 / 59MB。文件数是1/11.6,体积是1/1975。 更有意思的是时间维度的对比:同一个仓库隔几个小时再测,子树遍历从58,116文件涨到70,609文件,多了21%,纯粹来自这几个小时的编译。而git分区一动没动,两次都是5,996个文件。 一个随"仓库被工作了多久"增长的开销,是没法做预算的。这也解释了为什么第一次尝试加了一堆硬编码排除列表还是扛不住------排除列表追不上构建目录的膨胀速度。 当然,覆盖率不是100%,这个作者自己也直说了:一个已经脱离git跟踪、之后只被shell命令改动、又掉出了"最近100个"窗口的文件,不被跟踪。交集很窄,但确实存在。
为什么不直接用git
"在git仓库里工作,git checkout 不就行了?"------这个反驳在每个相关讨论串里都会出现。 如果你在git仓库里、工作区干净、开着Agent的人是开发者,那确实够用。但问题是:越来越多人拿Codex干的活不是写代码。文案、表格、研究笔记、合同草稿------在这些目录里 git init 不是工作流,是一件戏服。 而且就算在git仓库里,"用git就行"这个方案落到最后也是手工活。有人在issue里吐槽得很直接:
Since switching from Claude Code to Codex, I have to commit to Git after every single turn in the conversation, which is a huge hassle.
每轮提交一次,烦不烦?用stash的话,stash自己有它的坑。让模型记得打检查点呢------这是在用上下文窗口来做簿记,和任何"需要模型记住"的事情一样不可靠。让Agent自己撤销呢------shell命令、MCP工具调用改文件不会留下结构化的变更记录,Agent没法从一段对话记录里精确重建"我改了什么"。
而最硬的证据是:OpenAI做了两次。如果 git checkout 就能覆盖,第一次就不会上线又被拉下来,第二次也不会至今还留在Desktop里以另一种方式出问题。
顺带说一句,其他工具也没完全搞定
Claude Code有checkpoint,但只跟踪内置编辑工具的改动,shell命令改的文件不在恢复范围。OpenCode在内部维护git仓库做快照,官方文档自己都承认大仓库会慢会占磁盘。这问题各家都有自己的取舍,目前没有完美方案。codex-rewind至少选了一条被证明走不通的反方向:不碰git。
写在最后
这个问题本质上是AI编程工具普遍还没想清楚的一件事:Agent改了文件之后,"后悔药"到底该怎么做? 对话回退只是表象。真正的问题是,当Agent越来越自主,不只是写代码,还会跑shell、调API、改配置文件,我们给它的"撤销"能力远远跟不上它"搞事"的速度。Codex的两次翻车、codex-rewind的第三种尝试,本质上都是在回答同一个问题:AI工具的状态管理,到底该由谁来管、管多深。
我装上codex-rewind用了几天,日常写代码的场景基本够用了。不过我有个习惯是每个任务开新会话,所以"中途不能开关"这个限制对我影响不大。如果你的工作流是长时间在一个会话里反复调整,可能会碰到覆盖盲区。 准备再观察一段时间,稳定了的话就把它写进团队的标准工作流里。项目地址:github.com/extracurric... ,RFC写得比这篇文章详细得多,想深挖的可以直接看。