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
| Agent | Multi-file batch? | Validate-before-write? | Rollback on partial failure? | Checkpoint/undo | Git as safety net? |
|---|---|---|---|---|---|
| Codex | Yes (apply_patch) | Yes, full pre-flight | No — partial writes persist | Delta tracking (visibility only) | No |
| Grok Build | No (single-file only) | Yes, all ops before splice | N/A (all-or-nothing in memory) | Anchor-shift recovery suggestions | No |
| Qwen Code | N/A (isolation) | N/A | N/A | Git worktree isolation | Yes — worktrees |
| OpenCode/Kilocode | Yes (apply_patch) | Only pre-flight parse | No — documented gap | Shadow git repo, manual | Yes, but decoupled |
| Pi | Multiple calls only | No | No | Opt-in example (git stash) | No (core) |
| Goose | Multiple calls only | No | No | None | No |
Codex — Validate-Then-Apply, No Rollback Once Writing Starts
Architecture: repos/codex/codex-rs/apply-patch/ crate.
Two-Phase Design
-
Parse phase (
apply-patch/src/parser.rs):parse_patchparses the entire multi-file patch text into aVec<Hunk>(AddFile/DeleteFile/UpdateFile) before anything touches disk. -
Verify phase (
apply-patch/src/invocation.rs):verify_apply_patch_argsperforms 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. -
Apply phase (
apply-patch/src/lib.rs):apply_hunks_to_filesis a plainfor hunk in hunksloop (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:
-
Resolve all ops (lines 182-213): loops over every operation, calling
resolve_op→validate_anchoragainst the pre-edit file content. First failure short-circuits withreturnimmediately — no splicing has happened. -
Check overlaps (lines 215-230): validates no operations conflict before any mutation.
-
Splice (lines 232-306): only after both checks pass does it modify the in-memory
Vec<String>. -
Single write (
edit/mod.rs:401-424): onefs.write_filecall 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:
- Session-ownership check
- Dirty-state check (
hasWorktreeChanges— requires explicitdiscard_changes: true) - 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-treeat 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) doesgit 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_cwddoesfs::create_dir_all+fs::writedirectly (no temp file, no atomic rename)file_edit_with_cwddoesread_to_string→string_replace→fs::writedirectly
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.