codex-rewind为OpenAI Codex CLI增加对话+文件联合回溯功能,基于Git快照实现工作流恢复,解决Agent状态下文件版本不一致问题。
Codex 可以将对话回退到更早的提示词。缺失的那一半是工作区:旧的对话可以回来,但更新的文件仍然留在磁盘上。
本指南展示如何现在就为 Codex CLI 添加同步的对话与文件回退功能。第一部分是实际的安装配置,其余则是 Rust 层面的快照设计解析:它如何覆盖 Git 追踪的文件、智能体直接编辑的文件,以及有限数量的近期 shell 产生的变更,而不会对整个工作区进行无界限的快照。
声明:本人开发了 codex-rewind,即本文所用的项目。它是 OpenAI Codex CLI 的非官方发行版,与 OpenAI 没有关联或受其支持。本文描述的是其实现方式及局限性,并非宣称它是适用于所有工作流的最佳智能体。
从 npm 安装 codex-rewind:
npm install -g codex-rewind
可执行文件名为 codexr,因此它可以与官方 codex 命令共存而不会覆盖它。用以下命令启动一个新的被追踪的会话:
codexr --enable file_snapshots
正常使用 Codex。当一次实验走向不对的方向时,运行:
/rewind # 选择一个更早的提示词;恢复对话及其文件
/redo # 撤销该回退并返回到恢复前的状态
关键的词是"一起"。仅恢复文件会使模型持有一段描述已不存在的编辑的对话。仅恢复对话则会使模型在更新的文件仍然留在磁盘上的情况下推理一个旧的世界。选定的回合是两个历史记录的共同坐标。
要为未来的新会话启用追踪,请将以下内容添加到现有的 Codex 配置中:
# ~/.codex/config.toml
[features]
file_snapshots = true
追踪是会话级别的。一个会话从一开始就对其整个生命周期启用快照追踪,或者完全没有此功能。在错误发生后才开启该功能无法为更早的回合创建快照。
Git 之外的小测试
你可以在一个普通目录中测试该行为;不需要仓库:
mkdir rewind-demo
cd rewind-demo
printf 'an uncommitted idea\n' > local-notes.md
codexr --enable file_snapshots
让智能体删除 local-notes.md,然后使用 /rewind 并选择删除之前的提示词。文件和对话都应该回到那个时刻。/redo 应该再次将两者向前推进。
这个例子故意不涉及 Git 技巧。一个有用的智能体级撤销缓冲区必须能够处理未追踪的笔记、生成的文档或从未成为仓库的目录。
实际的失败模式:两份历史记录渐行渐远
一个智能体会话中有两个相关状态:
决定模型相信发生了什么事的对话记录。
包含实际发生了什么之结果的 workspace。
Codex CLI 用户在 open issues #9203 和 #11626 中都曾要求统一的恢复功能。后者精确描述了这个差距:对话回退存在,但选定时间点之后的代码变更仍留在工作树中。
Git 仍然是持久化、可审查的项目历史的正确工具。它并不能完全替代每个回合级别的安全网:
从未被添加的文件没有可恢复的提交版本。
智能体工作越来越多地涉及仓库之外的笔记、文档和数据。
将每个探索性回合都提交或暂存会把可丢弃的智能体历史与有意的项目历史混在一起。
Shell 命令和 MCP 工具可以修改文件,但不会产生结构化的编辑记录,以便模型稍后可以反向重放。
因此,目标不是"取代 Git",而是"让每个恢复点都携带一致的对话状态和可逆的 workspace 状态。"
边界一:拥有快照存储,而非仓库
codex-rewind 在 Codex 自己的主目录下存储内容寻址的 blob、manifest 和每线程引用:
~/.codex/file_snapshots/
├── blobs/
├── manifests/
└── refs/
snapshot crate 不依赖 Codex 的其他 crate,不调用 Git,也不写入 .git。恢复操作从它自己的存储中写入 workspace 文件。Git 追踪分区可能读取 index 以获取项目拥有的文件路径,但快照对象、refs、提交和 index 变更永远不会进入用户的仓库。
这种分离关乎所有权和兼容性。会话历史属于智能体;仓库历史属于用户及其工具。
它还保持了官方 Codex 会话格式不变。现有的 rollout 文件不会有新字段或新事件类型。一个 sidecar 文件将 Codex 回合 ID 映射到快照 manifest ID:
Codex rollout: ... turn_id = "turn-42" ...
Snapshot sidecar: "turn-42" -> "manifest-b3..."
因此,相同的对话可以用 codex 或 codexr 打开而无需转换。这是格式兼容性,而非神奇的快照覆盖:在官方构建中运行的回合不会创建 sidecar 快照,因此在 codexr 中重新打开该会话无法追溯性地使那些回合具备文件回退能力。

边界二:追踪有限并集,而非整个树
将存储从 .git 中分离出来并不能回答应该观察多少 workspace 文件的问题。这是另一个不同的问题。
在这个子系统开发所用的仓库上,一次完整的子树遍历看到了 70,609 个文件,约 116 GB。其中大部分是构建输出、日志和缓存,通常不会进入 Git。有界限的追踪并集约为 6,096 个文件,59 MB。
这些数字并非在论证 Git 不好的问题。它们证明了为什么捕获范围不能是"工作目录下的所有内容"。即使实际项目没有变化,构建也可以不断膨胀那个树。
实现将三个分区联合起来:
在 Rust 中,顺序是承重的。最近的文件最后被选择,这样小预算就不会浪费在前两个分区已经覆盖的路径上:
pub fn tracked_files(
roots: &[PathBuf],
already_known: impl IntoIterator<Item = PathBuf>,
include_hidden: bool,
) -> BTreeSet<PathBuf> {
let ignores: Vec<Gitignore> = roots.iter().map(|root| load_ignore(root)).collect();
// Partition 1 in this function: files the agent already touched.
let mut files: BTreeSet<PathBuf> = already_known.into_iter().collect();
// Partition 2: project-owned paths read from the Git index.
for (root, ignore) in roots.iter().zip(&ignores) {
files.extend(git_tracked_files(root, ignore));
}
// Partition 3: bounded residue not already covered above.
for (root, ignore) in roots.iter().zip(&ignores) {
files.extend(recent_files(root, ignore, include_hidden, &files));
}
files
}
该注释节选中的"分区 1"和"分区 2"描述的是程序顺序;概念上我通常先列出 Git 追踪的,因为它更容易解释。无论哪种方式,结果都是集合并集。
只有 recency 分区需要任意的硬限制:
pub const RECENT_MAX_FILE_BYTES: u64 = 16 * 1024 * 1024;
pub const RECENT_LIMIT: usize = 100;
// After filtering ignored, covered, oversized and churn-directory entries:
candidates.sort_by_key(|(modified, _)| std::cmp::Reverse(*modified));
candidates.truncate(RECENT_LIMIT);
其他两个分区由含义而非数字限制来限定边界。Git 追踪的文件是项目的一部分,即使它很大。智能体触碰过的文件是相关的,因为会话特意更改了它。静默丢弃第 101 个智能体编辑会使最重要的分区变得不可靠。

116 GB 是产生这些工程权衡的观察结果:使用所有三个分区来扩大覆盖范围,但永远不让随机构建 churn 决定每个检查点的大小。

独立的 ignore 文件是一个功能特性,而非重复配置
回退和版本控制回答的是不同的策略问题:
.gitignore:这个路径应该进入共享仓库历史吗?
.codexsnapignore:回退系统可以捕获并稍后触碰这个路径吗?
这些集合可以重叠,但彼此都不应隐含对方。例如,local-notes.md 可能故意被排除在 Git 之外,但仍然足够有价值值得回退。数据库、凭证文件或生成的缓存可能无论 Git 策略如何都被排除在快照之外。
# .codexsnapignore
.env
data/*.sqlite
tmp/
匹配器使用熟悉的 Gitignore 语法,但只加载回退特定的文件:
pub const SNAPSHOT_IGNORE_FILENAME: &str = ".codexsnapignore";
```rust
pub fn load_ignore(root: &Path) -> Gitignore {
let mut builder = GitignoreBuilder::new(root);
builder.add(root.join(SNAPSHOT_IGNORE_FILENAME));
builder.build().unwrap_or_else(|_| Gitignore::empty())
}
pub fn is_ignored(ignore: &Gitignore, path: &Path) -> bool {
ignore.matched_path_or_any_parents(path, false).is_ignore()
}
这条规则是对称的:被忽略的路径在任何操作中都不会被捕获、恢复或删除。仅在捕获阶段应用这条规则会产生危险的不对称性——一个被排除的文件仍然可能在后续被删除。

安全的恢复需要证据,然后才是逃生舱
一个有界的快照无法诚实地说"每个不存在的路径都应该被删除"。缺失可能意味着"不曾存在",也可能意味着"不在追踪集合内"。混淆两者是恢复操作变成删除 bug 的根源。
因此,恢复规划器只从有见证的历史中删除:子系统必须已经观察到一个路径足够完整的生命周期,才能知道目标检查点显示它是不存在的。它不会从部分目录列表中推断删除操作。
在应用任何恢复之前,系统也会对当前状态进行快照检查。/redo 恢复该安全清单。这将错误选择的代价从丢失工作变成额外的一轮往返。
仍然存在真实的限制:
没有任何检查点见过的文件无法被恢复。
在 Git 索引之外的文件,仅被 shell 命令修改且已被推出 100 文件最近窗口的可能会被漏掉。
隐藏文件默认被跳过,除非被直接编辑;.git 仍然被排除在捕获之外。
回退无法撤销远程副作用,如已推送的分支、已发送的请求或数据库操作。
两个并发会话可能相互覆盖对方的工作区更改;这不是一个合并系统。
陈述这些缺陷比声称任何快照系统是"完整的"更有用。
为什么核心层和应用服务层边界很重要
仅在终端 UI 中实现的功能只解决了一个层面的问题,并制造了下一个兼容性问题。Codex 也有桌面客户端和 IDE 客户端,因此可复用单元必须位于任何单一界面之下。
捕获逻辑位于 Rust 核心层,那里可以看到回合边界和工具执行。恢复功能通过应用服务协议暴露。该协议的字段是有意精简的:
/// 同时将追踪的工作区文件恢复到分支点的状态。
/// 首先记录安全检查点,因此恢复操作是可逆的。
#[experimental("thread/fork.restoreFiles")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub restore_files: bool,
codex-rewind 目前暴露了 CLI/TUI 用户流程。桌面客户端和 IDE 客户端尚未搭载此实现,但核心存储和协议路径的结构化设计使得另一个 Codex 界面可以复用相同的捕获和恢复语义,而不是另行发明一种检查点格式。

这种跨项目、跨界面的关注点很容易在本地 UI 补丁中被忽略。这也是为什么"在我的终端里能工作"不足以证明回退架构适合整个 Codex。
该机制与 Claude Code 和 OpenCode 的对比
这是边界的对比,不是产品排行榜。
Claude Code 的检查点文档说明它追踪通过其文件编辑工具所做的更改,但不追踪由 Bash 命令修改的文件。这是一个干净且可预测的边界。codex-rewind 维护了一个等效的智能体触碰集合,然后添加 Git 追踪路径和有界的最近残留,以捕获更多 shell 和 MCP 更改。好处是更广泛的写路径覆盖;代价是更多的范围机制和一个明确的有界覆盖缺口。
OpenCode 的快照文档说明它使用内部 Git 仓库,并警告大型仓库或多个子模块可能导致索引缓慢和显著的磁盘使用。codex-rewind 则使用自己的内容寻址存储和有界的追踪集合。好处是无需 Git 仓库依赖且与代码树大小耦合更少;代价是自定义存储、自定义垃圾回收和无 Git 原生互操作性。

因此尝试 codex-rewind 的具体原因是狭义的:你使用 Codex,现在就需要对话和文件回退,想要超越直接编辑工具的覆盖范围,且不希望将撤销历史写入项目的 Git 状态。
关于兼容性问题的快速解答
codex-rewind 会取代官方 codex 命令吗?
不会。它安装了一个单独的 codexr 可执行文件,因此两者可以在同一台机器上共存。它是一个从上游 Codex 基线构建的非官方发行版,不是加载到官方二进制文件中的扩展。
回退需要 Git 仓库吗?
不需要。在仓库中,子系统将索引作为项目文件路径的一个来源。在非仓库目录中该分区为空,而智能体触碰和最近修改的文件仍然提供覆盖。它永远不需要提交或 stash,也不会写入 Git 状态。
对话格式只是"兼容",还是实际上未改变?
是未改变的。实现没有向 Codex 的对话展开过程中添加任何字段或事件。文件历史存在于单独的 sidecar 文件中,通过现有的回合 ID 作为键。实际的注意事项是覆盖范围而非序列化:官方二进制文件可以读取对话,但不会为其运行的回合生成快照条目。
相同的功能是否跨项目和 Codex 界面工作?
捕获策略基于会话工作区根目录,而不是某个仓库布局,因此相同的子系统可以处理跨项目的仓库和普通目录。这里的用户面向实现是 CLI/TUI。其 Rust 核心和应用服务恢复 API 可被桌面客户端和 IDE 客户端复用,但那些官方界面尚未采用此子系统。
这是一个完整的备份系统吗?
不是。它是一个有界的、会话级别的撤销缓冲区。继续使用 Git 和正常备份来维护持久历史。三个分区增加了有用的覆盖范围,而无需承诺完整树快照,上文的限制部分定义了剩余的缺口。
兼容性与上游路径
该包跟随上游 Codex 版本发布,并与官方 CLI 并排安装。它故意共享正常的 ~/.codex 目录,以便登录、配置和对话历史可以跨版本延续。如果你在一个对话中交替使用可执行文件,请记住只有 codexr 写入文件快照;在启动它的可执行文件中完成追踪的对话,或者如果你偏好完全隔离就使用单独的 CODEX_HOME。
截至 2026 年 8 月 21 日,官方 Codex 贡献指南说明不接收外部代码贡献和拉取请求。它请求的是问题、根因分析和设计讨论。因此这个项目是一个可用的桥接和一个具体的设计实验,而不是上游 PR 将被合并的承诺。
如果你想检视或尝试它:
源码和安装说明
完整文件快照回退 RFC
最快的路径仍然是:
npm install -g codex-rewind
codexr --enable file_snapshots
如果你测试它,我最在乎的反馈不是"撤销听起来有用吗?"而是追踪边界在哪里让你感到意外:哪些真实的智能体造成的文件更改逃脱了三个分区的覆盖,或者哪些被捕获的路径你认为应该留在回退系统之外。
AI 辅助声明:我使用 AI 编程助手来帮助构建和编辑本文。在发布前,我根据项目源码和链接的文档审查了技术声明和代码片段。