# Peri Code: rewind 如何设计消息回退能力

> **[Peri Code](https://github.com/konghayao/peri)** — 用 Rust 写的开源 Coding Agent，兼容 Claude Code 生态。<https://github.com/KonghaYao/peri>

我们在 Peri 上跑了一次消息管线重构——十几层中间件、并发工具调度、ACP 协议桥接，改着改着发现第五六轮的时候走偏了。Agent 已经删掉了关键文件、重写了接口，想把这条错误的路径整个扔掉，回到之前某个正确的节点。

空格 + 退格做不到——已经写了五六轮，改了几十个文件。Ctrl+Z 也做不到——Agent 的每一步操作在文件系统上已经落盘了，编辑器撤销栈里没有。

所以需要 /rewind。但 /rewind 不是 Undo——不是你想象中那一条条退回去的东西。

## 选中一条消息，之后全部截断

rewind 的操作对象是整个消息子图。你选一条历史用户消息，rewind 会截掉它之后的所有消息——AI 的回复、工具调用、工具结果、后续几轮对话，全部移除。最后界面回到那条消息之前的瞬间，就像后面的事情没发生过。

| 对比 | Undo（编辑器的 Ctrl+Z） | Rewind（Peri 的 /rewind） |
|------|------------------------|--------------------------|
| 操作粒度 | 单次文本变更 | 消息子树（含 N 轮对话 + M 次文件操作） |
| 操作范围 | 一个文件 | 对话历史 + 所有被修改/创建的文件 |
| 恢复方式 | 文本 diff 反向 | 消息截断 + 文件逆向变更 + 持久化删除 |
| 后续状态 | 光标留在原地 | 输入框重置，可用旧输入重新开始 |

rewind 做的是多维度的会话析构——消息历史、文件系统、持久存储三个平面的状态必须同步截断，缺一个平面就会出现幽灵状态。

## 文件恢复倒序执行，单条失败跳过

最容易被误解的部分是文件回退。用户选 "Rewind + Revert files" 时，rewind 从被移除的消息段里提取所有成功的 Write/Edit 调用，倒序恢复文件。

倒序这个细节很重要——如果 Agent 先改了 parser.rs 第 1 行，后来又把第 1 行改成了别的内容，正序恢复会先恢复第一次的变更，第二次的变更再覆盖回来，最终得到中间状态。倒序恢复保证最终文件内容和用户选择回退的节点完全一致。

恢复策略是纯尽力模式，不是事务性回滚——

- **Edit 恢复**——`content.find(new_string)` 定位后手动字符串替换，只换首次出现，和 Edit 工具的行为完全对称。如果 new_string 在文件里找不到（文件已经被后续操作改写），跳过这条变更，继续处理下一个。
- **Write 恢复**——删除文件 + `git checkout HEAD -- path` 尝试恢复原始版本。git restore 失败不报错——文件可能根本不在 git 追踪里，删掉就好。
- **恢复失败不阻塞**——任何一条文件操作失败，记录警告、继续处理剩余的文件。即使 10 个文件里有 3 个恢复失败、7 个成功，也比什么都不做强。

rewind 不做事务、不做快照、没有 .peri-snapshot 文件夹。文件恢复是尽力模式，rewind 的定位是工作流方向选择，不是数据恢复。

## 那为什么不直接用 git

这个问题的直觉版本是——git 不就能把文件恢复到任意提交吗，rewind 费劲逐条逆向恢复文件干嘛。

git 回滚的粒度是 commit，rewind 回退的粒度是消息。两者根本不活在同一个坐标系里。

Agent 在两轮对话之间可能操作了几十个文件、改了上百处，但中间没有 commit。你当然可以手动打 commit——但 git 不知道「第四轮对话结束时」对应哪个 commit，它只知道 sha。rewind 知道的恰好是 git 不知道的东西——这条 AI 消息影响了哪些文件、对每个文件做了什么操作。`extract_file_changes` 从被移除的消息段里遍历每条 `tool_use`，拿到的不是目录 diff，是逐条的 Write/Edit 调用参数——文件路径、写入内容、old_string、new_string。这才是 rewind 需要的恢复信息。

还有一层——rewind 和 git 管的不是同一件事。git 管理代码的版本历史——文件状态在任何时刻都能回退到任意 commit。rewind 管理 Agent 工作流的方向——对话树走偏了，砍掉错误枝杈，从正确节点重新长。rewind 不该替 git 做 reset，因为你的代码历史在 git 里是安全的、精确的、随时可以回退的。rewind 只负责把「对话到了哪一步」这件事重置。

倒序恢复里保留 `git checkout HEAD -- path` 是尽力模式的兜底——Write 的文件被删掉后，git 能兜住最好，兜不住用户重发指令重新生成。

## 截断后扫描未配对的工具调用和结果

rewind 实现里有一个看似多此一举的步骤——`validate_tool_pairing`。在消息截断之后、持久化删除之前，检查保留的消息中是否每个 `tool_use` 都有对应的 `tool_result`。

这个检查的来由是 Peri Agent 里一个踩了三次才固化的硬约束——工具调用和工具结果必须成对出现。API 层要求严格配对，少一个 `tool_result`，下轮 LLM 调用直接返回 400。

rewind 截断消息时，你选中的那条用户消息后面可能有一个 AI 消息包含了 `tool_use`，但对应的 `tool_result` 在前面的消息里——因为 `tool_result` 和 `tool_use` 不一定在同一个 AI/Human 消息对里。截断后 `tool_use` 被移除，`tool_result` 留在保留区域，变成了孤儿。 `validate_tool_pairing` 在删除前扫描整个保留区，发现问题至少发 warning，告诉用户这片区域不干净。

孤儿的 tool_result 不会阻碍当前运行，但下一轮 LLM 调用可能直接 400。这个检查不阻塞回退——只告诉你脚下的悬崖在哪。

回头看那次消息管线重构——走偏的第五六轮被 rewind 砍掉，历史消息、文件变更、持久化记录三处同步截断，界面回到第四轮结束时的状态。rewind 的价值不是撤销，是让你在错误路径上停下来换方向的时候，不用从头再走一遍。

---

项目地址：[github.com/konghayao/peri](https://github.com/konghayao/peri)
