| 1 | # Design: Checkpoints & Rewind |
| 2 | |
| 3 | Status: **Phase 1 + 2 implemented** — snapshot store, capture seam, the Esc-Esc / |
| 4 | `/rewind` CLI picker, and the desktop hover-rewind, with the full Claude Code menu: |
| 5 | restore code / conversation / both, fork-from-here, and summarize from / up to |
| 6 | here. Snapshot-based and aligned with Claude Code. An optional git-backed mode is |
| 7 | the remaining (lower-priority) follow-up. Tracks the most requested missing |
| 8 | capability from v1 — an edit safety net / undo. |
| 9 | |
| 10 | This document describes rewind snapshots. For the autonomous-run rule about when |
| 11 | the agent should pause and ask the user, see |
| 12 | [`TASK_CONTRACT.md`](TASK_CONTRACT.md). |
| 13 | |
| 14 | For the read-only per-turn diff and check results built on these snapshots, see |
| 15 | [Turn results](TURN_RESULTS.md). |
| 16 | |
| 17 | ## Goal |
| 18 | |
| 19 | Let a user rewind a session to a previous point and restore **code**, |
| 20 | **conversation**, or **both** — without touching their git history. Aligned with |
| 21 | Claude Code's rewind (Esc-Esc / `/rewind`), driven identically from the CLI and |
| 22 | the desktop. |
| 23 | |
| 24 | ## Mechanism: file snapshots, not git |
| 25 | |
| 26 | Like Claude Code (and v1's `checkpoints.ts`), checkpoints are **file snapshots**, |
| 27 | independent of git: |
| 28 | |
| 29 | - **Zero git pollution** — never commits, stages, or touches `.git/`. Works in a |
| 30 | non-git directory. |
| 31 | - **Tracks only previewable edit-tool changes** — `write_file` / `edit_file` / `multi_edit`. |
| 32 | File moves via `move_file` follow the same workspace permission boundary, but |
| 33 | are not yet represented in checkpoint previews. |
| 34 | `bash` side effects are **not** tracked (no way to know what a shell command |
| 35 | touched), exactly as Claude Code. Risky bash is already permission-gated. |
| 36 | - Full pre-edit content snapshots (simple; storage bounded by retention, below). |
| 37 | |
| 38 | An optional **git-backed mode** (v1's `auto-git-rollback`) is a possible Phase 2 |
| 39 | for users who want git-level safety; it is explicitly out of scope here. |
| 40 | |
| 41 | ## Anchors & capture |
| 42 | |
| 43 | - **One checkpoint per user turn.** A checkpoint opens when a turn starts |
| 44 | (`Controller.Send` / `runTurn`), labelled with the user prompt. |
| 45 | - **Pre-edit snapshot.** In `agent.(*Agent).executeOne`, before running a tool |
| 46 | whose `ReadOnly()` is false and which implements `tool.Previewer`, call |
| 47 | `Preview(args)` → `diff.Change{Path, Kind, OldText}` and record a snapshot of |
| 48 | that file into the active checkpoint. `tool.Previewer` already exists and the |
| 49 | file-writers implement it, so this is one centralized seam — no per-tool code. |
| 50 | - Dedup per path per turn: only the **first** touch is snapshotted (that is the |
| 51 | file's turn-start content). |
| 52 | - `Kind == create` (file did not exist) → store `Content = nil` so a restore |
| 53 | *deletes* it. `modify`/`delete` → store `OldText`. |
| 54 | - `bash` has no `Previewer`, so it is naturally excluded — matching the |
| 55 | "edit-tools only" contract. |
| 56 | |
| 57 | ## Data model |
| 58 | |
| 59 | ```go |
| 60 | type FileSnap struct { |
| 61 | Path string // workspace-relative |
| 62 | Content *string // nil → file did not exist at the anchor (restore deletes it) |
| 63 | } |
| 64 | |
| 65 | type Checkpoint struct { |
| 66 | Turn int // user-message index this anchors (0-based) |
| 67 | Time time.Time |
| 68 | Prompt string // user message text — the picker label |
| 69 | Files []FileSnap // distinct files touched during this turn, turn-start state |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | ## Storage |
| 74 | |
| 75 | - **Sidecar to the session**, under `config.SessionDir()`: `<session-id>.ckpt/`. |
| 76 | It is separate from the message JSONL (`agent.Session.Save`), so the session |
| 77 | format is unchanged. |
| 78 | - **Persists across sessions** — resuming a session re-loads its checkpoints, so |
| 79 | rewind works after a restart (Claude Code parity). |
| 80 | - **Schema v3 layout**: each turn is a directory: |
| 81 | `turns/<turn>/meta.json` plus raw `files/NNNN.before` payloads. New captures do |
| 82 | not duplicate preimages in the content-addressed blob store. v1/v2 JSON and |
| 83 | blobs remain readable for upgrade compatibility; transaction/undo payloads |
| 84 | may still use blobs. Each v3 turn also writes a payload-free v2 compatibility |
| 85 | marker (`turn-<turn>.json`). A previous Reasonix version can therefore keep |
| 86 | turn numbering monotonic after a downgrade, but cannot restore the v3 file |
| 87 | payload represented by that marker. The marker is also the v3 turn's liveness |
| 88 | record: if an older reader truncates the marker, a later upgrade ignores the |
| 89 | leftover directory instead of resurrecting the future turn. |
| 90 | - **Retention**: keep the newest 100 v3 turn directories by default and remove |
| 91 | an expired turn as one directory. Raw v3 preimages also have a soft 1 GiB |
| 92 | budget; the current or transaction-protected turn may temporarily exceed it, |
| 93 | and older whole turns are removed once they are unprotected. Legacy blobs use |
| 94 | the same budget value in their separate compatibility store. Both limits are |
| 95 | configurable via `[checkpoints]` (`retain_turns`, `blob_quota_bytes`); an |
| 96 | omitted or non-positive value keeps the default, so `retain_turns = 0` does |
| 97 | not disable retention. Session cleanup removes the whole sidecar. |
| 98 | |
| 99 | ## Controller API (the one seam both frontends drive) |
| 100 | |
| 101 | Checkpoints live on `control.Controller`, beside `SetPlanMode` / `Compact` / |
| 102 | `NewSession`, so the terminal TUI, the desktop webview, and the HTTP/SSE server |
| 103 | drive rewind identically and none re-implement it. |
| 104 | |
| 105 | ```go |
| 106 | type RewindScope int // Code | Conversation | Both |
| 107 | |
| 108 | func (c *Controller) Checkpoints() []CheckpointMeta |
| 109 | func (c *Controller) PrepareRewind(turn int, scope RewindScope) (RewindPlan, error) |
| 110 | func (c *Controller) CommitRewind(planID string) (RewindResult, error) |
| 111 | func (c *Controller) CommitRewindInPlace(planID string) (RewindResult, error) |
| 112 | func (c *Controller) UndoRewind(transactionID string) (RewindResult, error) |
| 113 | ``` |
| 114 | |
| 115 | - **Code**: for every checkpoint from `turn` to the latest, take the earliest |
| 116 | `FileSnap` per path and restore each file to that content (delete if `nil`) — |
| 117 | i.e. undo all edits made at or after `turn`. Path-escape re-checked against the |
| 118 | live workspace root. |
| 119 | - **Conversation**: fork a `rewind` head of the same session log at the turn |
| 120 | boundary; a format-1 session forks a new session file instead. The previous |
| 121 | chain is never truncated. See [`SESSION_OWNERSHIP.md`](SESSION_OWNERSHIP.md). |
| 122 | - **Both**: fork first, then restore files. A file conflict keeps the new |
| 123 | head and reports `partial=true`. |
| 124 | - `CommitRewind` leaves the controller where it was and returns the new head |
| 125 | (or fork path) in `Branch`; `CommitRewindInPlace` moves the controller onto |
| 126 | the rewound conversation. Desktop tabs and the terminal use the in-place |
| 127 | form, so the same tab or screen shows the rewound transcript. |
| 128 | - `UndoRewind` restores the file after-images. When the controller sits on a |
| 129 | rewind head that received nothing since, it returns to the parent head and |
| 130 | retires the empty rewind head; a continued rewind head stays as a version. |
| 131 | |
| 132 | A `Rewound` event (or reuse of a history-replace event) lets every frontend |
| 133 | re-render uniformly. |
| 134 | |
| 135 | ## CLI UX (aligned with Claude Code) |
| 136 | |
| 137 | - **`Esc Esc`** with an empty composer, or **`/rewind`**, opens a picker listing |
| 138 | each user turn (time + which files it changed). `chat_tui` already tracks the |
| 139 | double-Esc timing. |
| 140 | - Select a turn → sub-menu: **`[code+conversation] [conversation] [code] [cancel]`**. |
| 141 | - On a conversation/both restore, the terminal replays the rewound head in |
| 142 | place and prefills the selected prompt into the composer; the previous chain |
| 143 | stays listed under `/branch`. |
| 144 | |
| 145 | ## Desktop UX (aligned with the VS Code extension) |
| 146 | |
| 147 | - Each user message in the transcript gets a hover **rewind** control → menu: |
| 148 | **rewind code / rewind conversation / both / fork-from-here**. |
| 149 | - It calls the same prepare/commit rewind API over the desktop host protocol; the controller's |
| 150 | event stream pushes the restored state and React re-renders. No rewind logic in |
| 151 | the frontend. |
| 152 | - Conversation rewind and fork-from-here keep the current tab and switch it to |
| 153 | the new head; the previous chain remains under *View versions*. Only an |
| 154 | isolated-worktree fork opens a new tab, because it copies the session into |
| 155 | the new workspace. |
| 156 | |
| 157 | ## Non-goals & edge cases |
| 158 | |
| 159 | - **bash / external side effects** (`rm`, `mv`, DB writes, deploys) are not |
| 160 | tracked — rewind cannot undo them (Claude Code parity). |
| 161 | - **External edits between turns**: restore compares the current existence, |
| 162 | SHA-256, and mode with Reasonix's last after-image. A mismatch is reported as |
| 163 | a conflict and is not overwritten. |
| 164 | - **Deletions**: an edit-tool deletion is restorable (snapshot has the content); a |
| 165 | `bash rm` is not. |
| 166 | - **Large files**: full snapshots, with a 32 MiB per-file capture limit. The |
| 167 | turn-count and soft byte budgets bound retained history; a protected or |
| 168 | current turn may temporarily exceed the byte budget. |
| 169 | |
| 170 | ## Phasing |
| 171 | |
| 172 | 1. **Phase 1**: snapshot store + `executeOne` capture seam + controller |
| 173 | prepare/commit (code/conversation/both) + CLI picker (Esc-Esc + `/rewind`). |
| 174 | 2. **Phase 2**: desktop hover-rewind UI; "fork from here"; "summarize from/up to |
| 175 | here"; optional git-backed mode. |
| 176 | |
| 177 | ## Open questions |
| 178 | |
| 179 | - Snapshot on `/compact` and on `NewSession` boundaries? |
| 180 |