diff --git a/docs/advanced-usage/available-tools/apply-patch.md b/docs/advanced-usage/available-tools/apply-patch.md index 9e60c56d..5a1a54c0 100644 --- a/docs/advanced-usage/available-tools/apply-patch.md +++ b/docs/advanced-usage/available-tools/apply-patch.md @@ -108,3 +108,10 @@ The patch format uses custom headers followed by unified diff blocks: - [`apply_diff`](/advanced-usage/available-tools/apply-diff): Use for single-file search-and-replace with fuzzy matching - `apply_patch`: Use for multi-file operations with unified diff format - [`write_to_file`](/advanced-usage/available-tools/write-to-file): Use for creating entire new files + +--- + +## Write safety + +This tool publishes through the guarded write path: the write is checked against the file's observed version, and a rejected write (for example, the file changed after it was read) returns a re-read-then-retry error the agent can act on. See [File-Write Safety](/features/file-write-safety). + diff --git a/docs/advanced-usage/available-tools/edit-file.md b/docs/advanced-usage/available-tools/edit-file.md index bfce915c..a17df325 100644 --- a/docs/advanced-usage/available-tools/edit-file.md +++ b/docs/advanced-usage/available-tools/edit-file.md @@ -89,3 +89,10 @@ When the `edit_file` tool is invoked, it follows this process: - [`apply_diff`](/advanced-usage/available-tools/apply-diff): Use for precise, context-aware edits with fuzzy matching These are different implementations of search-and-replace with varying capabilities. + +--- + +## Write safety + +This tool publishes through the guarded write path: the write is checked against the file's observed version, and a rejected write (for example, the file changed after it was read) returns a re-read-then-retry error the agent can act on. See [File-Write Safety](/features/file-write-safety). + diff --git a/docs/advanced-usage/available-tools/edit.md b/docs/advanced-usage/available-tools/edit.md index 108aaa4d..f09eb4a9 100644 --- a/docs/advanced-usage/available-tools/edit.md +++ b/docs/advanced-usage/available-tools/edit.md @@ -89,3 +89,10 @@ When the `edit` tool is invoked, it follows this process: :::info Deprecated Alias `SearchAndReplaceTool` is a deprecated internal alias for `EditTool`. They are the same tool. ::: + +--- + +## Write safety + +This tool publishes through the guarded write path: the write is checked against the file's observed version, and a rejected write (for example, the file changed after it was read) returns a re-read-then-retry error the agent can act on. See [File-Write Safety](/features/file-write-safety). + diff --git a/docs/advanced-usage/available-tools/write-to-file.md b/docs/advanced-usage/available-tools/write-to-file.md index 10b0be2f..dd2b639a 100644 --- a/docs/advanced-usage/available-tools/write-to-file.md +++ b/docs/advanced-usage/available-tools/write-to-file.md @@ -198,3 +198,10 @@ export function debounce(func, delay) { 18 ``` + +--- + +## Write safety + +This tool publishes through the guarded write path: the write is checked against the file's observed version, and a rejected write (for example, the file changed after it was read) returns a re-read-then-retry error the agent can act on. See [File-Write Safety](/features/file-write-safety). + diff --git a/docs/basic-usage/the-chat-interface.md b/docs/basic-usage/the-chat-interface.md index 05e4f062..ca35c852 100644 --- a/docs/basic-usage/the-chat-interface.md +++ b/docs/basic-usage/the-chat-interface.md @@ -69,6 +69,10 @@ For more productivity tips, check out our [Tips & Tricks](/tips-and-tricks) guid * **Copying Text:** You can copy text from the chat history by selecting it and using the standard copy command (Ctrl/Cmd + C). Some elements, like code blocks, have a dedicated "Copy" button. * **Expanding and Collapsing**: Click on a message to expand or collapse it. +### Change Cards + +When a step changes files, a change card appears in the chat listing each file with its additions and removals. The card offers **Rollback step** (restore every file the step touched) and a per-file rollback button, each confirmed before anything is written. Cards are powered by per-write checkpoints and the per-task change journal — see [File-Write Safety](/features/file-write-safety). + ### Mermaid Diagrams Zoo Code renders Mermaid diagrams directly in chat. Diagram backgrounds, labels, lines, and exported PNGs follow your active IDE color theme, including light and high-contrast themes. If you change themes while a conversation is open, existing diagrams update automatically. diff --git a/docs/features/checkpoints.mdx b/docs/features/checkpoints.mdx index e380d7eb..ed25c141 100644 --- a/docs/features/checkpoints.mdx +++ b/docs/features/checkpoints.mdx @@ -54,6 +54,7 @@ Access checkpoint settings in Zoo Code settings under the "Checkpoints" section: 1. Open Settings by clicking the gear icon → Checkpoints 2. Configure checkpoint behavior: - Check or uncheck the "Enable automatic checkpoints" checkbox + - Check or uncheck "Checkpoint after each file write" (on by default) — takes a snapshot after every successful file write by the agent, which powers the per-step change cards and rollback. See [File-Write Safety](/features/file-write-safety) - Adjust the "Checkpoint initialization timeout" (10-60 seconds, default: 30s) Checkpoint settings in Zoo Code configuration diff --git a/docs/features/file-write-safety.md b/docs/features/file-write-safety.md new file mode 100644 index 00000000..1dc64622 --- /dev/null +++ b/docs/features/file-write-safety.md @@ -0,0 +1,127 @@ +--- +description: How Zoo Code guards every file the agent writes — version-checked writes that fail loudly instead of silently clobbering, per-write checkpoints, per-step change cards, and one-click rollback. +keywords: + - file write safety + - guarded writes + - version guard + - atomic writes + - write rejection + - stale version + - change cards + - rollback + - per-write checkpoints + - change journal +--- + +# File-Write Safety + +When the agent writes a file, Zoo Code checks the on-disk state against what the agent actually saw before the write is published. If the file changed underneath — by you, another agent instance, or a background process — the write is **rejected loudly** with a message that tells the agent exactly what to do (re-read the file, then retry) instead of silently overwriting your changes. + +Every accepted write is also **atomic** (the file either holds the old content or the new one, never a partial write) and is recorded: a checkpoint snapshot per write and a one-line journal entry per change, which power the per-step change cards and the rollback controls in the chat. + +--- + +## How a write is guarded + +Three pieces work together: + +- **Version token** — a fingerprint of the on-disk file state: device, inode, size, and the file's modification and change times (in nanoseconds). Two reads of an unchanged file produce the same token; any change produces a different one. +- **Observation** — reading a file records its version token for the task. A write is only allowed to proceed against a file the agent has observed (read) first. +- **Guarded publish** — the write is re-checked against the observed token at publish time: + + | Write kind | Guard | + |---|---| + | Create a new file | Fails if the file already exists and was not read first | + | Update (overwrite) | Publishes only if the on-disk token still matches the observed one | + | Edit (literal replace / patch) | Requires a prior observation, then publishes only on a token match | + +Writes to the same file are ordered through a per-path queue, so concurrent tool calls cannot interleave half-written states. The actual bytes are published by the atomic write primitive: write to a temporary file in the same directory, then rename over the target — a crash mid-write leaves either the old or the new content, never a truncated file. + +--- + +## When a write is rejected + +A rejected write does not happen silently. The tool returns an error to the agent that names the problem and the fix: + +- File not read yet -- read the file, then retry. — the agent tried to edit a file it never read. It reads the file, then retries the same edit. +- File already exists at (path) and was not read before this write -- read the file first, then retry. — a create-style write targeted an existing file the agent had not read. +- Stale version -- the file changed since you read it (expected (token), current (token)); re-read the file, then retry. — the file was modified after the agent read it. The write is refused and the agent re-reads, so your newer content becomes the base for the retry. +- File was deleted after it was read -- the version recorded at read time (token) no longer exists; re-read the file, then retry. — the file was removed after the read. The agent re-reads (creating a fresh observation) before retrying. + +Real I/O failures (permission denied, disk error) are surfaced as I/O errors, not guard verdicts — the guard only rejects on a state mismatch. + +Because a rejection is a normal tool error, the agent recovers on its own: it re-reads the file, re-bases its edit on the current content, and retries. You do not need to intervene unless you want to. + +--- + +## Per-write checkpoints + +With checkpoints enabled, Zoo Code snapshots the workspace **after every successful agent write**, in addition to the task-start baseline. The snapshots live in the same shadow Git store that powers the existing [Checkpoints](/features/checkpoints) feature — no new storage. + +The behavior is controlled by the **Checkpoint after each file write** setting (on by default) in Settings → Checkpoints: + +- **On** (default): each accepted write produces its own checkpoint, so a change card can roll back to the state *before* that specific write. +- **Off**: checkpoints are only taken at the existing coarser cadence. + +--- + +## Per-step change cards + +After a step in which the agent changed files, a **change card** appears in the chat: + +Change card summary: three files changed this step with per-file add/remove counts and a Rollback step button + +*The summary card lists each changed file with its additions and removals, plus a step-level rollback button.* + +- **Summary** (default): file names with added/removed line counts. +- **Full**: the unified diff for each file, shown inline in the card. Selected with the **Show full diff in change cards** setting: + +Change card full detail with inline unified diffs for each changed file + +The card data comes from the per-task change journal (changes.jsonl), which records one line per written file: the file path, the operation (created or modified), and the checkpoint the write produced. The journal is appended one line at a time; if a line is ever torn, the loader repairs the tail and keeps the readable prefix. + +--- + +## Rollback + +The change card offers two rollback scopes: + +- **Per file** — the rollback button on a file row restores that one file to the content it had at the step's checkpoint. The other files of the step are untouched. +- **Per step** — the **Rollback step** button restores every file the step touched. + +Each action asks for confirmation before anything is written: + +Change card with per-file rollback states: one file rolled back, one failed rollback, and a step in progress + +Rollback reuses the existing checkpoint restore service — the same shadow Git store and the same restore primitive the Checkpoints UI uses — so a rolled-back file is restored exactly to the checkpointed content. Failures are reported per file in the card: a rollback that cannot restore one file says so next to that file instead of failing the whole step. + +Rollback requires checkpoints to be enabled for the task. If they are not, the card shows a message instead of acting. + +--- + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| **Checkpoint after each file write** | On | Takes a checkpoint after every successful agent write (see [Checkpoints](/features/checkpoints)). | +| **Show full diff in change cards** | Off (summary) | Full: show the inline unified diff in change cards. Summary: file list with line counts. | + +Checkpoints settings section showing the per-write checkpoint and change card detail controls + +Both settings are regular persisted settings: they round-trip through the settings export/import and apply to new tasks once saved. + +--- + +## Default write path + +The file-write safety guards apply to every file the agent writes — write-to-file, edit-file, search-replace, and apply-diff all publish through the guarded path. The diff-approval behavior that pairs with it: when the agent edits a file, the change is approved in the chat (the change card above) rather than opening a separate diff editor, so the write happens while your focus stays in the conversation. + +--- + +## Where the data lives + +| Data | Location | +|---|---| +| Checkpoint snapshots (per write + task start) | The task's shadow Git checkpoint store (same as the existing Checkpoints feature) | +| Change journal | changes.jsonl inside the task's storage directory | +| Version tokens | Computed on demand from file statistics; nothing is stored | diff --git a/docs/features/index.md b/docs/features/index.md index 8e0aefb1..af380120 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -32,6 +32,7 @@ Discover the powerful features that make Zoo Code your ultimate AI-powered codin ### Workflow Management - [**Task Todo List**](/features/task-todo-list) - Track progress on complex multi-step tasks - [**Checkpoints**](/features/checkpoints) - Save and restore conversation states +- [**File-Write Safety**](/features/file-write-safety) - Version-guarded writes, per-write checkpoints, change cards, and rollback - [**Boomerang Tasks**](/features/boomerang-tasks) - Reusable task templates - [**Custom Modes**](/features/custom-modes) - Create specialized AI assistants for specific workflows diff --git a/sidebars.ts b/sidebars.ts index 33c5b5cd..92660f71 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -27,6 +27,7 @@ const sidebars: SidebarsConfig = { "features/auto-approving-actions", "features/boomerang-tasks", "features/checkpoints", + "features/file-write-safety", "features/code-actions", "features/codebase-indexing", "features/custom-instructions", diff --git a/static/img/file-write-safety/change-card-full.png b/static/img/file-write-safety/change-card-full.png new file mode 100644 index 00000000..825fa0e3 Binary files /dev/null and b/static/img/file-write-safety/change-card-full.png differ diff --git a/static/img/file-write-safety/change-card-summary.png b/static/img/file-write-safety/change-card-summary.png new file mode 100644 index 00000000..3064ec92 Binary files /dev/null and b/static/img/file-write-safety/change-card-summary.png differ diff --git a/static/img/file-write-safety/rollback-state.png b/static/img/file-write-safety/rollback-state.png new file mode 100644 index 00000000..4bfeea25 Binary files /dev/null and b/static/img/file-write-safety/rollback-state.png differ diff --git a/static/img/file-write-safety/settings-checkpoints.png b/static/img/file-write-safety/settings-checkpoints.png new file mode 100644 index 00000000..83d309db Binary files /dev/null and b/static/img/file-write-safety/settings-checkpoints.png differ