Skip to content

feat(tools): wire guarded writes into the diff-view save paths (S4b, #1375) - #1408

Open
easonLiangWorldedtech wants to merge 79 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/guarded-write-wiring-s4b
Open

easonLiangWorldedtech wants to merge 79 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/guarded-write-wiring-s4b

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes: #1400 — "Tracking S4b: wire guarded writes into the diff-view save paths" (this unit's tracking issue in this repository).

  • Chain tracking issue: 1991 — the declared split plan, unit boundaries, and merge order for the guarded-write chain; this pull request is unit S4b of that plan.
  • Epic: 1375 (file-write safety series; this unit does not close it — the chain does).

Description

Split unit S4b of the guarded-write chain: wires the S4a compare-and-swap guard into every write tool and the diff-view save paths. What this head actually does:

  1. write_to_file / edit_file / search_replace / apply_diff / apply_patch route their publish through guardedWrite: an unobserved write to an existing file fails loudly ("read the file first, then retry"), a stale-version write fails with the re-read-then-retry remediation, and the model self-heals through its standard read-retry loop — no silent overwrite path remains. Failures surface as tool-call errors with a step event in chat.
  2. edit_file keeps its existing literal-match check and adds the version guard on top.
  3. The apply_patch in-place publish records the read observation for the hunk target (stat before/after, observe when the version is unchanged) so the guarded publish is not rejected as an unobserved write; the move publish carries the source's completeness to the destination and publishes through saveDirectly(..., "create").
  4. guardedWrite carries the S4a CAS core (createIfAbsent / replaceIfVersion, per-path FIFO chain) plus a disposal guard: a queued guarded write checks the task's disposed flag at the head of its queue link and throws CancelledTaskWriteError before publishing anything.
  5. DiffViewProvider.saveDirectly / saveChanges publish through the guard; a refused guarded publish rolls the editor buffer back.
  6. ReadFileTool captures the S2 observation on read. The unit carries its own copies of the S1–S4a primitives (observationRegistry, guardedWrite, safeWriteText, safeWriteJson) because unit branches are not cumulative; the declared merge order lands S1–S4a first and this branch's copies reconcile against main at rebase.

Honest contract: the version check and the commit rename are separate syscalls — the window is one syscall wide, documented at guardedWrite.ts and safeWriteText.ts. The chain-level owed changes (canonical lock / compare-and-publish, publish-target confinement, backup cleanup policy) are registered on the chain tracking issue (record 6106959976).

Content source of record: the S4b unit of the guarded-write chain declared on tracking issue 1991; branch feat/guarded-write-wiring-s4b carries the unit content on the current org main tip. Trial review context: org PR 1413.

Budget (GitHub view against the base): +4931/−179 across 26 files. The bulk is the carried primitive copies and their specs; a primitive copy and the spec that tests it cannot be separated without breaking the fidelity contract. Documented deviation: the mutation-diff gate reports 504 changed executable lines against the 500 cap (4 over); the maintainer declares this a budget-cap deviation, not a split trigger — this unit is superseded content that reconciles at rebase once the U-chain primitives land in main (chain tracking issue 1991).

Test Procedure

pnpm --dir src exec vitest run --globals core/tools/__tests__/guardedWrite.spec.ts core/task/__tests__/observationRegistry.spec.ts services/file-safety/__tests__/safeWriteText.spec.ts utils/__tests__/safeWriteJson.test.ts
# expected: 102 passed, 1 skipped
pnpm --dir src exec vitest run --globals core/tools/__tests__/writeToFileTool.spec.ts core/tools/__tests__/editFileTool.spec.ts core/tools/__tests__/editTool.spec.ts core/tools/__tests__/searchReplaceTool.spec.ts
# expected: 126 passed, 5 skipped
pnpm --dir src exec vitest run --globals core/tools/__tests__/applyPatchTool.execute.spec.ts core/tools/__tests__/applyDiffTool.guardedWrite.spec.ts core/tools/__tests__/readFileTool.spec.ts integrations/editor/__tests__/DiffViewProvider.spec.ts
# expected: 187 passed; the two DiffViewProvider failures below are local-only
pnpm --dir src exec tsc --noEmit
pnpm --dir src exec eslint --max-warnings=0 <every changed file>

Two DiffViewProvider.spec.ts tests fail only on the Windows junction donor, where @roo-code/types resolves to a stale donor build: "should use default values when no parameters provided" (known local-only) and "pins the default write delay to zero" (this branch defines DEFAULT_WRITE_DELAY_MS = 0 in packages/types/src/global-settings.ts; the donor dist exports 1000). CI platform-unit-test passes both on ubuntu and windows.

Pre-Submission Checklist

  • Issue Linked: Closes: Tracking S4b: wire guarded writes into the diff-view save paths #1400 (see Related GitHub Issue above).
  • Scope: one gate scope — write-tool and diff-view wiring of the guard; the CAS core itself is S4a.
  • Self-Review: thorough self-review performed.
  • Testing: per-tool guard branches (unobserved-existing fails, stale fails, observed-success unchanged) in each tool's spec; the disposal guard and the move-publish rejection path are pinned.
  • Documentation Impact: considered — no user-facing documentation changes required (internal wiring).

Documentation Updates

No user-facing documentation updates are required.

Verification at this head (aab1cb9)

  • CI: required checks 7/7 green at this head.
  • Local (Windows junction): vitest 415 passed / 6 skipped / 2 local-only donor-dist failures (named above); ESLint --max-warnings=0 clean on all 25 changed files; tsc --noEmit local baseline carries 137 donor-junction artifacts in files outside this PR's diff (the donor @roo-code/types dist predates main commits this branch merged) — CI compile is green; src/eslint-suppressions.json unchanged.

Updated this round

  • Body rewritten to describe the current head (addresses the Description check row): Related GitHub Issue now uses the required "Closes: Tracking S4b: wire guarded writes into the diff-view save paths #1400" format and links the chain tracking issue and the epic; the template sections are complete; review-status commentary removed.
  • No code change this round: the remaining pre-merge rows (Security Boundaries, Persistence Integrity, Lifecycle Resource Cleanup) are the three chain-level primitive registrations recorded at tracking record 6106959976 — their disposition (owning-unit implementation with port, or reconciliation at rebase once the earlier chain units land) is a chain-level decision tracked with the maintainer.

Additional Notes

Merge order: S1 (PR 1383) → S2 (PR 1394) → S3 (PR 1395) → S4a (PR 1405) → this unit (S4b). Unit branches are not cumulative; this branch carries copies of the earlier primitives by design, and the mutation-diff budget row (504 changed executable lines against the 500 cap) is a maintainer-declared documented deviation (see Budget).

Main refresh (7c11826)

Refreshed onto main 4f1dee18e (merge 314209a11, conflict resolutions: read-token pairing blocks kept alongside main's catch { narrowing in EditTool/SearchReplaceTool, spec import union; lint-gate commit drops imports unused after the merge and adds the now-required toolCallId to the branch specs' ToolCallbacks literals). tsc drift-only, 297/303 affected specs, mergeable true. Body dispositions unchanged.

CI round final head (7ad72aa)

Lint-gate commit drops the SafeWriteTextOptions import main's side orphaned. All six required checks green at 7ad72aab2.


Corrections, 2026-11-09 — measured at head 9fa7737142fcf25d254d6af9bca838eaf7d3f459

The sections above are left exactly as written; this section adds the numbers that hold now.

claim above measured at this head how it was measured
+4931 / -179 26 paths, +6123 / −50 git diff --numstat 73931fe8be253b93ec11103a0672c615e849f895 9fa7737142fcf25d254d6af9bca838eaf7d3f459
"7/7 required checks passing" the pipeline has six required arms: check-translations, compile, knip, e2e-mock, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest); all six were success at the previous head ff4686cd18…, four are success here required-contexts of the Protect Main ruleset, read per head
"415 passed / 6 skipped / 2 donor-dist failures" matches neither lane: vitest.core.config.ts → 200 files, 4351 passed | 9 skipped, exit 0. vitest.unit.config.ts → 578 files, 12514 passed | 37 skipped, 11 failed, exit 1 both lanes run in this worktree with pinned node v22.23.4
"ESLint clean on all 25 changed files" true for the 25 lintable .ts paths (0 messages, exit 0); the diff carries 26 paths and the 26th is src/eslint-suppressions.json, which is not a lintable source file eslint --max-warnings=0 over the changed files
mutation-diff passing completed / failure at ff4686cd18…, queued at this head — advisory, not chased GET /commits/{sha}/check-runs

DEFAULT_WRITE_DELAY_MS = 0 is real (packages/types/src/global-settings.ts:26) but that file is not in this unit's diff, so it is a statement about main rather than about this change. The unit-lane failures named above (MarketplaceManager ×4, rules symlink rows ×5, parser ×2 TelemetryService not initialized) reproduce identically at the parent ff4686cd18…, so they are this Windows box's donor-node_modules class, not this head's.

Arm status at this head: check-translations, compile, knip, e2e-mock success; platform-unit-test (windows-latest) failure (run 42565686409, job 128991579779, step Run extension test lanes → pnpm run test:core, Tests 9 failed | 4354 passed | 9 skipped (4372)); platform-unit-test (ubuntu-latest) cancelled by that run's fail-fast.

The windows failure is 9 rows of src/core/tools/__tests__/applyDiffTool.outsideWorkspace.spec.ts — a file that is not on this branch. Main took it in 1b9cb7d35 (#2343) at 2026-11-09T00:14:38Z, after the 2026-11-08T20:36:39Z run that made the previous head green, and CI checks out refs/remotes/pull/1408/merge: it type-checks and tests the head+main merge tree, not the head. Measured in that tree (git merge-tree --write-tree 9fa7737142… e487cd7b2a… → tree 73632a8b3e57…, clean): main tip alone 11 passed (11); the merged tree 9 failed | 2 passed; with the spec's fs/promises double given a stat and its diffViewProvider.open() double recording its re-read — 11 passed (11) and that tree's whole core lane 201 files, 4363 passed | 9 skipped, exit 0. Nothing on this branch has to change for that: the main-side fix is #2372 and the windows arm is recorded here as red pending it rather than patched around on this branch. Carrying the file across was built and measured and rejected — git merge-tree --write-tree then exits 1 with an add/add conflict on that path (stage 2 = main's cbaa2433ca5e…, stage 3 = the carried-and-adapted bf27e65ce6d8…), trading one red matrix arm for a conflicted pull request.

Housekeeping: two review replies here initially cited 9fa775714, a typo for 9fa7737142… — sorry — corrected in place (comments 4509629757, 4509629950, plus correction reply 4509643728).

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-author PR is waiting for the author to address requested changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking S4b: wire guarded writes into the diff-view save paths

2 participants