6.5 KiB
Design: Checkpoints & Rewind
Status: Phase 1 + 2 implemented — snapshot store, capture seam, the Esc-Esc /
/rewind CLI picker, and the desktop hover-rewind, with the full Claude Code menu:
restore code / conversation / both, fork-from-here, and summarize from / up to
here. Snapshot-based and aligned with Claude Code. An optional git-backed mode is
the remaining (lower-priority) follow-up. Tracks the most requested missing
capability from v1 — an edit safety net / undo.
This document describes rewind snapshots. For the autonomous-run rule about when
the agent should pause and ask the user, see
TASK_CONTRACT.md.
Goal
Let a user rewind a session to a previous point and restore code,
conversation, or both — without touching their git history. Aligned with
Claude Code's rewind (Esc-Esc / /rewind), driven identically from the CLI and
the desktop.
Mechanism: file snapshots, not git
Like Claude Code (and v1's checkpoints.ts), checkpoints are file snapshots,
independent of git:
- Zero git pollution — never commits, stages, or touches
.git/. Works in a non-git directory. - Tracks only previewable edit-tool changes —
write_file/edit_file/multi_edit. File moves viamove_filefollow the same workspace permission boundary, but are not yet represented in checkpoint previews.bashside effects are not tracked (no way to know what a shell command touched), exactly as Claude Code. Risky bash is already permission-gated. - Full pre-edit content snapshots (simple; storage bounded by retention, below).
An optional git-backed mode (v1's auto-git-rollback) is a possible Phase 2
for users who want git-level safety; it is explicitly out of scope here.
Anchors & capture
- One checkpoint per user turn. A checkpoint opens when a turn starts
(
Controller.Send/runTurn), labelled with the user prompt. - Pre-edit snapshot. In
agent.(*Agent).executeOne, before running a tool whoseReadOnly()is false and which implementstool.Previewer, callPreview(args)→diff.Change{Path, Kind, OldText}and record a snapshot of that file into the active checkpoint.tool.Previeweralready exists and the file-writers implement it, so this is one centralized seam — no per-tool code.- Dedup per path per turn: only the first touch is snapshotted (that is the file's turn-start content).
Kind == create(file did not exist) → storeContent = nilso a restore deletes it.modify/delete→ storeOldText.bashhas noPreviewer, so it is naturally excluded — matching the "edit-tools only" contract.
Data model
type FileSnap struct {
Path string // workspace-relative
Content *string // nil → file did not exist at the anchor (restore deletes it)
}
type Checkpoint struct {
Turn int // user-message index this anchors (0-based)
Time time.Time
Prompt string // user message text — the picker label
Files []FileSnap // distinct files touched during this turn, turn-start state
}
Storage
- Sidecar to the session, under
config.SessionDir():<session-id>.ckpt/with one JSON per checkpoint plus a small index (v1's layout — cheap delete, a corrupt snapshot only loses itself). Kept separate from the message JSONL (agent.Session.Save) so the session format is unchanged. - Persists across sessions — resuming a session re-loads its checkpoints, so rewind works after a restart (Claude Code parity).
- Retention: prune with the session (default ~30 days, configurable), to bound disk from full-content snapshots.
Controller API (the one seam both frontends drive)
Checkpoints live on control.Controller, beside SetPlanMode / Compact /
NewSession, so the terminal TUI, the desktop webview, and the HTTP/SSE server
drive rewind identically and none re-implement it.
type RewindScope int // Code | Conversation | Both
func (c *Controller) Checkpoints() []CheckpointMeta // for the picker
func (c *Controller) Rewind(turn int, scope RewindScope) error
- Code: for every checkpoint from
turnto the latest, take the earliestFileSnapper path and restore each file to that content (delete ifnil) — i.e. undo all edits made at or afterturn. Path-escape re-checked against the live workspace root. - Conversation: truncate
Session.Messagesto just before turnturn's user message, re-Save, and emit the truncated history as events so the frontend re-renders. The turn's prompt is restored into the composer for re-send/edit (Claude Code behavior). - Both: code + conversation.
A Rewound event (or reuse of a history-replace event) lets every frontend
re-render uniformly.
CLI UX (aligned with Claude Code)
Esc Escwith an empty composer, or/rewind, opens a picker listing each user turn (time + which files it changed).chat_tuialready tracks the double-Esc timing.- Select a turn → sub-menu:
[code+conversation] [conversation] [code] [cancel]. - On a conversation/both restore, the selected prompt is prefilled into the composer.
Desktop UX (aligned with the VS Code extension)
- Each user message in the transcript gets a hover rewind control → menu: rewind code / rewind conversation / both / fork-from-here.
- It calls the same
controller.Rewindover the Wails binding; the controller's event stream pushes the restored state and React re-renders. No rewind logic in the frontend.
Non-goals & edge cases
- bash / external side effects (
rm,mv, DB writes, deploys) are not tracked — rewind cannot undo them (Claude Code parity). - External edits between turns: a snapshot holds the file's turn-start content, so restoring overwrites edits made outside reasonix in the meantime.
- Deletions: an edit-tool deletion is restorable (snapshot has the content); a
bash rmis not. - Large files: full snapshots — retention cleanup bounds disk; revisit dedup (content-addressed snapshots) if it becomes a problem.
Phasing
- Phase 1: snapshot store +
executeOnecapture seam +Controller.Rewind(code/conversation/both) + CLI picker (Esc-Esc +/rewind). - Phase 2: desktop hover-rewind UI; "fork from here"; "summarize from/up to here"; optional git-backed mode.
Open questions
- Snapshot on
/compactand onNewSessionboundaries? - Default retention window and whether to expose it in
[checkpoints]config. - Content-addressed dedup vs one-file-per-snapshot from the start.