fix(tools): patch idempotency guard prevents duplicate-content loops on re-edit (#18426) - #56570
fix(tools): patch idempotency guard prevents duplicate-content loops on re-edit (#18426)#56570Tranquil-Flow wants to merge 2 commits into
Conversation
…on re-edit (NousResearch#18426) Re-applying the same old_string/new_string after the first patch already landed left old_string absent from the file. The context_aware / block_anchor fuzzy strategies then matched the already-modified region and substituted new_string over a partial slice, corrupting the file by duplicating trailing lines (NousResearch#18426). Add an idempotency guard: when old_string is no longer present but new_string already is, fail cleanly with a re-read hint instead of falling through to the fuzzy strategies. The guard does not fire for legitimate fuzzy first-patches (new_string is absent until the change lands) or deletions (empty new_string).
Competing fix for #18426. #18614 (open, earlier) adds an idempotency guard by checking whether the matched region already equals |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating a real retry-corruption path. Current main still reproduces it: context_aware accepts the changed retry candidate at tools/fuzzy_match.py:696-728, and fuzzy_find_and_replace applies that span at tools/fuzzy_match.py:85-153.
Problems
- The new whole-content condition at
tools/fuzzy_match.py:85in this PR is too broad. A valid first fuzzy edit is rejected whennew_stringappears in a separate section of the file. On current main, a config with productionstatus = enabledand whitespace-drifted stagingstatus = disabledsuccessfully updates staging throughwhitespace_normalized; this PR's predicate is true before matching. - Coverage is helper-only. The mutation boundary is
ShellFileOperations.patch_replaceattools/file_operations.py:1593-1606, so the no-write-on-retry guarantee is not exercised through file I/O.
Suggested changes
- Scope the guard to the selected candidate rather than whole-file
new_stringpresence, and add the unrelated-occurrence regression. - Add a live
patch_replaceretry test asserting both an error and unchanged on-disk content.
Automated hermes-sweeper review.
| # (skips this check); a legitimate *fuzzy* first patch has ``old_string`` | ||
| # absent but ``new_string`` also absent (the change has not landed yet), so | ||
| # the check does not fire and the strategy chain proceeds normally. | ||
| # ``new_string`` must be non-empty so a deletion (empty replacement) does |
There was a problem hiding this comment.
This whole-file predicate rejects a valid first fuzzy edit when new_string already occurs elsewhere. For example, production may already contain status = enabled while a whitespace-drifted staging status = disabled should be changed to the same value; current main matches staging through whitespace_normalized, but this condition returns early. Please scope the check to the selected candidate rather than global content presence.
…Research#56570) Sweeper feedback: guard was checking whole-file new_string presence, which blocked legitimate fuzzy matches when new_string appeared in unrelated parts of the file. Now uses first-line overlap heuristic: only blocks when old/new first lines co-occur or new first line is present with sufficient length. Added unrelated-occurrence regression test + retry test.
What
Re-applying the same
old_string/new_stringafter the firstpatchalready landed leftold_stringabsent from the file. Thecontext_aware/block_anchorfuzzy strategies then matched the already-modified region and substitutednew_stringover a partial slice, corrupting the file by duplicating trailing lines. Repeated re-application could multiply a section 2–4× and the agent would loop on apparent success until interrupted manually.Fix
Add an idempotency guard at the top of
fuzzy_find_and_replace()(tools/fuzzy_match.py): whenold_stringis no longer present in the file butnew_stringalready is, fail cleanly with a re-read hint instead of falling through to the fuzzy strategy chain.Why this is safe (no false positives on legitimate edits)
old_stringis present → guard skipped, exact match proceeds.old_stringis absent butnew_stringis also absent (the change has not landed yet) → guard skipped, fuzzy proceeds normally.new_string=""): thenew_string andshort-circuit prevents the vacuous"" in contenttest from firing.new_stringis re-indented on write, so the rawnew_string in contentcheck isFalse→ guard skipped; the existing "Found 2 matches" uniqueness guard catches it.Verification
main: apply old→new once (exact, succeeds), re-apply the same old→new →context_aware(50% line-similarity) matched the modified region and duplicated the trailingreturn resultline (count 1 → 2).test_fuzzy_match.py+test_file_operations.py+test_skill_manager_tool.py).main(0 behind / 1 ahead).Tests
New
TestIdempotencyGuardclass intests/tools/test_fuzzy_match.py(6 tests, all call the realfuzzy_find_and_replaceproduction path):test_repatch_after_apply_does_not_duplicate— the headline reprotest_already_applied_error_guides_reread— message guides a re-readtest_legitimate_fuzzy_match_with_new_absent_still_works— regression guardtest_replace_all_already_applied_errors_cleanly—replace_allpathtest_old_absent_new_absent_still_fuzzy_matches— fuzzy still works when neither presenttest_deletion_with_empty_new_does_not_trip_guard— emptynew_stringedge caseCloses #18426.
*Auto-published by Moonsong via Path B automated pipeline.