Repository navigation
Open
Description
Unit U6 of the PR #1066 split (5/6).
Split plan: #703
PR: #1932
Content source of record: tag pr1066-source = 46d1d218701f0ce2d675b1b315489bacb6b0f77d (easonLiangWorldedtech/Zoo-Code).
Unit contract
The execute() cleanup invariant: a finally around handleError so the diff cleanup runs even when handleError rejects, the writeApproved flag, and the consecutive-mistake-counter order.
Why this unit exists on its own
Single provider group + single gate scope (the execute() error path).
Boundary
- base:
e3c10401f760(previous unit head, U3) - head:
9b93a6f882a4 - chain: U3 -> U6 -> U7
- merge order is fixed: U12 -> U4 -> U5 -> U3 -> U6 -> U7/FINAL; the FINAL integration PR (fix(write-to-file): clean partial state on missing-param and rooignore denial + integrate the #1066 split series (6/6 + FINAL) #1928) is the sole merge target.
Files and budget
src/core/tools/WriteToFileTool.ts+45/-11 — content subset of sourcesrc/core/tools/__tests__/writeToFileTool.spec.ts+379/-4 — content subset of source- budget: 439 a+d / 2 files — SOFT-OVERSHOOT (rationale: single provider group + single gate scope; under the 1000 hard cap)
- mutation gate: 34 changed executable lines — under the 500 cap; valid mutants for the whole PR are 116 / 400, so no directive was added by this unit.
Verification (must pass by once, binary)
zdt split verify --contract U6.json --worktree <wt> --head 9b93a6f882a4— PASS (every changed file is a content subset of the source of record, or an explicitly sanctionedallowNewfile).- Tests: 259 passed / 5 skipped, exit 0 (narrowest relevant suites:
Task.spec.ts,writeToFileTool.spec.ts,writeToFileTool-partial-state-cleanup.spec.ts,removeClineFromStack-delegation.spec.ts,presentAssistantMessage-custom-tool.spec.ts). - Changed-line coverage: 14 covered / 0 uncovered — PASS.
- ESLint
--prune-suppressions --max-warnings=0: clean on every touched file; suppression counts unchanged. - No
.changesetfile, no CHANGELOG edit (AGENTS.md).
Deviations recorded
- Owns the test
reports a filesystem error only once across the streaming and execute phases, originally attributed to U5 (see U5).
Reproduce
git fetch https://github.com/easonLiangWorldedtech/Zoo-Code p1066/u6-execute-error-path-cleanup
node zdt.mjs split verify --contract U6.json --worktree <wt> --head 9b93a6f882a4
node zdt.mjs split measure --worktree <wt> --base e3c10401f760 --head 9b93a6f882a4
pnpm --dir src exec eslint --prune-suppressions --max-warnings=0 <touched file>
Scope re-cut (assigned coding scope)
Additional files defined and assigned to this unit (CodeRabbit Out of Scope Changes on #1932 at 443993e57; full reasoning in #41 (comment)):
src/integrations/editor/DiffViewProvider.tsandsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts- the diff-cleanup target that this unit'sfinallyinvokes: rollback of an unapproved stream (placeholder, created directories, and the buffer that must not be saved). Without it the invariant this unit asserts is only vacuously satisfied at this unit's head, becauserevertChanges()returned early whenopenDiffEditor()had rejected.- NOT this unit's, listed only to close the row:
BaseTool.tsbelongs to U3 (feat(tools): onParameterParseFailure teardown boundary (split 4/6 of #1066) #1931,e3c10401f) andClineProvider.tsto U4 (feat(write-to-file): per-task partial stream state + cleanup primitives (split 2/6 of #1066) #1929,d172c95ae); both show in fix(editor): release abandoned diff streams and owned placeholders (split 5/6 of #1066, unit U6a) #1932's file list because the chain is opened againstmainand the diff is cumulative through this unit.
Metadata
Metadata
Assignees
Labels
No labels
Activity
easonLiangWorldedtech commentedon Oct 10, 2026
Scope re-cut for U6 (#1932): which extra files are not U6's, and which one is
Trigger: CodeRabbit Out of Scope Changes check (Error) on #1932 at
443993e57. Row, verbatim:The row's own second remedy is the one we are taking:
or update issue #1937 to explicitly define and assign the additional coding scope before merge. This comment is that definition; #1937 now carries the same text in its body. It is per file, because the three files have three different answers, and the reason for each is the owning commit, not the fact that the code already exists.1.
src/core/tools/BaseTool.ts(+36/-3) - NOT U6. Owned bye3c10401f feat(tools): onParameterParseFailure teardown boundary, which is U3 (#1931).git log -m 036245c5e..HEAD -- src/core/tools/BaseTool.tsreturns that commit and the org-main merge and nothing else. It appears in #1932's file list only because the split branches live on the fork, so every unit PR in the chain is opened againstmainand GitHub shows the cumulative diff through this unit - which is stated in the PR body under Chain position. Merge order is fixed at U12 (#1927) -> U4 (#1929) -> U5 -> U3 (#1931) -> U6 (#1932) -> U7/FINAL (#1928); by the time U6 merges, U3 has merged and this hunk is no longer in U6's diff.2.
src/core/webview/ClineProvider.ts(+6/-0) - NOT U6. Owned byd172c95ae feat(write-to-file): per-task partial stream state + cleanup primitives, which is U4 (#1929): thewriteToFileTool.clearTaskState(task)call beforetask.dispose(), i.e. the history-task disposal the row names. Same cumulative-diff explanation as above.3.
src/integrations/editor/DiffViewProvider.ts(+121/-10) andsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts(+213/-1) - U6's own, and it cannot live outside U6. Owned by443993e57, a U6 commit. The reason is not that it was written here first:a finally around handleError so the diff cleanup runs even when handleError rejects. The cleanup thatfinallyruns isDiffViewProvider.revertChanges(). Before this hunk, for a new file whoseopenDiffEditor()had rejected there was noactiveDiffEditorandrevertChanges()returned early, leaving the empty placeholder and the directoriesopen()had created on disk. The invariant U6 asserts was then only vacuously satisfied at U6's own head: the finally ran, called a function, and that function did nothing. A test ofthe cleanup runsthat cannot distinguishcalledfromdid somethingdoes not constrain the code U6 is about.revertChanges()from saving a dirty buffer on the rollback path. Saving unapproved streamed content is the state theexecute()error path exists to roll back, so the flagwriteApprovedand the buffer behaviour are the same invariant seen from two sides; splitting them across units would let one side move without the other.Assigned to U6 explicitly:
src/integrations/editor/DiffViewProvider.tsandsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts, for the diff-cleanup target of theexecute()error path only (rollback of an unapproved stream: placeholder, created directories, and the buffer that must not be saved). Anything in that file beyond the rollback target stays out of U6.Head this declaration is written against:
443993e57d65b75c8b1a3a2121af7e495104d538. The follow-up commit that fixes the two remaining rows on #1932 (Persistence Integrity: route the rollback throughdiscardUnapprovedStream()as defined by the earliest unit that defines it, U7/#1928f8f7ce19a; Lifecycle: the two-window + liveness shape from U4/#192961dd05a2c) stays inside the file pair declared above.fix(write-to-file): cover the pre-streaming window and discard withou…
easonLiangWorldedtech commentedon Oct 10, 2026
Scope re-cut for U6 (#1932): which extra files are not U6's, and which one is
Trigger: CodeRabbit Out of Scope Changes check (Error) on #1932 at
443993e57. Row, verbatim:The row's own second remedy is the one we are taking:
or update issue #1937 to explicitly define and assign the additional coding scope before merge. This comment is that definition; #1937 now carries the same text in its body. It is per file, because the three files have three different answers, and the reason for each is the owning commit, not the fact that the code already exists.1.
src/core/tools/BaseTool.ts(+36/-3) - NOT U6. Owned bye3c10401f feat(tools): onParameterParseFailure teardown boundary, which is U3 (#1931).git log -m 036245c5e..HEAD -- src/core/tools/BaseTool.tsreturns that commit and the org-main merge and nothing else. It appears in #1932's file list only because the split branches live on the fork, so every unit PR in the chain is opened againstmainand GitHub shows the cumulative diff through this unit - which is stated in the PR body under Chain position. Merge order is fixed at U12 (#1927) -> U4 (#1929) -> U5 -> U3 (#1931) -> U6 (#1932) -> U7/FINAL (#1928); by the time U6 merges, U3 has merged and this hunk is no longer in U6's diff.2.
src/core/webview/ClineProvider.ts(+6/-0) - NOT U6. Owned byd172c95ae feat(write-to-file): per-task partial stream state + cleanup primitives, which is U4 (#1929): thewriteToFileTool.clearTaskState(task)call beforetask.dispose(), i.e. the history-task disposal the row names. Same cumulative-diff explanation as above.3.
src/integrations/editor/DiffViewProvider.ts(+121/-10) andsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts(+213/-1) - U6's own, and it cannot live outside U6. Owned by443993e57, a U6 commit. The reason is not that it was written here first:a finally around handleError so the diff cleanup runs even when handleError rejects. The cleanup thatfinallyruns isDiffViewProvider.revertChanges(). Before this hunk, for a new file whoseopenDiffEditor()had rejected there was noactiveDiffEditorandrevertChanges()returned early, leaving the empty placeholder and the directoriesopen()had created on disk. The invariant U6 asserts was then only vacuously satisfied at U6's own head: the finally ran, called a function, and that function did nothing. A test ofthe cleanup runsthat cannot distinguishcalledfromdid somethingdoes not constrain the code U6 is about.revertChanges()from saving a dirty buffer on the rollback path. Saving unapproved streamed content is the state theexecute()error path exists to roll back, so the flagwriteApprovedand the buffer behaviour are the same invariant seen from two sides; splitting them across units would let one side move without the other.Assigned to U6 explicitly:
src/integrations/editor/DiffViewProvider.tsandsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts, for the diff-cleanup target of theexecute()error path only (rollback of an unapproved stream: placeholder, created directories, and the buffer that must not be saved). Anything in that file beyond the rollback target stays out of U6.Head this declaration is written against:
443993e57d65b75c8b1a3a2121af7e495104d538. The follow-up commit that fixes the two remaining rows on #1932 (Persistence Integrity: route the rollback throughdiscardUnapprovedStream()as defined by the earliest unit that defines it, U7/#1928f8f7ce19a; Lifecycle: the two-window + liveness shape from U4/#192961dd05a2c) stays inside the file pair declared above.easonLiangWorldedtech commentedon Oct 10, 2026
discardFileTab()restore-then-save beforeclose(tab),saveBufferClean()5c0f21219close()'s 2nd arg ispreserveFocus, so the forced close never discarded a dirty tabsaveChanges(onCommit)commit point +writeCommittedstand-down2a9bfabfbplaceholderPathreleased after the approved save8084eab2esaveChanges()Deliberate deviation: the Critical thread also asks to gate
revertChanges()on an active edit session. Not taken in #1932 - that guard is #1931's shape of the same method (already filed as a follow-up for #1931's next push). #1932 takes the other half (reset()clearsrelPath/newContent), which removes the stale-path trigger the thread's reproductions depend on, and its tests record the current guard rather than the desired one.Correction to the Major rooignore thread: the two missing-parameter early returns did already call
reset(); what none of the three branches did was discard the content an earlier streaming delta had put in the diff view. All three now run the shared discard-then-reset teardown.easonLiangWorldedtech commentedon Oct 10, 2026
Port note: bring the commit-point semantics with the
DiffViewProviderhalfThe
DiffViewProvidercontent U6 ports from5c0f21219(restore buffers before closing) sits inthe same method as the commit-point signal added in U3
2a9bfabfb:saveChanges(diagnosticsEnabled, writeDelayMs, onCommit?)firesonCommitimmediately after thedocument write (including the clean-document case, where disk already matches), and
WriteToFileToolkeeps awriteCommittedflag so its catch path does not roll back a write thatalready landed.
Why it matters here: U6 owns the
execute()catch cleanup. If it rolls the diff view backwithout checking the commit point, it undoes a write the user approved. Two further facts about
the catch path, both verified at U3 head
5ff7bea73:revertChanges()early-returns when!isEditing, andisEditingis set only byopen(), soon the
saveDirectly(prevent-focus-disruption) branch the cleanup is a no-op while thecaller still reads "reverted". A test that runs only the default branch passes and misses it.
reset()clearscreatedDirs/adoptedCreatedDirswithout touching disk, so anything notrolled back before the reset is unreachable.
Port source (per-hunk ownership = the earliest commit defining the hunk): U3 / #1931 commits
5c0f21219and2a9bfabfb. CarryonCommitandwriteCommittedin the same positions, or thetwo branches will disagree when they merge.
Ported from the retired fork tracking item 6093538947 / the U3 note above.
easonLiangWorldedtech commentedon Nov 12, 2026
Re-split plan for U6 (#1932): two units, posted before anything opens or moves
Carrier verified rather than assumed. This issue's title is
[split-1066] U6 - fix(write-to-file): run the diff cleanup when handleError rejects, and its body names the pull twice: the linePR: https://github.com/Zoo-Code-Org/Zoo-Code/pull/1932, and in the scope re-cut: "NOT this unit's, listed only to close the row:BaseTool.tsbelongs to U3 (#1931,e3c10401f) andClineProvider.tsto U4 (#1929,d172c95ae); both show in #1932's file list because the chain is opened againstmainand the diff is cumulative through this unit." The chain map itself lives on #1989 ([Tracking] split-1066 chain: rules, port sources, chain-level rows), whose body carries the row| U6 | #1937 | #1932 | p1066/u6-execute-error-path-cleanup | open |and the sentence "Declared merge order: U3 #1931 -> U4 #1929 -> U5 #1930 -> U6 #1932 -> U7 #1928 (final integration, base main)." A chain-level copy of the unit-list change goes to #1989; everything unit-scope stays here.What splits, and into what
p1066/u6-execute-error-path-cleanup. Content:src/integrations/editor/DiffViewProvider.ts(all 21 hunks) andsrc/integrations/editor/__tests__/DiffViewProvider.spec.ts. That is the abandoned / changed-path stream handling and the placeholder lifecycle: the newdiscardUnapprovedStreamanddiscardOwnedPlaceholder, the newisEditingDifferentPathguard, andplaceholderPathbeing retained rather than cleared.revertChanges()still returnsPromise<void>at this head, so nothing in this unit changes the Task-side contract.p1066/u6b-tool-stream-teardown, basemain, stacked on U6a. Content:src/core/tools/WriteToFileTool.ts(28 hunks),src/core/tools/BaseTool.ts(2 hunks, theonParameterParseFailureteardown seam and the rewired native-args parse-failure path),src/core/webview/ClineProvider.ts(2 hunks, the dispose-timewriteToFileTool.clearTaskState(task)),src/core/tools/__tests__/writeToFileTool.spec.ts, the newsrc/core/tools/__tests__/writeToFileTool-partial-state-cleanup.spec.ts,src/__tests__/removeClineFromStack-delegation.spec.ts, andsrc/eslint-suppressions.json(writeToFileTool.spec.tsno-explicit-any5 -> 3, a decrease).discardUnapprovedStreamis called atWriteToFileTool.ts:174(declared 172, reused 254 and 311),discardOwnedPlaceholderat:196(declared 194),isEditingDifferentPathat:381and:716, andplaceholderPathappears in the comment at:190; those four names have zero references anywhere else at main tip. The provider side compiles alone; the tool side cannot lead it.Figures, every one with its head attached
Units are counted executable lines (instrument cap 500), selectors from the same
path:startLine-endLinelist, and instrumented mutants produced byStryker run --dryRunOnlyon that identical selector list (gate cap 400 valid mutants). Instrument:scripts/stryker-diff.mjsblob7dfe0006d2d5, selector logic run in process at caps 500 / 400, extension leg only. Test, mock, fixture and test-utils paths are excluded from both counts by the instrument itself, so moving specs is free in width.53cb5b5e7c8014d3226e5636bbca8999ed3b8526merged with pull head1eabeca9abbbbac031cc69643c4bcb7462db8363, candidatefe447fa7a766eb36f76d3361d2f4331817e97d61, tree84711aad7d7940c9c787fe460b1c6efad6d92d7d210b94a981bd34d343d662e132370b4d22760491, tree089b78a5ac60ae50a3e7cffc442057c34730db88, parent main tip210b94a981bd28c086751c16b76b1e7086fa85d96e641d147578, tree = the parent tree28c086751c16Additivity is exact, so nothing is estimated: 207 + 243 = 450 lines, 68 + 95 = 163 selectors, 183 + 227 = 410 mutants. Per file at the parent shape, from four separate dry runs on the same selector list:
DiffViewProvider.ts207 lines / 68 selectors / 183 mutants;WriteToFileTool.ts228 / 89 / 217;BaseTool.ts14 / 5 / 9;ClineProvider.ts1 / 1 / 1 -- each column sums to the parent total exactly.Density is what makes this pull answer differently from #2385: 2.52 instrumented mutants per selector here, against 3.55 measured on #2385's
safeWriteText.ts(527 lines / 150 selectors / 533 mutants), where every defensible seam put one half back over a cap. Here the files split along a real call-graph boundary and both halves keep 173-217 mutants of headroom, so no carve and no gate exclusion is needed.What these numbers are
Vitest failed to find test files related to mutated filesand thenNo tests were executed. It is a count of mutants produced, never a verdict, a score, a kill rate or a pass, and no unit is called green on it.2026-11-11T23:21:22Z, GraphQLstatusCheckRollupfor 22 contexts at head1eabeca9abbb), not from my own API call, and I have not re-read the ruleset.The eight type errors U6a carries, and what CI actually reports for this pull
Measured on a worktree whose install matches the repository lockfile (
tsc --noEmitfromsrc, build-info file redirected so no shared cache was written): main tip53cb5b5e7c80gives 0 errors; the pull head alone1eabeca9abbbgives 10 errors, none of themTS2540; the merge of the two gives exactly 8, allTS2540: Cannot assign to 'isDirty' because it is a read-only propertyatsrc/integrations/editor/__tests__/DiffViewProvider.spec.tslines 1414, 1447, 1475, 1500, 1548, 1674, 1745 and 1805.Mechanism, measured rather than assumed: upstream rewrote that spec to build its doubles with the typed helpers in
src/test-utils/vscode.ts(makeTextDocument/makeTextEditor, whose returns areas unknown as vscode.TextDocument/TextEditor), andvscode.TextDocument.isDirtyis read-only. The pull'sisDirtyassignments survive the merge, and eight of them bind to that read-only property. U6a repairs them with a locally widened structural type --type MutableTextDocument = Omit<vscode.TextDocument, "isDirty"> & { isDirty: boolean }plus a typed accessor -- which is assignment-compatible without a cast, so noas unknown asand noanyis added. That repair is new content in U6a and is listed as such: it is not in the pinned content source.This is not sold as a CI blocker. In the posture read above, the only FAILURE at this head is
webview-visual(check run 122373017189, 2026-10-27T18:13:47Z -> 2026-10-27T18:17:38Z), which is one of the advisory legs;compile(122373016641),knip(122373015993),e2e-mock(122373016536),platform-unit-teston ubuntu and windows (122373016581, 122373016878),Build test VSIX(122373016513) anddependency-review(122373016496) are COMPLETED/SUCCESS, as are bothmutation-diffrows (122373016368, 128583563671). A red compile leg is not what CI reported for this pull today; the eight errors are real at the shape this re-cut lands on, and they are repaired because U6a is rebased onto today's main.Cross-unit overlap I am naming instead of hiding
1eabeca9abbb's history containse3c10401f760(the head this issue declares as U3's) andd172c95ae3(U4's), soBaseTool.ts(14 lines / 5 selectors / 9 instrumented mutants) andClineProvider.ts(1 / 1 / 1) ride in fix(editor): release abandoned diff streams and owned placeholders (split 5/6 of #1066, unit U6a) #1932's cumulative diff exactly as this issue's scope re-cut says they do. Those 10 mutants are the whole excess over the cap: the unit's own two files come to exactly 400 instrumented mutants of the 410.ls-remoteof the fork at 2026-11-12T01:32:28Z:BaseTool.tsat fix(editor): release abandoned diff streams and owned placeholders (split 5/6 of #1066, unit U6a) #1932's head is blob827011fba0d5and U7 fix(write-to-file): clean partial state on missing-param and rooignore denial + integrate the #1066 split series (6/6 + FINAL) #1928's branch head94f0925066a4carries the identical blob, whileClineProvider.tsat fix(editor): release abandoned diff streams and owned placeholders (split 5/6 of #1066, unit U6a) #1932's head is blobd4db11ff5dfeand every sibling unit branch head carriesaa4cab38ed43-- that one line is unique to this unit and would be lost if it were simply dropped rather than moved.mainis cumulative while U6a is open, exactly like every other unit in this chain: its GitHub-visible diff prices at 450 lines / 410 instrumented mutants, so its advisorymutation-diffrow will read red until U6a merges, after which its own delta is 243 / 227. Both numbers are my measurements, not CI's.1eabeca9abbb. The tag this issue cites as the content source of record covers the original two-file unit; the file list grew to nine paths through review rounds, so the re-cut is priced against the head rather than the tag. Sanctioned new content: the eight-site type repair in U6a's spec, listed above.DiffViewProvider.tsblob6c23c73efa66andWriteToFileTool.tsblobbc34047bf0d2, both different from this unit's. I record that rather than act on it.Meter and limits for this plan post
This comment is one REST core (POST issue comment). Reading this issue, PR #1932 and #1989 was three unauthenticated GETs on the 60/hour unauthenticated pool, which draws no campaign token. All shape work was local git plumbing (
ls-remote,merge-tree,commit-tree,rev-list,cat-file,show,grep) plus local Stryker dry runs and local tsc, none of which is metered. Nothing is pushed, opened, retitled or reassigned by this comment, and no CI result is claimed.fix(editor): release abandoned diff streams and owned placeholders (s…
fix(write-to-file): tear down per-task stream state on parse failure …
fix(write-to-file): tear down per-task stream state on parse failure …
fix(editor): release abandoned diff streams and owned placeholders (s…
fix(write-to-file): tear down per-task stream state on parse failure …
fix(editor): release abandoned diff streams and owned placeholders (s…
fix(write-to-file): tear down per-task stream state on parse failure …
fix(editor): release abandoned diff streams and owned placeholders (s…
fix(write-to-file): tear down per-task stream state on parse failure …
fix(editor): release abandoned diff streams and owned placeholders (s…