Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-File Atomicity

How agents handle changes that span multiple files — validation, rollback, and error recovery.

The Core Finding

Validation-before-write is common, but true transactional rollback of already-written files is rare to nonexistent.

Most agents either:

  • (a) Validate everything upfront so most failures produce zero side effects, or
  • (b) Apply changes sequentially with no rollback, leaving partial state on disk if something fails partway through

Git-based safety nets exist in several agents, but they are almost universally manual, coarse-grained undo/checkpoint features for the user, not automatic transactional rollback tied to a single tool call.

Cross-Agent Comparison

AgentMulti-file batch?Validate-before-write?Rollback on partial failure?Checkpoint/undoGit as safety net?
CodexYes (apply_patch)Yes, full pre-flightNo — partial writes persistDelta tracking (visibility only)No
Grok BuildNo (single-file only)Yes, all ops before spliceN/A (all-or-nothing in memory)Anchor-shift recovery suggestionsNo
Qwen CodeN/A (isolation)N/AN/AGit worktree isolationYes — worktrees
OpenCode/KilocodeYes (apply_patch)Only pre-flight parseNo — documented gapShadow git repo, manualYes, but decoupled
PiMultiple calls onlyNoNoOpt-in example (git stash)No (core)
GooseMultiple calls onlyNoNoNoneNo

Codex — Validate-Then-Apply, No Rollback Once Writing Starts

Architecture: repos/codex/codex-rs/apply-patch/ crate.

Two-Phase Design

  1. Parse phase (apply-patch/src/parser.rs): parse_patch parses the entire multi-file patch text into a Vec<Hunk> (AddFile/DeleteFile/UpdateFile) before anything touches disk.

  2. Verify phase (apply-patch/src/invocation.rs): verify_apply_patch_args performs a full pre-flight pass over every hunk — for updates it computes the actual diff via context-line matching; for deletes/updates it confirms the file exists. This must succeed for the entire patch before the runtime is ever invoked.

  3. Apply phase (apply-patch/src/lib.rs): apply_hunks_to_files is a plain for hunk in hunks loop (lines 390–560) that writes/deletes files one at a time in patch order.

Atomicity Guarantees

Before writing starts: Strong. Test apply_patch_cli_verification_failure_has_no_side_effects (core/tests/suite/apply_patch_cli.rs:965) proves a patch with a valid Add File and an invalid Update File fails entirely — created.txt is never written.

After writing starts: None. Test test_failed_move_returns_committed_destination_delta (apply-patch/src/lib.rs:1118) shows a failed move leaving the destination written and source untouched — an inconsistent state. apply_patch_aggregates_diff_preserves_success_after_failure confirms that in a two-call sequence, the first file’s change persists after the second call fails.

Recovery Mechanism

Instead of rolling back, Codex tracks what was committed via AppliedPatchDelta/AppliedPatchChange (lib.rs:182–273), including an exact: bool flag set false when write side effects are uncertain (e.g., partial write before ENOSPC). This delta surfaces through TurnDiffTracker for UI/model visibility — not for automatic reversal.


Grok Build — Atomic Single-File Batches via In-Memory Computation

Architecture: repos/grok-build/crates/codegen/xai-grok-tools/src/implementations/grok_build_hashline/

Scope Limitation

HashlineEditInput (edit/types.rs:12) has a single file_path field plus a Vec<HashlineOp>. There is no cross-file batch construct — atomicity is per-file only.

How “Reject All” Works

apply_edits (edit/apply.rs:149-306) operates in strict phases:

  1. Resolve all ops (lines 182-213): loops over every operation, calling resolve_op → validate_anchor against the pre-edit file content. First failure short-circuits with return immediately — no splicing has happened.

  2. Check overlaps (lines 215-230): validates no operations conflict before any mutation.

  3. Splice (lines 232-306): only after both checks pass does it modify the in-memory Vec<String>.

  4. Single write (edit/mod.rs:401-424): one fs.write_file call of the fully-computed new content.

Error message on stale anchor: “Edit 2/2 (replace): … Because this anchor failed validation, none of the edits were applied. Retry all 2 edits with fresh anchors.”

Why This Is Truly Atomic

The file is never touched until the entire new content is computed in memory. A partial write is structurally impossible because there’s only one disk write of a complete string. If that write itself fails, the original file is untouched.

Anchor-Shift Recovery

When an anchor is stale, scheme.find_shifted attempts fuzzy recovery within DEFAULT_SEARCH_RADIUS, returning a suggested fresh anchor so the model can retry immediately without re-reading the file.


Qwen Code — Isolation via Git Worktrees

Architecture: repos/qwen-code/packages/core/src/services/gitWorktreeService.ts + packages/core/src/tools/enter-worktree.ts / exit-worktree.ts

Design Philosophy

Qwen Code sidesteps multi-file atomicity by isolating parallel agents into separate git worktrees. Each worktree is a fully separate checkout — an agent’s in-progress edit set never leaks into the main tree or other agents.

Create/Enter

createUserWorktree() (gitWorktreeService.ts:1534-1626) runs git worktree add -b <branch> <path> <base>. The base ref is always the current session’s checked-out branch. Refuses nested worktree creation and writes a session-ownership marker file.

Exit/Teardown

ExitWorktreeTool has keep/remove semantics with three safety gates:

  1. Session-ownership check
  2. Dirty-state check (hasWorktreeChanges — requires explicit discard_changes: true)
  3. Unconditional unmerged-commits check (no override — committed work can never be discarded)

Merge-Back

applyWorktreeChanges() (gitWorktreeService.ts:850-909) diffs from a baseline commit and applies via git apply (optionally --3way). In ArenaManager (multi-model competition), the user picks one winner — no automated cross-worktree conflict resolution.

Atomicity Verdict

Isolation, not atomicity. The merge-back is a single git apply whose failure surfaces as an error with no automatic resolution.


OpenCode / Kilocode — Sequential Apply-Patch + Decoupled Shadow Git

Multi-File Batch Tool

Two apply_patch implementations (both Codex-style syntax):

  • packages/opencode/src/tool/apply_patch.ts (legacy)
  • packages/core/src/tool/apply-patch.ts (v2)

The v2 code’s own doc string admits the gap (line 72): “Operations apply sequentially; if a later operation fails, earlier operations remain applied and the failure reports them explicitly. Moves and atomic rollback are not supported yet.”

Proof of No Rollback

packages/core/test/tool-apply-patch.test.ts:374 (“preserves a later commit defect after earlier sequential applications”): deleting first.txt then second.txt, where the second delete is forced to fail — first.txt stays deleted, second.txt stays intact.

Pre-write validation failures (parse/pre-check errors) do leave zero side effects (packages/opencode/test/tool/apply_patch.test.ts:372).

Shadow Git Checkpoint System

packages/opencode/src/snapshot/index.ts implements a bare shadow git repository (~/.local/share/opencode/snapshot/<project-id>/<hash>/, sharing objects with the real .git via alternates):

  • Runs git write-tree at every LLM step boundary (step-start/step-finish)
  • Stores tree hashes plus per-step patch metadata as message parts
  • Revert (session/revert.ts:70-73) does git checkout <hash> -- <file> per affected file
  • Operates at user-message granularity — not per tool call

Kilocode’s docs confirm this deliberately replaced the legacy “shadow git intercepting every tool call” design for performance.

Atomicity Verdict

No atomic multi-file transaction. The checkpoint system is a coarse, manual, message-level undo/redo feature — architecturally decoupled from any single tool call’s writes.


Pi — No Core Rollback; Opt-In Example Only

Core Tools

packages/coding-agent/src/core/tools/write.ts / edit.ts call fs.writeFile/fs.readFile directly — no backup, temp-file, or undo logic.

Concurrency Control

Per-path serialization via file-mutation-queue.ts (withFileMutationQueue) prevents concurrent writes to the same file but provides no cross-file atomicity.

Error Handling in the Loop

packages/agent/src/agent-loop.ts runs tool calls sequentially or in parallel per turn (executeToolCallsSequential/executeToolCallsParallel, lines 433/489), wrapping each in try/catch — a failure becomes an isError: true result fed back to the model. The loop never unwinds prior successful edits.

Opt-In Git Checkpoint

Only exists as a sample extension: packages/coding-agent/examples/extensions/git-checkpoint.ts runs git stash create on turn_start and optionally git stash apply on session_before_fork. Opt-in, requires workspace to be a git repo.


Goose — Independent Immediate Writes, No Rollback

Architecture

The built-in “developer” platform extension (repos/goose/crates/goose/src/agents/platform_extensions/developer/):

  • edit.rs: file_write_with_cwd does fs::create_dir_all + fs::write directly (no temp file, no atomic rename)
  • file_edit_with_cwd does read_to_string → string_replace → fs::write directly

Multi-File Changes

Come from the model issuing multiple sequential tool calls per turn. The dispatch loop (agents/agent.rs, reply_internal) streams results back individually and continues regardless of errors — a failed edit on file 2 leaves file 1’s change on disk.

Hooks (Not Rollback)

A hooks framework (crates/goose/src/hooks/mod.rs) exposes PreToolUse/PostToolUse/PostToolUseFailure/AfterFileEdit events for user-configured shell commands — extension points for logging/notification, not built-in rollback.


Design Patterns

1. Validate-Before-Write is the Primary Defense

Codex and OpenCode/Kilocode both prove that a full pre-flight validation pass catches most failures (context mismatch, missing file, parse error) with zero disk side effects. This is the most cost-effective pattern — it handles the common case without the complexity of rollback.

2. In-Memory Computation Achieves True Atomicity (at Single-File Scope)

Grok Build’s hashline approach — compute the entire new file in memory, then do one write — is the only design that provides true all-or-nothing semantics. The tradeoff is limiting the batch to one file.

3. Isolation > Transactions for Multi-File

Qwen Code’s worktree approach shows a different philosophy: rather than making multi-file edits atomic, isolate them in a throwaway branch. If the work is bad, discard the worktree. If it’s good, merge it. This maps naturally to how humans use git branches.

4. The “Documented Gap” Pattern

Both Codex and OpenCode acknowledge the lack of rollback explicitly — Codex via its AppliedPatchDelta tracking (visibility without reversal), OpenCode via its code comment. This suggests the gap is a conscious tradeoff, not an oversight: true multi-file transactions would require either filesystem journaling or a git-based wrapper around every tool call, both expensive.

5. Model-as-Recovery-Agent

In Pi and Goose, the recovery strategy is simply: report the error to the model and let it fix the mess. This works because the model can read the current state, understand what went wrong, and issue corrective edits — turning the LLM itself into the “transaction manager” at a higher level of abstraction.