Skip to content

[ENHANCEMENT] File-write safety series (upstream epic #1375): version-guarded atomic writes + per-step visibility/rollback — PR series plan #33

Description

@easonLiangWorldedtech

| TRIAL | feat/fws-trial-all | open — #1413 (trial build of all 15; head 4fc14c4 = 11 merge commits + addenda 6a0feb1 + 376e013 (spec composition, apply_diff self-observation) + 178e6f4 (CodeRabbit round 3: apply_patch observation, mcp merge array guard + spec-mock parity, safeWriteJson doUnmock, webview writeDelayMs shared default, ChangeCard no-files step + grow, CheckpointSettings sibling, Catalan) + d2239ce (restore safety: rev-parse checkpoint verification + realpath symlink containment) + 4fc14c4 (CodeRabbit round 4: McpHub.spec unknown-safe error.code guard, exported createInitialExtensionState + pre-hydration defaults test); all CodeRabbit findings replied — cross-process TOCTOU scoped out and tracked as upstream follow-up Zoo-Code-Org#1414 with epic risk wording proposed; all 15 component PR heads verified mergeable into upstream/main; CI on 4fc14c4 GREEN (ubuntu unit + e2e-mock + codecov/patch + compile + VSIX build all success); PR Zoo-Code-Org#1413 body updated to the green run 33134141568 (artifact 9671394094, inner vsix sha256 1cbe1793...); all CodeRabbit findings confirmed incl. the round-4 pair (01:59-02:02 UTC); the 8 component PRs re-pushed with the propagated fixes (round-10 notes on each row) and all 8 new heads merge-checked clean against upstream/main; round 11: head 83d83a3 = previous + addendum 6 (re-merge of the updated Zoo-Code-Org#1411 94fea2f + Zoo-Code-Org#1412 c48522c after the trial-review user feedback: apply_diff per-write checkpoint + change card; change-card open-in-editor control incl. the CodeRabbit a11y native-button fix; CI GREEN); round 12: head d6d08e8 = 83d83a3 + addendum 7 (re-merge of Zoo-Code-Org#1411 341a9c2 + Zoo-Code-Org#1412 502f8ca after the CodeRabbit round-11 fixes; CI GREEN run 33161152644 — inner vsix sha256 016e930f…, PR body on that green run; no visible UI change) + addendum 8 (re-merge of Zoo-Code-Org#1411 67d8525 — the trial-review audit found the edit and search_replace tools wrote with no per-write checkpoint / journal entry / change card; both now wired with the same checkpointSave call site as the other four write tools + focused 8-test spec; CI running); fork PR #34 (→ trial) still open, CI 15/15 green, re-verified mergeable against the new trial tip d6d08e8; round 13 (2026-08-29): the CodeRabbit review round was fixed in the introducing PRs — Zoo-Code-Org#141039afe1b (a card whose file was re-written in a later step is now refused instead of overwriting the newer write, and an unreadable journal fails loudly instead of a false no-op), Zoo-Code-Org#1411c202745 + 8c5c53b (typed settings test doubles + nl changeCardDetail sentence), Zoo-Code-Org#1412a0693e9 + ea90ea8 (correlated webview failure results, localized no-task results, focusable change-card error states, openFile path labels, es/hi/it/ko locale corrections, schema-invalid spec coverage); all 18 CodeRabbit findings replied (15 original + 3 round-2 on Zoo-Code-Org#1412) and CR confirmed each fix; all four heads merge-checked clean vs upstream/main (efc30cf) and ubuntu CI green (Zoo-Code-Org#1410 6/6, Zoo-Code-Org#1411 6/6 — the round-1 mocked-E2E failure was a history-write/restart race flake, green on re-run, Zoo-Code-Org#1412 8/8); trial re-merged via addendum 9 (779cb4b, empty CI-retry commit for the flake) + addendum 10 (22a3f33, 46 files +496/-99) — CI GREEN run 33257633298 (inner vsix sha256 aaf90d5e…), PR Zoo-Code-Org#1413 body updated to that green run (artifact 9716305621); fork PR #34 re-verified MERGEABLE/CLEAN against the new trial tip 22a3f33; round 14 (2026-08-30): upstream/main moved efc30cf147147c (5 commits: Zoo-Code-Org#1434 the subtask-approval mocked-E2E timeout fix — the exact flake that failed Zoo-Code-Org#1411's round-1 run, Zoo-Code-Org#1426 Extension Host visual regression rework incl. new electron visual suite + playwright harness restructure, Zoo-Code-Org#1445 label clipping, Zoo-Code-Org#1446 typed host message capture, Zoo-Code-Org#1449 partial-ask e2e ignore); all 16 series heads re-verified merge-tree CLEAN against the new tip 147147c; required checks per the main ruleset = 6 (check-translations, platform-unit-test ubuntu + windows, compile, knip, e2e-mock) — all green on every head; the org is rolling out a new PR review gate (Zoo-Code-Org#1437: required CI → CodeRabbit current-head review → human maintainer approval, SQUASH merge queue ALLGREEN) — its 'PR review gate' status is advisory for fork PRs (hard enforcement = native required checks + review protections) and no code change is needed for it; Zoo-Code-Org#1412 carried a stale CodeRabbit CHANGES_REQUESTED (all findings fixed + individually confirmed by CR) so it was labeled awaiting-author — /approve requested on Zoo-Code-Org#1412 and /review requested on Zoo-Code-Org#1411 + Zoo-Code-Org#1413 at 03:49Z but CodeRabbit picked up no manual command within 30 min (the new gate flow owns CR triggering; the org CR config may restrict fork users), so empty commits were pushed to re-trigger the native synchronize flow: Zoo-Code-Org#1411 8c5c53b0d31ea0, Zoo-Code-Org#1412 ea90ea863dcd24 (the push also dismisses the two stale CodeRabbit CHANGES_REQUESTED reviews per the ruleset dismiss_stale_reviews_on_push), Zoo-Code-Org#1413 22a3f335cb1d96 (addendum 11); zero code change (empty commits), all three re-verified merge-tree CLEAN against main 147147c; NOTE: every one of the 16 series PRs is currently mergeable=true / mergeStateStatus=blocked — the block is the ruleset requiring 1 approving review + code-owner review, i.e. awaiting maintainer review, not CI; CI (6 required checks) is green on all 16 heads; the only remaining automation work is CodeRabbit current-head reviews on the three re-pushed PRs (watching), after which the series is fully at the human-approval stage; round 15 (2026-08-30, after inspecting Zoo-Code-Org#1440 which cleared the bot stage): the gate flow per Zoo-Code-Org#1440's process comment = (1) required CI green, (2) workflow starts CodeRabbit automatically, (3) CodeRabbit APPROVES the latest commit (native review state — COMMENTED/acked findings do NOT count), (4) human maintainer approval; Zoo-Code-Org#1440 cleared step 3 with a clean re-review (zero new comments → APPROVED in 2 min) and sits at awaiting-maintainer; our 16 PRs were all at the gate step "Wait for CodeRabbit to approve the latest commit" (CR states were COMMENTED), so all 16 got the same empty-commit re-trigger: 1380→046b78cad, 1381→309ffd644, 1382→b67e57312, 1383→2a42f886f, 1384→d03877438, 1394→a00eef8e0, 1395→7fd49bcea, 1403→cb1360613, 1404→abc10b43e, 1405→be894d9e0, 1406→7b83143cf, 1408→e96df628a, 1410→87471b558 (plus the earlier 1411→0d31ea062, 1412→63dcd24e2, 1413→5cb1d9674); zero code change in all 16 (empty commits), all 16 re-verified merge-tree CLEAN vs main 147147c, all 16 bodies carry a review-gate re-trigger note (code heads unchanged); the "PR review gate" status stays pending by design until a maintainer approves (advisory — the hard gates are the 6 required checks [all green] + 1 approving review + code-owner); watching CI + CR approvals on the 16 new heads; round 16 (2026-08-30): gate-mechanism forensics + @coderabbitai canary result + 1412 CI clear. (a) The review gate (guide comment + "Zoo Code / PR review gate" status + awaiting-coderabbit/awaiting-maintainer labels) is produced by the NEW label-pr-review-state.yml which exists ONLY on Zoo-Code-Org#1437 gate branch feature/human-pr-review-gate-0shw6cv6mcu (main still runs the old label-only reconciler, last touched 07-13); it executes when zoomote[bot] workflow_dispatches it from that branch — dispatch run 33280010451 (08-29 23:02Z) batch-posted Zoo-Code-Org#1440 plus 13 of our guide comments in a single run. Zoo-Code-Org#1440 bot pass, end to end: real-diff push (18:10Z) → CodeRabbit native APPROVED review on the exact head commit within ~2 min (zero findings) → workflow phase "maintainer" (awaiting-maintainer label + gate step "A human maintainer must now review and approve it"); the gate status is advisory — the hard gates are the 6 required CI checks + 1 approving review + code-owner review (re-verified against ruleset 15494382 "Protect Main", which also has a merge_queue rule with ALLGREEN grouping). (b) CANARY NEGATIVE: @coderabbitai review mention on Zoo-Code-Org#1381 (05:58Z) received a CodeRabbit reply within 7 seconds: "Action not completed — No files to review. CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused." → no review object can be created on an empty-commit head; consistent with the 90-min watcher showing zero fresh CR reviews on all 16 heads → the mention path cannot clear the CR stage; no rollout performed. (c) 1412 addendum 12 (empty commit ab281f5) cleared the flaky ubuntu unit run (the vitest EnvironmentTeardownError x9 was an infra false positive, no assertion failures) → 1412 now 6/6 required green and stable. ALL 16 PRs now: 6/6 required checks green, mergeable, zero mechanical blockers; the only remaining gates are the two human rules (1 approving review + code-owner review), plus — for the merge-queue path only — the advisory extension-host-visual on the 6 B1-stack PRs, whose root cause (B1 task-start baseline checkpoint → SeeNewChangesButtons rendering below the completion row against the pre-FWS Zoo-Code-Org#1426 baseline; deterministic 432-px delta per Playwright / 973 px in the raw PNG diff; pixel-identical across 1404/1406/1410/1411/1412/1413) is documented in those PR bodies for maintainer disposition. The CR stage clears automatically if Zoo-Code-Org#1437 merges (its .coderabbit.yaml gates auto-review on the coderabbit-review-active label all 16 carry); otherwise the path to merge is direct maintainer approval (the gate comment is advisory). |# [ENHANCEMENT] File-write safety series (upstream epic Zoo-Code-Org#1375): version-guarded atomic writes + per-step visibility/rollback — PR series plan

PR series plan + execution tracker (DTE-style: one independently mergeable PR per phase).
Story of record: upstream epic Zoo-Code-Org/Zoo-Code#1375.
Phase 0 PRs (already open): #1380, #1381, #1382.

1. Goal

Make agent file writes fast, loud-failing, self-healing, atomic, and auditable — in that order.

  • Safety: no silent corruption (races, truncated tool calls, cross-instance state writes); stale writes fail loudly with a remediation the model can act on.
  • Speed: zero artificial latency on the default write path.
  • Visibility: every agent write is checkpointed, journaled, and shown as a per-step change card; rollback per file or per step. Explicit non-goals (per epic): shell-tool writes, workspace lockfiles, diff-editor approval UX replacement, worktrees as the task-start mechanism.

2. Series overview

PR Epic item Scope Deps Est. Status
Phase 0a A5 — Zoo-Code-Org#1371 Atomic mcp_settings.json stub creation (safeWriteJson + merge under advisory lock) done open (#1380)
Phase 0b Speed DEFAULT_WRITE_DELAY_MS 0 + remove delay(300) done open (#1381)
Phase 0c A5 — Zoo-Code-Org#1021 saveClineMessages abandoned guard done open (#1382)
S1 A1 Version token (pure function + unit tests) 0.5d OPEN (#1383)
S2 A2 Per-task observation registry (ReadFileTool populates) S1 1d OPEN (#1394)
S3 A4 Atomic publish: safeWriteText (staging + fsync + rename / Windows ReplaceFile+DACL) 2d OPEN (#1395)
S4a A3 Guarded write core: CAS (createIfAbsent / replaceIfVersion) + per-path FIFO chain S1+S2+S3 2d OPEN (#1405, tracking #1399); round 10: head 56ce4bf (CodeRabbit fixes from trial addendum: safeWriteJson cleanup now doUnmock + resetModules; CI green)
S4b A3 Guarded write tool wiring (WriteToFile / EditFile / SearchReplace / ApplyPatch / ApplyDiff) S4a 1d OPEN (#1408, tracking #1400); round 10: head 88c9352 (CodeRabbit fixes from trial addendum: apply_patch hunk read now records the S2 observation for the guarded publish; CI green)
S5 A5 — Zoo-Code-Org#920 Cross-instance updateTaskHistory regression test — (track Zoo-Code-Org#1319) 1d planned
S6 A6 — Zoo-Code-Org#1221 Truncated tool-call parser fix (no partial-args reuse) stacked on Zoo-Code-Org#1066 1d blocked
L1 Speed Async post-save diagnostics (follow-up event, not awaited) S3 1d OPEN (#1403, tracking #1396)
L2 Speed Chat-diff (PREVENT_FOCUS_DISRUPTION) as default approval path 1d OPEN (#1384)
B1 B1 Per-write checkpoint (extend shadow git; O(1) task-start baseline) S3 2d OPEN (#1404, tracking #1397); round 10: head b98f159 (CodeRabbit fixes from trial addendum: exported createInitialExtensionState + pre-hydration perWriteCheckpoints test; CI green)
B2 B2 Per-task JSONL change journal (torn-tail repair) B1 1d OPEN (#1406, tracking #1398)
B3a B3 Per-step change cards + changeCardDetail setting (extension host; split from the planned B3a by the 1000-line cap) B1+B2 2d OPEN (#1411, tracking #1401); round 10: head 9b0787a (CodeRabbit fixes from trial addendum: exported createInitialExtensionState + pre-hydration changeCardDetail test; CI green; round 11: head 94fea2f (user feedback from the trial review: apply_diff writes now record the per-write checkpoint and emit the per-step change card, parity with write_to_file / edit_file / apply_patch; new 4-test spec; CI GREEN); round 12: head 67d8525 (341a9c2 + trial-review audit fix: the edit and search_replace tools — exact string replacement, exposed by default — now also record the per-write checkpoint and emit the per-step change card, parity with all six write tools; focused 8-test spec; CI running); round 13: heads c202745 + 8c5c53b (CodeRabbit review fixes: Slider test double typed narrow instead of any + nl changeCardDetail.description full sentence; VSCodeCheckbox/VSCodeLink settings doubles fully typed; tsc 0, eslint 0, 5/5 spec; CI GREEN 6/6 — the round-1 mocked-E2E failure was a history-write/restart race flake, green on re-run; merge-tree clean)
B3c B3 Per-file/per-step rollback service (extension host; split from B3a) B3a 1d OPEN (#1410, tracking #1409); round 10: head d64389c (CodeRabbit fixes from trial addendum: checkpoint availability verification + symlinked-ancestor realpath containment; CI green); round 13: head 39afe1b (CodeRabbit review fixes on the Zoo-Code-Org#1435 semantic head: stale-card rollback refused via latest-entry checkpointId comparison instead of overwriting the newer write; journal read failures (EACCES/EISDIR/torn JSON) propagate instead of a false no-op — only ENOENT yields []; EISDIR + non-Error rejection regression tests; tsc 0, eslint 0, 100% changed-line coverage; CI GREEN 6/6; merge-tree clean)
B3b B3 Change cards UI + rollback buttons (webview) B3a+B3c 1d tracking (#1402); round 10: head 0e021ef (CodeRabbit fixes from trial addendum: ChangeCard no-files step resolution, Tailwind v4 grow, sibling settings, Catalan fix; CI green; round 11: head c48522c (user feedback from the trial review: per-file open-in-editor control on change cards - CodeAccordion jump icon on diff rows + native button on no-diff rows - via the existing openFile webview message; changeCard.openFile i18n key in all 18 locales; 3 new spec tests incl. the CodeRabbit a11y fix making the compact-row control a native button; CI re-running); round 12: head 502f8ca (typed VSCodeCheckbox change events + typed settings test doubles; CI GREEN); round 13: head ea90ea8 (CodeRabbit review fixes: the three rollback/restore handler cases post correlated failure results when the import/journal/git restore throws, no-active-task results post localized copy across 18 locales, the change-card error states are focusable status elements (role=status + tabIndex + aria-label = the error), the compact-row open-file label names the target file (openFile {{path}} slot, 18 webview locales), es/hi/it/ko locale corrections, and the spec covers the schema-invalid JSON path; tsc 0, eslint 0, 100% changed-line coverage; CI GREEN 8/8; merge-tree clean)

3. PR specs

S1 — A1 Version token

  • New file src/utils/versionToken.ts: computeVersionToken(filePath): Promise<string> + pure versionTokenOfStat(stats) for testability.
  • Token format: dev:ino:size:mtimeNs:ctimeNs from one fs.stat; ns fields as decimal strings from BigInt (no float precision loss).
  • No production callers in this PR — pure infrastructure. A disk fact: every process observing the same file state computes the same token.
  • Tests (unit): determinism on identical stat; distinct token on size/mtime change; BigInt large-size handling; absent file rejects with ENOENT.
  • Acceptance: unit suite green; zero behavior change (no imports from production code).

S2 — A2 Observation registry

  • New file src/core/task/observationRegistry.ts: per-task Map<absolutePath, { version, observedAt }>; owner = task, so parent and subtask observations are independent.
  • Task owns an instance; ReadFileTool records an observation after a successful read (one extra stat, same call the write guard will reuse).
  • No behavior change — observations are recorded but not yet consulted.
  • Tests: registry unit tests (observe/get/replace-on-reobserve); ReadFileTool spec-level test asserting a read of an existing file registers an observation with the on-disk version; subtask isolation test.
  • Acceptance: read path cost = +1 stat; no other observable change.

S3 — A4 Atomic publish

  • New file src/services/file-safety/safeWriteText.ts: temp file in a private per-write staging dir → write → fsync → close → atomic rename; on Windows: ReplaceFile with DACL copy, rename fallback. Generalizes the staging/backup/rollback logic currently inside safeWriteJson (which is refactored to call this).
  • saveDirectly (all five write tools + write_to_file) switches from raw fs.writeFile to safeWriteText.
  • Behavior-preserving (no version guard yet): same writes succeed, but now crash/power-loss safe (reader sees only old-or-new complete content).
  • Tests: staging dir created/cleaned; fsync called before rename (mock fs); crash-during-write leaves no torn target (simulate failure between write and rename); Windows ReplaceFile path + DACL preservation; safeWriteJson existing suite still 100% (no regression); patch coverage 100%.
  • Acceptance: saveDirectly no longer calls raw fs.writeFile; safeWriteJson behavior unchanged.

S4 — A3 Guarded write/edit (the behavior change)

  • New file src/core/tools/guardedWrite.ts: compare-and-swap on the write path:
  • unobserved target → createIfAbsent: new file succeeds; existing file fails (forces a read first — the model re-reads and retries with the observed version);
  • observed-absent → createIfAbsent;
  • observed-present → replaceIfVersion(version): mismatch fails with "stale version — re-read the file, then retry";
  • edit keeps its literal-match check plus the version guard; unobserved edit fails with "file not read yet — read the file, then retry".
  • Per-absolute-path tail-promise chain (in-process FIFO) wrapping read → guard → publish: concurrent subtask mutations to the same file are deterministically ordered — one wins, the rest fail as stale.
  • Wired into WriteToFile / EditFile / SearchReplace / ApplyPatch / ApplyDiff.
  • Diff budget note: core (guard + FIFO chain + registry hook) is S4a; tool wiring is S4b if the combined diff would exceed the 800-line target.
  • Cross-process stance: no lockfile (would block the user's own editor); two processes on the same file are detected via the token, loser fails as stale and re-reads.
  • Tests (unit + concurrency): each guard branch per tool; stale-version failure carries the remediation suffix; unobserved-edit failure; two concurrent writers on one path → exactly one succeeds; observed-absent then concurrent-create → second fails stale; regression: normal single-writer flow unchanged (no new failures on existing tool suites).
  • Acceptance: all five write tools fail loudly (tool-call error, step event in chat) on stale/unobserved writes; model self-heals via re-read+retry in the standard loop; no silent overwrite path remains.

S5 — A5 Zoo-Code-Org#920 cross-instance regression test

  • Regression test: two extension instances (parallel tabs) racing updateTaskHistory on the locked-merge write; asserts the second instance's merge preserves the first's fields (no clobbered history item).
  • Base material: local branch fix/920-concurrent-task-history-cross-instance (5 commits, prior work).
  • Track [Fix] Task history disappears when user reopens a task Zoo-Code-Org/Zoo-Code#1319 (task-history safe-write retry + advisory-lock merge) — if it lands first, adapt or close as covered; do not double-fix.
  • Acceptance: test reproduces the clobber on the pre-fix code path (or documents why it no longer reproduces) and passes post-fix.

S6 — A6 Zoo-Code-Org#1221 truncated tool-call parser

L1 — Async post-save diagnostics

  • saveDirectly tail: instead of awaiting LSP diagnostics, emit them as an asynchronous follow-up event once settled.
  • Tests: save resolves without waiting on diagnostics; follow-up event fires with the settled diagnostic payload; no event on clean save (or a no-op event, per implementation).
  • Acceptance: post-save latency drops by the LSP-settle time; diagnostic information is still delivered (just later).

L2 — Chat-diff default approval path

  • Make the chat-diff (PREVENT_FOCUS_DISRUPTION) path the default approval flow; diff-editor path remains available.
  • Setting: flips the existing PREVENT_FOCUS_DISRUPTION default falsetrue (src/shared/experiments.ts:22); toggle kept as escape hatch, storage key unchanged (§6).
  • Tests: default path resolves to chat-diff; focus not stolen from the active editor (behavioral test at the provider level).
  • Acceptance: default approval no longer opens/refocuses the diff editor.

B1 — Per-write checkpoint

  • Extend ShadowCheckpointService: every successful write_to_file / edit_file / apply-patch records a checkpoint (currently only user message sends do), keyed by step; checkpoint payload reuses the existing shadow git at <globalStorage>/tasks/<taskId>/checkpoints.
  • Task start becomes a real O(1) baseline (today: no-op). Not a worktree.
  • Tests: checkpoint created per successful write (mock service assertions); task-start baseline recorded once; rollback-to-checkpoint restores file content; checkpoint count bounded per task.
  • Acceptance: after any agent write, git-in-shadow can show that step's snapshot; task start is cheap (no worktree).
  • Setting: perWriteCheckpoints (boolean, default true) — master switch for the B cluster; full Persisted Setting Checklist round trip ships in this PR (§6).

B2 — Per-task change journal

  • Append-only changes.jsonl under the task dir; one entry per file write: path, operation, checkpoint id, diff stats (additions/deletions from the already-computed approval diff).
  • Torn-tail repair on load (incomplete final line discarded, rest parsed).
  • Tests: append format; load with clean tail; load with torn tail (write truncated line, repair drops it); journal entry references the B1 checkpoint id.
  • Acceptance: journal is the single per-task audit list; survives a crash mid-write (no corrupt load).

B3 — Per-step change cards + rollback

  • Per-step change cards in the existing chat flow: reuse the unified diff + computeDiffStats already produced for approval; "N files changed this step" + per-file diff.
  • Rollback to any checkpoint, per file or per step (shadow git restore, journal as the index).
  • Auto-approval paths get the same cards after the fact.
  • Diff budget note: B3a = extension host (card payloads + rollback service), B3b = webview (cards UI + rollback buttons); split point = the 800-line target per PR.
  • Setting: changeCardDetail ("full" | "summary", default "summary") — round trip ships in B3a (§6).
  • Tests: card payload for a multi-file step; rollback per file restores only that file; rollback per step restores all its files; auto-approval step still emits cards.
  • Acceptance: the user can see and undo any agent step after the fact, including on fully auto-approved runs.

4. Order

  1. Phase 0 (open): fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371) Zoo-Code-Org/Zoo-Code#1380fix(task): guard saveClineMessages against abandoned tasks (fixes #1021) Zoo-Code-Org/Zoo-Code#1382perf(write-path): remove artificial write delays by default (part of #1375) Zoo-Code-Org/Zoo-Code#1381 land in any order; they are independent.
  2. S1 — first of the series (pure function, unblocked, unblocks S2).
  3. S3 ∥ S5 ∥ L2 ∥ S2 — S3 and S5 are independent of S1/S2; S2 needs S1. Interleave by CI availability.
  4. S4 — after S1+S2+S3 all merged.
  5. L1 — after S3 (it touches the saveDirectly tail).
  6. B1 → B2 → B3 — B chain after S3 (B1 records at the publish point; independent of S4 but sequenced after S3 to sit on the atomic-publish path).
  7. S6 — whenever fix(write-to-file): address partial filesystem error review Zoo-Code-Org/Zoo-Code#1066 is ready (stack on its head).

5. Risks

Risk Mitigation
S4 changes model-visible behavior (unobserved writes now fail) Remediation suffix makes the loop self-heal; loud+recoverable-stale is the epic's explicit model; regression suites for all five tools must stay green
Zoo-Code-Org#1379 (DTE mega-PR) touches Task.ts — rebase hazard for S2/S4/S5 Land S2 before Zoo-Code-Org#1379 merges if possible; otherwise rebase S-series on the newer head
S6 depends on Zoo-Code-Org#1066 (open since 08-23) Stack on Zoo-Code-Org#1066's head; do not cherry-pick around it
S5 overlaps Zoo-Code-Org#1319 Check Zoo-Code-Org#1319 status before starting; adapt or close as covered
Windows ReplaceFile DACL edge cases (S3) Dedicated unit tests on the Windows CI lane; rename fallback kept
B1 shadow-git growth (checkpoints per write) Per-task dir, bounded by existing checkpoint policy; monitor task dir size in tests
Cross-process writes: token detects, doesn't lock Epic decision — a lockfile would block the user's own editor; loser re-reads and retries

6. Configuration / user settings

Principle: the safety core (A1–A6) is not configurable — an off-switch would reintroduce the corruption this epic fixes. Configurable surface = the B cluster (visibility/rollback convenience) + existing behavior toggles (speed, approval surface). Every new setting follows the AGENTS.md Persisted Setting Checklist — full round trip: global-settings.ts (definition + shared default constant) → ExtensionState/message types → SettingsView binding to local cachedState (never live state) → updateSettings payload → webviewMessageHandler persistence via ContextProxy → ClineProvider.getState() with default → getStateToPostToWebview() (destructure + return) → every consumer same default semantics → import/export schema → focused tests (UI binding/save, persisted + unset via posted state).

New settings

Key Type Default Controls Lands in
perWriteCheckpoints boolean true Master switch for the whole B cluster: per-write checkpoints (B1), JSONL journal (B2), change cards + rollback (B3). Off = writes behave as today, no B artifacts, no rollback. B1
changeCardDetail `"full" "summary"` "summary" Card granularity: summary = one line ("N files changed +X −Y") + per-file list + rollback, diff rendered lazily on expand; full = inline unified diff rendered by default. Auto-approval steps always get the compact card regardless.
  • No "off" level for card detail: visibility is the point; users who don't want cards turn the master off.
  • Checkpoint retention stays with the existing checkpoint policy (bounded per task); revisit only on disk-usage reports — no setting now.

Existing settings (reused — no new key)

Key Phase Action
writeDelayMs 0b (open) Done: default 1000 → 0 (DEFAULT_WRITE_DELAY_MS); setting, UI, round trip unchanged.
PREVENT_FOCUS_DISRUPTION L2 Flip default falsetrue (experimental map, src/shared/experiments.ts:22). Keep the toggle as the escape hatch for users who prefer diff-editor approval. Decision inside the L2 PR: if the experimental settings section is hard to discover, surface it as a first-class approval-surface setting — move/rename in the UI only; the storage key stays stable so existing user values carry over.

Deliberately NOT settings (decision record)

  • A1–A4, A5, A6 (version token, observation registry, CAS guard, atomic publish, internal-state firebreak, truncation guard): always on. No "legacy unsafe mode" — the epic's acceptance criteria (no silent apply failures, loud + recoverable stale) are non-negotiable.
  • L1 async diagnostics: informational follow-up event; no toggle (a setting would only re-introduce the blocking path or drop the information).

Round-trip cost and diff budget

Each new setting touches ~8–10 files (types, extension host, provider, webview settings, schema, tests) ≈ 100–300 changed lines, and counts toward the diff budget (800-line target / 1000 hard cap):

  • perWriteCheckpoints round trip ships inside the B1 PR (not a separate PR) — it is part of the 2d B1 estimate.
  • changeCardDetail round trip ships inside B3a (extension-host side); if B3a + round trip exceeds budget, B3a keeps setting + payload and B3b is UI-only.
  • L2's default flip is one line in experiments.ts + its spec update — trivial.
  • Per-setting tests (checklist): SettingsView save binds cachedState and posts updateSettings; posted state asserts both persisted and default; import/export round-trips.
  • i18n: names/descriptions added to all locale files; check-translations CI covers it.

7. E2e plan (apps/vscode-e2e, aimock)

Principle (AGENTS.md test pyramid): e2e only for real extension-host boundaries and full-workflow smoke; detailed assertions stay at unit/spec. Existing base suites to extend: suite/tools/write-to-file.test.ts (real task → write_to_file → disk assertions, aimock-replayed) and suite/subtasks.test.ts (+ fixtures/subtasks.ts). New fixtures follow the aimock workflow: TEST_FILE-filtered record runs, toolCallId matching for turn 2+, match strings without timestamps/paths, verify with pnpm --filter @roo-code/vscode-e2e test:ci:mock.

Phase E2e decision
Phase 0 none — unit-level fixes
S1, S2 none — pure function + spec level
S3 no new e2e (crash-safety = unit tests on mocked fs); existing write-to-file smoke must stay green as the real-host regression
S4 E1 stale-write self-heal (ships with S4b): scripted multi-turn fixture — tag prompt → write_to_file on an existing file the model never read → expect the tool-call failure step ("file not read yet") → read_file (matched by toolCallId) → write_to_file retry → attempt_completion; assert final on-disk content = new content and the chat step shows the failure + success. E2 concurrent subtask writers (ships with S4b or as a follow-up small PR): extend the subtasks suite — two subtasks write the same file; assert final content is one complete version (no torn/interleaved file); keep ordering assertions loose to avoid CI flake
S5 unit-level cross-instance test only; a true two-live-instance e2e would require extending the restart coordinator (sequential phases today) — stretch goal, out of scope for the series
S6 E3 truncated tool call writes nothing (ships with S6): fixture whose write_to_file arguments are truncated JSON → task step shows the "arguments were truncated" error → assert the target file is absent/unchanged on disk. Feasibility check during implementation: if aimock cannot replay a malformed arguments payload, fall back to parser-level integration test (still justified)
L1 none — timing assertions are flaky in e2e
L2 extend write-to-file suite: default approval surface resolves to chat-diff; approval completes and file is saved without the diff editor opening
B1 unit; optional restart-scenario smoke that the checkpoints dir exists after a write
B2 none — unit (journal format/repair)
B3 webview-ui tests for card rendering + rollback button state; E4 step rollback end-to-end (ships with B3b): multi-step write task → assert per-step change cards appear → invoke rollback of one step → assert on-disk content of that step's files reverts to the pre-step version

8. Per-PR execution rules (AGENTS.md)

  • One PR per phase; each PR: focused diff, its own commit history, own CI.
  • Diff budget (two tiers): total changed lines (additions + deletions) target < 800 per PR, hard cap 1000. The 800 target leaves a ~200-line buffer so that fixes for reviewer/bot (CodeRabbit) comments added to the same PR — new tests, extra branch coverage, a small refactor — still land under the 1000 cap without a force-push/reopen. If the initial diff already exceeds 800, expect to split into sequential sub-PRs (S4a/S4b, B3a/B3b below). Verify with the PR diff stat before opening, and re-check the stat after any review-fix commit so the final PR stays ≤ 1000.
  • Narrowest-layer tests: pnpm --dir src exec vitest run <path>; suite green before push.
  • pnpm --dir src exec eslint --max-warnings=0 <files>; suppression counts never increase.
  • No .changeset files, no CHANGELOG edits (per AGENTS.md).
  • Patch coverage 100% (codecov) on every PR.
  • PR body: Summary / Changes / Tests / Provenance (when extracting from a feature branch), linking this issue + epic [EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption Zoo-Code-Org/Zoo-Code#1375.

9. Acceptance criteria (series complete)

  • All PRs merged (Phase 0 + S1–S6 + L1–L2 + B1–B3, including any S4/S4b and B3a/B3b splits); every PR at or under the 800-line target, and never over the 1000-line hard cap (the 800→1000 band is reserved for review-fix commits).
  • Epic [EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption Zoo-Code-Org/Zoo-Code#1375 acceptance criteria met: no silent apply failures; loud + recoverable stale; per-step visibility; multi-agent safety; task start is a real baseline; write-path default latency ≈ 0; suppression counts unchanged.
  • E2e suite green on the mock lane (pnpm --filter @roo-code/vscode-e2e test:ci:mock), including E1 (stale-write self-heal), E2 (concurrent subtask writers), E3 (truncated tool call writes nothing), and E4 (step rollback end-to-end).
  • Settings: perWriteCheckpoints and changeCardDetail complete the full Persisted Setting Checklist (UI save via cachedState, posted-state persisted + default assertions, import/export round trip, all locales); PREVENT_FOCUS_DISRUPTION defaults to true with the existing toggle preserved.

Execution status (synced 2026-08-29 (round 13): UI/UX review of the change-card rollback flow found a P1 semantic bug - the card rollback restored a file to the step's own POST-write checkpoint (a no-op for the newest step; contradicts the epic AC "undo what the agent did" and the card's own confirm copy). User chose Option A + forward restore, shipped as: (1) Zoo-Code-Org#1410 head 630f273 - rollback now resolves the restore target from the B2 change journal to the file's PRE-step state (preceding journal entry's checkpoint; task-start baseline for the file's first change; undoing a create deletes it again, undoing a delete restores it) + new restoreLatestFile (forward direction; clean no-op success when the task never wrote the file); 20-test spec; all local gates green (vitest 20/20, tsc 0, eslint 0 with suppression unchanged, 100% changed-line coverage); merge-tree clean vs upstream/main efc30cf. (2) Zoo-Code-Org#1412 head 9a430d6 (stacked on 630f273) - per-file "Restore latest version" control (two-step confirm + warning), new checkpointRestoreLatestFile webview message + handler case (results reuse checkpointRollbackResult carrying kind rollback/restore-latest; legacy no-kind results still route to the rollback control), per-file rollback confirm now shows the warning text, 5 new/updated chat:changeCard.* keys x 18 locales, +5 ChangeCard tests + +4 handler tests; merge-tree clean vs efc30cf. New upstream tracking issue Zoo-Code-Org#1435 documents the semantic correction; both PR bodies refilled. Trial addendum 9: head 7591bc8 (cherry-picks f0f3313 + 7591bc8 applied cleanly onto d6d08e8; local gates: tsc 0 both dirs, eslint 0, vitest 31+22 green, merge-tree clean; Code QA CI running). Fork PR #34 (-> trial) remains open, CI 15/15, head 7efbf33 merge-tree clean vs the new trial tip 7591bc8. The fuzzy-match observability/perf suggestions stay tracked as upstream Zoo-Code-Org#1418; all other previously-green heads unchanged)

PR Branch State
Phase 0a Zoo-Code-Org#1380 fix/mcp-settings-stub-race-1371 open, CI green, awaiting review (tracking #1385); round 10: head 9dd9825 (CodeRabbit fixes from trial addendum: mcp merge array guard, spec-mock production parity, unknown-safe error.code; CI green)
Phase 0b Zoo-Code-Org#1381 fix/write-delay-default open, CI green (fixed a missed DiffViewProvider spec assertion), awaiting review (tracking #1386); round 10: head 6548013 (CodeRabbit fixes from trial addendum: webview writeDelayMs now uses the shared DEFAULT_WRITE_DELAY_MS + pre-hydration test; CI re-running)
Phase 0c Zoo-Code-Org#1382 fix/abandoned-subtask-save-race-1021 open, CI green, awaiting review (tracking #1387)
S1 Zoo-Code-Org#1383 feat/version-token-s1 open — #1383 (3 commits; final review APPROVE; CI green on ubuntu gate; tracking #1388)
L2 Zoo-Code-Org#1384 feat/l2-chat-diff-default open — #1384 (74 lines, 1 commit; ALL checks green incl. ubuntu+windows unit + codecov/patch; CodeRabbit 3/3 findings confirmed fixed & replied in c0ef476; tracking #1389)
S3 Zoo-Code-Org#1391 feat/atomic-publish-s3 open — #1395 (amended to a37dd24 after the 5th CodeRabbit pass: the "no temp left on failure" spec now drives a real post-commit backup cleanup failure (unlink EPERM) and asserts the target stays committed with no temp behind — the non-fatal cleanup path is now covered; 4th-pass tempPath fchmod fix retained; local gates green: 49 unit, safeWriteText.ts 100% stmts/branch/lines, eslint 0, tsc 0; ALL checks green incl. ubuntu unit + e2e-mock + codecov/patch; CodeRabbit review completed; 14/14 findings replied; tracking #1391)
S2 Zoo-Code-Org#1390 feat/observation-registry-s2 open — #1394 (stacked on Zoo-Code-Org#1383, targets main; CodeRabbit findings addressed in 2965ad1, ubuntu CI green; tracking #1390)
L1 Zoo-Code-Org#1396 feat/async-save-diagnostics-l1 open — #1403 (991ab69, rebased onto S3 head a37dd24: preDiagnostics race fixed; diagnostics tail filtered to the saved file via arePathsEqual (case-insensitive on Windows); stale .catch comment corrected; as any -> bracket notation (ledger 310->306); 4th CodeRabbit pass fixed — 100 ms in-memory settle delay moved from the blocking save path into the diagnostics tail (saves with diagnostics off / writeDelayMs 0 no longer pay it); 77/77 unit, eslint 0, tsc 0; all 12 CodeRabbit findings replied — 3 cured by the S3 rebased base; e2e-mock passed (subtasks resumeTask flake did not reproduce); stacked on Zoo-Code-Org#1395 (S3); tracking #1396)
B1 feat/per-write-checkpoints-b1 open — #1404 (amended to abfbe7f (round 3: task-start baseline now awaited before the request loop + deferred-promise test) — perWrite garbled i18n fixed in 7 locales, task-start baseline forced, CheckpointSettings spec updated; ALL checks green incl. ubuntu+windows unit + codecov/patch, independent on upstream main 78c712a: per-write checkpoints in write_to_file/edit_file/apply_patch — checkpoint now gated on full patch success (handlers report success; no checkpoint after rejected approval / failed write), task-start baseline guard (incl. unset default-on test), perWriteCheckpoints setting default-on round-trip incl. explicit-false webview-state test + 17-locale translations; ubuntu CI + e2e-mock + codecov green; all 5 CodeRabbit findings replied (1 false positive — single shared default constant verified); local gates: eslint 0, tsc 0 src+webview, affected suites green; tracking #1397)
S4a feat/guarded-write-s4a open — #1405 (amended to 7a25fc0 — enqueue settled-chain eviction + replaceIfVersion ENOENT normalization + regression tests; ubuntu + codecov/patch green after the amend; on S3 head a37dd24 + S1×3 + S2 stack: CAS core + per-path FIFO chain + registry hook; CodeRabbit pass fixed — resolveAbsolutePath always path.resolve (registry-key match), pre/post-read bigint stat token capture on native + legacy read paths (mutation mid-read leaves target unobserved), safeWriteJson locks the resolved publish target (symlink-alias coordination); local gates: 169 units, 100% line coverage on changed lines, eslint 0, tsc 0; 3/3 findings replied; tracking #1399)
S4b feat/guarded-write-wiring-s4b open — #1408 (rebased to 68be264 on amended S4a 7a25fc0 — inherits the ENOENT fix; CI re-running; guard wired once at the DiffViewProvider.saveDirectly choke point (6 tools / 7 call sites), per-tool writeKind plumbing, fail-closed on collected taskRef; local gates: 188 units, 100% on changed lines, eslint 0, tsc 0; tracking #1400)
B2 feat/change-journal-b2 open — #1406 (amended to 93a8329 (round 3: mistake counter now resets only on a fully successful patch + 2 regression tests) on B1 head baacf59 — handler result objects, no-op/move gating, partial-flush gate patchSucceeded
B3a feat/change-cards-b3a open — #1411 (amended to 2500ab3 (round 3: apply-patch + edit-file checkpoints now awaited, no fire-and-forget interleaving + 2 deferred-promise tests) on B2 head 410591e — round-2 CR fixes: checkpointSave.spec negative assertions now filter recorded say calls by the change_card type (the three-argument toHaveBeenCalledWith can never match the seven-argument call, so the old assertion guarded nothing), mergeExtensionState spec fixture uses non-default perWriteCheckpoints/changeCardDetail and asserts a partial push that omits the keys preserves them; CI re-running; change_card payload + checkpointSave emission, tool approval-diff plumbing (WriteToFile / EditFile / ApplyPatch), changeCardDetail setting full round trip, i18n 18 locales; split from the planned B3a scope by the 1000-line cap — 934 changed lines; local gates: 429 units, eslint 0 src+webview+types, tsc 0 both dirs, 100% on changed lines; round 11: head 341a9c2 (94fea2f + CodeRabbit fix: pre-hydration defaults spec now asserts both the changeCardDetail and perWriteCheckpoints initializer defaults; CI GREEN); round 12: head 67d8525 (trial-review audit: the edit and search_replace tools wrote with no per-write checkpoint / journal entry / change card — both now wired with the same checkpointSave call site + the auto-approved compact-card flag; focused 8-test spec; merge-tree clean vs upstream/main; CI running); tracking #1401)
B3c feat/rollback-service-b3c open - #1410 (round 13: head 630f273 - rollback semantic correction, upstream Zoo-Code-Org#1435: rollbackFile/rollbackStep resolve the restore target from the B2 change journal to the file's PRE-step state (preceding journal entry's checkpoint; task-start baseline for the file's first change; undoing a create deletes it again, undoing a delete restores it); new restoreLatestFile(task, filePath) forward direction (no-op success when the task never wrote the file); 20-test spec; incremental diff 2 files +371/-134; local gates: vitest 20/20 + checkpoints dir 79/79, tsc 0, eslint 0 (suppression unchanged), 100% changed-line coverage; merge-tree clean vs upstream/main efc30cf; CI running; tracking #1409 + semantic correction #1435)
B3b feat/change-cards-ui-b3b open - #1412 (round 13: head 9a430d6 stacked on B3c 630f273 - per-file "Restore latest version" control (two-step confirm + warning text), new typed checkpointRestoreLatestFile webview message + handler case (results reuse checkpointRollbackResult with kind rollback/restore-latest so the two per-file controls never cross-talk; legacy no-kind results still route), per-file rollback confirm now shows the warning text, i18n: 5 new/updated chat:changeCard.* keys x 18 locales, ChangeCard spec +5 tests, handler spec +4 tests; incremental diff 27 files +929/-150 (includes the B3c service files arriving via the stack); local gates: tsc 0 both dirs, eslint 0 (suppression unchanged), vitest 22+11 green, 100% changed-line coverage, i18n parity 18/18; merge-tree clean vs upstream/main efc30cf; CI running; raw-diff budget exception still documented in the PR body; tracking #1402 + semantic correction #1435)
S5 blocked on upstream Zoo-Code-Org#1319 (open)
S6 blocked on upstream Zoo-Code-Org#1066 (CHANGES_REQUESTED)
TRIAL feat/fws-trial-all open - #1413 (trial build of all 15 + the B3c/B3b semantic correction; round 13 head 7591bc8 = d6d08e8 + cherry-picks of Zoo-Code-Org#1410 630f273 (f0f3313) and Zoo-Code-Org#1412 9a430d6 (7591bc8), both applied cleanly; local gates: tsc 0 both dirs, eslint 0, vitest 31+22 green, merge-tree clean vs efc30cf; Code QA CI running - PR body will be refilled with the new run/artifact/inner-vsix sha256 once it settles; screenshot release assets (v3.80.0-fws-trial tag) unchanged)

Blockers to watch: Zoo-Code-Org#1066 (S6), Zoo-Code-Org#1319 (S5), Zoo-Code-Org#1046 landing (raises A3 urgency), Zoo-Code-Org#1379 merge (Task.ts rebase).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions