Repository navigation
fix(tools): preserve unmatched text in partial-line legacy edits - #1420
Merged
Merged
Conversation
The legacy Edit shape (`old_string` / `new_string` without `tag` / `ops`)
located the unique match by byte offset but then emitted a whole-line
`PUT first.=last:` with `new_string` as the body. `PUT N.=M` replaces whole
lines, so when `old_string` matched only part of a line the unmatched prefix
of the first line and suffix of the last line were silently deleted while the
tool still reported success (`let x = foo;` with `= foo;` -> `= bar;` became
`= bar;`). Counting `old_string.split('\n')` lines also overshot by one when
`old_string` ended in a newline, so deleting `foo\n` wiped the next line too.
Build the exact substring result instead, trim the unchanged leading and
trailing lines, and lower the difference to a single line-anchored op
(`PUT a.=b:`, `CUT a.=b`, or a `PUT >N:` insertion) that still goes through
`hashline::apply_edit` with the live tag. Only changed lines are anchored, so
the provenance gate sees exactly what changed. Exact whole-line matches produce
the same file as before; an identical replacement still fails with
`EDIT_NO_CHANGE`, and not-found / ambiguous matches still fail with
`EDIT_LEGACY_MATCH_FAILED`.
Refs vastsa#1106
The line-anchored edit format preserves terminal newline state, so the legacy substring adapter cannot express a terminal-newline-only change. Pin the result in a regression test and make the compatibility contract explicit.
Owner
|
Thanks for the focused fix. I added a small landing completion for the terminal-newline-only edge case: the regression test now records the current no-change behavior, and the line-edit contract documents that limitation. The PR is merged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The legacy
Editshape (old_string/new_stringwithouttag/ops, adapted in #1269 for #1217) treated a substring match as a whole-line replacement. Whenold_stringcovered only part of a line, the rest of that line was silently deleted and the tool still returned success with a fresh tag. The file is corrupted and the model gets no signal:c1afefea7)let x = foo;= foo;→= bar;let x = bar;= bar;value = 1; // keep mevalue = 1;→value = 2;value = 2; // keep mevalue = 2;call(alpha, beta);alpha→gammacall(gamma, beta);gammafn a() { one();⏎ two(); } // endone();⏎ two();→uno();fn a() { uno(); } // enduno();a⏎foo⏎b⏎foo⏎→ ``a⏎b⏎a⏎⏎(neighbour linebdeleted)This PR covers only the legacy-edit sub-item of the #1106 umbrella (Agent Harness tool feedback); the other points in that issue are out of scope.
Root cause
tool_edit(crates/host-core/src/tools/mod.rs:1547-1555onc1afefea7) used the unique match offset only to compute the first and last touched line numbers, then emittedPUT first.=last:whose body wasnew_stringsplit on\n.PUT N.=Mreplaces whole lines (hashline/apply.rs:129-137), so the unmatched prefix of the first line and suffix of the last line were dropped. The line count came fromold.split('\n').count(), which overshoots by one whenold_stringends with a newline, so a whole-line delete such asfoo\n→ `` also wiped the following line.Change
legacy_replace_opsnext totool_edit:text[..start] + new_string + text[start + old.len()..];PUT a.=b:,CUT a.=b, or aPUT >N:/PUT <1:insertion.hashline::apply_editwith the live tag, so EOL, BOM, and trailing-newline handling, tag minting, and the provenance gate are the same code path as before. Only lines that actually change are anchored.EDIT_NO_CHANGEdirectly; the not-found and ambiguous paths (EDIT_LEGACY_MATCH_FAILED) are untouched.Production diff: +51 / −9 in
tools/mod.rs; the rest is tests and docs.Evidence
af701dea5(fix/legacy-edit-partial-line). Base main:c1afefea7(fix(mcp): bound the session/get compaction history before truncating #1418), an ancestor of HEAD;pnpm check:pr-basepasses.edit_legacy_replacement_preserves_unmatched_bytes(11 cases) plusedit_legacy_identical_replacement_leaves_file_unchanged:c1afefea7(see the table above, plus CRLF). The whole-line case 9 passes on both.cargo test -p host-core --locked: 757 passed, 0 failed (mainc1afefea7: 755 passed; +2 new tests). The existingedit_accepts_legacy_old_string_new_string_shapeis unchanged and passes.cargo fmt --check: clean.cargo clippy -p host-core --all-targets: no warnings.pi-desktop-host-corebinary over stdio. Each case runstools.executeRead followed by a legacy Edit in an agent session, and the file on disk is compared byte for byte:af701dea5): 7/7 byte-exact;c1afefea7): 1/7 (only the exact whole-line case);pnpm test:e2eonaf701dea5: 23/23 passed, 2 skipped (E2E-008-live-modelandE2E-009-streamneedPI_DESKTOP_TEST_API_KEY). The identical result on mainc1afefea7.HOMEand data dir. No Electron UI suite applies to this host-only change.pnpm docs:check: pass.git diff --check: clean.Specs / E2E docs
docs/spec/03-runtime/18-line-anchored-edit-contract.md§11: one paragraph stating the legacy compatibility semantics. A unique match is required, exactly the matched text is replaced, and the result is lowered to one op over the changed lines.docs/spec/06-delivery/04-e2e-test-plan.md: newE2E-EDIT-legacy-replacement-preserves-unmatched-text, placed after E2E-156 (Status: Automated via the host-core tests).Compatibility / risk
old_string/new_stringbranch changes. Thetag+opscontract, the parser, andapply_editare untouched. Exact whole-line legacy matches write the same file as before.opsechoed in the result now name only the lines that changed, which can be a narrower range than before.EDIT_NO_CHANGE; the ops path cannot express that change either.Refs #1106