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.ts、rewind.ts | local 命令,不支持非交互;call() 只调 context.openMessageSelector() 并返回 { type: 'skip' } |
| 键位 | keybindings(MessageSelector context) | handleShowMessageSelector toggle 选择器 |
| 消息操作栏 edit | REPL.tsx messageActionCaps.edit | 选中某条历史消息"编辑重发",走同一套 rewind 逻辑 |
| headless / SDK | cli/print.ts | 直接调 fileHistoryGetDiffStats + fileHistoryRewind |
openMessageSelector 在 REPL.tsx 里就是 setIsMessageSelectorVisible(true),渲染 <MessageSelector> 组件。
2. MessageSelector:选消息 + 选范围#
components/MessageSelector.tsx:
- 列出所有可选 user message(
selectableUserMessagesFilter),外加一条"当前 prompt"的虚拟消息。 - 选中一条后,根据该 messageId 是否有文件快照(
fileHistoryCanRestore)给出恢复选项:both(默认):恢复代码 + 对话conversation:只恢复对话code:只恢复代码summarize/summarize_up_to:从这里 / 到这里做部分压缩(partial compact,不是 rewind)nevermind:取消
- 选消息时会先用
fileHistoryGetDiffStats(dry-run)算出"回退会改几个文件、+/- 多少行",显示给用户确认。
both 时按顺序调 onRestoreCode(文件)→ onRestoreMessage(对话),各自独立 try/catch,分别报错。
3. 对话恢复:rewindConversationTo#
REPL.tsx 的 rewindConversationTo(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,改一改再发。
MessageSelector 路径用 setImmediate 延迟 rewind,让 "Interrupted" 消息先渲染到 static output,否则它会残留在屏幕顶部。中断自动恢复路径则用同步版,让 React 把 abort 的 setMessages 和 rewind 批成一次渲染,避免闪烁。
4. 代码恢复:fileHistoryRewind#
utils/fileHistory.ts 的 fileHistoryRewind(updater, messageId):
- 用 no-op updater 捕获当前
FileHistoryState(rewind 是纯文件系统副作用,不改 state)。 findLast找到该 messageId 对应的targetSnapshot。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}
- 用
copyFile而不是readFileSync + writeFileSync——避免把大文件读进 JS heap 导致 OOM。 chmod保留原文件权限。- 路径在 state 里用相对路径(
maybeShortenFilePath)做 key,减少 session storage 占用。 MAX_SNAPSHOTS = 100,超出后 LRU 丢最老;snapshotSequence单调递增计数器即使快照被淘汰也不回退,给useGitDiffStats当活动信号。null备份是一个显式的"该版本文件不存在"标记,使 rewind 能"删除一个回退点之后才创建的文件"。
7. 预览与无损快速路径#
fileHistoryGetDiffStats(dry-run):算回退会改哪些文件、insertions/deletions,用diffLines,供 UI 显示。fileHistoryHasAnyChanges:轻量 bool 版,早退,不算行数,用于"要不要弹确认框"的判断。- 无损直接 rewind:消息操作栏 edit 时,若
noFileChanges(回退不改任何文件)且onlySynthetic(该消息后只有合成消息),直接 rewind 不弹确认;否则才打开 MessageSelector 确认。
8. resume / 跨会话迁移#
- 快照记进 transcript(
recordFileHistorySnapshot);resume 时fileHistoryRestoreStateFromLog重建FileHistoryState(含 abs→rel 路径迁移),useFileHistorySnapshotInit在 mount 时一次性恢复。 copyFileHistoryForResume:换 session id 后,用 hard link(失败 fallbackcopyFile)把旧 session 的备份文件迁到新 session 目录,这样 resume 后仍能 rewind。EEXIST 跳过,ENOENT 报错。restoreWorktreeForResume/exitRestoredWorktree(sessionRestore.ts):resume 时还会处理 worktree 工作目录的恢复与退出。
9. 开关#
- settings
fileCheckpointingEnabled(默认开)。 CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING关闭。- 非交互 / SDK 默认关,需
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING开启。
10. 关键文件与设计要点#
| 关注点 | 文件 |
|---|---|
| 命令入口 | commands/rewind/index.ts、commands/rewind/rewind.ts |
| 选择器 UI + 恢复选项 | components/MessageSelector.tsx |
| 对话截断 + 重发 | screens/REPL.tsx(rewindConversationTo / restoreMessageSync) |
| 文件快照 / 备份 / 回滚 | utils/fileHistory.ts |
| resume 恢复 | utils/sessionRestore.ts、hooks/useFileHistorySnapshotInit.ts |
| headless rewind | cli/print.ts |
设计要点:
- 对话与代码 rewind 解耦,可单独执行,用 messageId 锚定同一时间点。
- 文件快照按 message 存而非按时间,天然对齐"回到那条消息"。
- 备份用 copyFile + hash 命名 + 版本号 + per-session 目录,resume 用 hard link 迁移,省空间。
- rewind 顺带把原 prompt 填回输入框,把"回退"变成"编辑重发"。
- trackEdit/makeSnapshot 的三阶段 + re-check 防 speculative/并发 race 污染 v1。
- 截断对话时连带重置 microcompact / contextCollapse / conversationId / promptSuggestion,防 stale 引用。
- 回退前 dry-run diff 预览 + 无损快速路径,既安全又不啰嗦。