机制专题 · 12

Rewind / Checkpoint 机制

深挖 /rewind(别名 /checkpoint/undo):它如何把代码和 / 或对话恢复到之前的某个时间点。现行行为以 2.1.233 为准,/rewind 仍在 slash 表。

一句话结论

/rewind 的本质是两个正交维度的恢复对话恢复把消息历史截断回某条 user message 之前,代码恢复用 file history 把磁盘文件回滚到该消息对应的快照。两者由同一个 messageId 锚定,但可以分别执行(恢复代码 / 恢复对话 / 两者都恢复)。文件快照不是按时间存的,而是按 message 存的——所以"回到那条消息" = 对话截断到那 + 文件恢复到那一刻。

相关阅读

上下文管理、压缩与 checkpoint(其中区分了三类 checkpoint)· 启动与运行时(query 主循环、resume)

1. 入口与触发#

入口路径行为
/rewind 命令commands/rewind/index.tsrewind.tslocal 命令,不支持非交互;call() 只调 context.openMessageSelector() 并返回 { type: 'skip' }
键位keybindings(MessageSelector context)handleShowMessageSelector toggle 选择器
消息操作栏 editREPL.tsx messageActionCaps.edit选中某条历史消息"编辑重发",走同一套 rewind 逻辑
headless / SDKcli/print.ts直接调 fileHistoryGetDiffStats + fileHistoryRewind

openMessageSelectorREPL.tsx 里就是 setIsMessageSelectorVisible(true),渲染 <MessageSelector> 组件。

2. MessageSelector:选消息 + 选范围#

components/MessageSelector.tsx

both 时按顺序调 onRestoreCode(文件)→ onRestoreMessage(对话),各自独立 try/catch,分别报错。

3. 对话恢复:rewindConversationTo#

REPL.tsxrewindConversationTo(message) 核心只有一行截断,但连带重置了一堆易 stale 的状态:

setMessages(prev.slice(0, messageIndex))   // 截断到该消息之前
→ setConversationId(randomUUID())          // 新会话 id
→ resetMicrocompactState()                 // 防 stale pinned cache edits 引用被截断的 tool_use_id
→ resetContextCollapse()                   // staged queue / ID map 引用了 stale uuid,全部重置
→ 恢复该 message 的 permissionMode
→ 清 promptSuggestion

restoreMessageSync 进一步把该消息的文本和图片重新填回输入框textForResubmit + pasted images),所以 rewind 同时是"编辑后重发"的入口——你回到那条 prompt,改一改再发。

细节:两条 rewind 路径的渲染时序

MessageSelector 路径用 setImmediate 延迟 rewind,让 "Interrupted" 消息先渲染到 static output,否则它会残留在屏幕顶部。中断自动恢复路径则用同步版,让 React 把 abort 的 setMessages 和 rewind 批成一次渲染,避免闪烁。

4. 代码恢复:fileHistoryRewind#

utils/fileHistory.tsfileHistoryRewind(updater, messageId)

  1. 用 no-op updater 捕获当前 FileHistoryState(rewind 是纯文件系统副作用,不改 state)。
  2. findLast 找到该 messageId 对应的 targetSnapshot
  3. applySnapshot:遍历所有 tracked files:
    • 该版本备份为 null(文件当时不存在)→ unlink 删除现在的文件。
    • 备份有内容 → 若当前文件与备份不同(checkOriginFileChanged)→ restoreBackup(copyFile 覆盖 + chmod 还原权限)。
    • 只动确实变了的文件(基于 stat 的 mode/size/mtime,必要时再比内容)。

5. 文件快照怎么来的(写入侧)#

函数时机作用
fileHistoryMakeSnapshot(messageId)每个 turn(一条 user message)开一个新快照,关联 messageId,回填本轮 tracked 文件备份
fileHistoryTrackEdit(path, messageId)Edit/Write/新建文件执行前把文件当前内容备份成 v1,使"编辑前"状态可回滚

fileHistoryTrackEdit三阶段设计,防 speculative write 和并发 race:

Phase 1: 检查最近快照是否已跟踪该文件(已跟踪则不动 v1,防覆盖)
Phase 2: async createBackup(v1)
Phase 3: commit,再次 re-check(可能有另一个 trackEdit 抢先)

fileHistoryMakeSnapshot 同样分三阶段:capture state(no-op updater)→ async 备份所有 tracked 文件(比对 mtime/size 决定复用旧备份还是建新版本)→ commit 新快照。

6. 备份存储格式#

{configDir}/file-history/{sessionId}/{sha256(filePath)[:16]}@v{version}

7. 预览与无损快速路径#

8. resume / 跨会话迁移#

9. 开关#

10. 关键文件与设计要点#

关注点文件
命令入口commands/rewind/index.tscommands/rewind/rewind.ts
选择器 UI + 恢复选项components/MessageSelector.tsx
对话截断 + 重发screens/REPL.tsxrewindConversationTo / restoreMessageSync
文件快照 / 备份 / 回滚utils/fileHistory.ts
resume 恢复utils/sessionRestore.tshooks/useFileHistorySnapshotInit.ts
headless rewindcli/print.ts

设计要点:

  1. 对话与代码 rewind 解耦,可单独执行,用 messageId 锚定同一时间点。
  2. 文件快照按 message 存而非按时间,天然对齐"回到那条消息"。
  3. 备份用 copyFile + hash 命名 + 版本号 + per-session 目录,resume 用 hard link 迁移,省空间。
  4. rewind 顺带把原 prompt 填回输入框,把"回退"变成"编辑重发"。
  5. trackEdit/makeSnapshot 的三阶段 + re-check 防 speculative/并发 race 污染 v1。
  6. 截断对话时连带重置 microcompact / contextCollapse / conversationId / promptSuggestion,防 stale 引用。
  7. 回退前 dry-run diff 预览 + 无损快速路径,既安全又不啰嗦。