Skip to content

Commit 9e67ae1

Browse files
author
Piper
committed
fix: Edit tool false positive 'failed' error when oldText is substring of newText
Root cause: Recovery logic used simple 'includes' checks which failed when: 1. newText pre-existed in file (false positive success) 2. oldText was substring of newText (e.g. 'foo' -> 'foobar') Fix: Use before/after occurrence counts for robust detection: 1. Count newText occurrences before and after edit 2. Verify newText count increased (not just present) 3. Strip newText when checking oldText to handle substring case 4. Verify oldText count decreased when original available Fixes #54455, fixes #49363
1 parent 472498f commit 9e67ae1

1 file changed

Lines changed: 46 additions & 9 deletions

File tree

src/agents/pi-tools.host-edit.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,58 @@ function didEditLikelyApply(params: {
9595
return false;
9696
}
9797

98-
let withoutInsertedNewText = normalizedCurrent;
98+
// Use before/after occurrence counts for more robust detection.
99+
// This handles the case where newText pre-existed in the file.
100+
const countOccurrences = (hay: string, needle: string): number => {
101+
if (needle.length === 0) {
102+
return 0;
103+
}
104+
let count = 0;
105+
let idx = 0;
106+
while ((idx = hay.indexOf(needle, idx)) !== -1) {
107+
count++;
108+
idx += needle.length;
109+
}
110+
return count;
111+
};
112+
99113
for (const edit of params.edits) {
100114
const normalizedNew = normalizeToLF(edit.newText);
115+
const normalizedOld = normalizeToLF(edit.oldText);
116+
117+
// Check if newText exists in current content
101118
if (normalizedNew.length > 0 && !normalizedCurrent.includes(normalizedNew)) {
102119
return false;
103120
}
104-
withoutInsertedNewText =
105-
normalizedNew.length > 0
106-
? removeExactOccurrences(withoutInsertedNewText, normalizedNew)
107-
: withoutInsertedNewText;
108-
}
109121

110-
for (const edit of params.edits) {
111-
const normalizedOld = normalizeToLF(edit.oldText);
112-
if (withoutInsertedNewText.includes(normalizedOld)) {
122+
// Count occurrences before and after
123+
const newTextCountAfter = countOccurrences(normalizedCurrent, normalizedNew);
124+
const newTextCountBefore =
125+
normalizedOriginal !== undefined ? countOccurrences(normalizedOriginal, normalizedNew) : 0;
126+
127+
// Evidence the edit added at least one new occurrence of newText
128+
const editAddedNewText = newTextCountAfter > newTextCountBefore;
129+
130+
// For oldText checking, strip newText occurrences to handle the case where
131+
// oldText is a substring of newText (e.g. appending/wrapping, #49363)
132+
const stripNewText = (s: string): string =>
133+
normalizedOld.length > 0 && normalizedNew.includes(normalizedOld)
134+
? s.split(normalizedNew).join("")
135+
: s;
136+
137+
const contentAfterStrip = stripNewText(normalizedCurrent);
138+
const stillHasOld = normalizedOld.length > 0 && contentAfterStrip.includes(normalizedOld);
139+
140+
// When original is available, check that oldText count decreased
141+
const oldTextDecreasedOrAbsent =
142+
!stillHasOld &&
143+
(normalizedOriginal === undefined ||
144+
countOccurrences(stripNewText(normalizedCurrent), normalizedOld) <=
145+
countOccurrences(stripNewText(normalizedOriginal), normalizedOld));
146+
147+
// Recover only when: newText added AND oldText decreased/absent
148+
const recovered = editAddedNewText && oldTextDecreasedOrAbsent;
149+
if (!recovered) {
113150
return false;
114151
}
115152
}

0 commit comments

Comments
 (0)