fix: preserve files during fuzzy edits (#51) - #96
Conversation
🦋 Changeset detectedLatest commit: 9c21426 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Thanks for your interest in Cloudflare Computer. This repository does not accept unsolicited pull requests. Please use one of the accepted contribution paths instead:
If a maintainer asked you to open this pull request, they can add the |
|
Thanks for your interest in Cloudflare Computer. This repository does not accept unsolicited pull requests. Please use one of the accepted contribution paths instead:
If a maintainer asked you to open this pull request, they can add the |
commit: |
|
@agent-think can you resolve the outstanding PR feedback. |
Build Unicode source maps only for lines that contain candidate fuzzy boundaries. NFKC-stable lines map directly, avoiding per-grapheme work for large files while preserving fail-closed handling of ambiguous normalization.
| while (fuzzyIndex !== -1) { | ||
| originalStart = mapFuzzyBoundary(fuzzyContent, fuzzyIndex, "start"); | ||
| originalEnd = mapFuzzyBoundary(fuzzyContent, fuzzyIndex + fuzzyOldText.length, "end"); | ||
| normalizedSpan = | ||
| originalStart === undefined || originalEnd === undefined | ||
| ? undefined | ||
| : normalizeForFuzzyMatch(content.slice(originalStart, originalEnd)); | ||
| if (normalizedSpan === fuzzyOldText) break; | ||
| originalStart = undefined; | ||
| originalEnd = undefined; | ||
| normalizedSpan = undefined; | ||
| fuzzyIndex = fuzzyContent.text.indexOf(fuzzyOldText, fuzzyIndex + 1); | ||
| } |
There was a problem hiding this comment.
🟡 Editing a file full of special characters can freeze the request for minutes
Every candidate location for a loose text match is re-scanned from the start of the file (fuzzyContent.text.indexOf(fuzzyOldText, fuzzyIndex + 1) at packages/computer/src/tools/fs/edit-diff.ts:294) with a full line walk per candidate, so a file with many similar special-character spots takes time that grows with the square of the file size.
Impact: One edit call on such a file blocks the worker for minutes before it finally returns an error.
Retry loop over unmappable occurrences is quadratic
findText walks every normalized occurrence of the search text until one maps back cleanly to the original text. Each iteration calls mapFuzzyBoundary twice, and getTextBoundary (packages/computer/src/tools/fs/edit-diff.ts:119-129) scans the whole line array from index 0, so cost is O(occurrences x lines).
When no occurrence maps cleanly (for example a file of lines containing the ligature fi and oldText: "f"), the loop runs to exhaustion. Measured with the compiled module: 20,000 lines takes ~0.6 s, 60,000 lines (~240 KB) takes ~4.3 s; the edit tool's default cap is 2 MiB, which extrapolates to minutes of synchronous work inside the isolate before unsafeFuzzyBoundary is thrown.
Uniqueness is checked only after findText returns (packages/computer/src/tools/fs/edit-diff.ts:428-434), so the duplicate guard does not bound this loop. Tracking the current line index while scanning forward (occurrences are found in increasing order), or bailing out after a small number of unmappable candidates, would remove the quadratic term.
Prompt for agents
In packages/computer/src/tools/fs/edit-diff.ts, findText loops over every normalized occurrence of the search text looking for one whose boundaries map back to the original content. Each iteration maps two boundaries, and mapFuzzyBoundary calls getTextBoundary, which linearly scans the normalized line array from the first line. For content where no occurrence maps cleanly (for example many lines containing an NFKC-expanding character such as the ligature, with a one-character oldText), the loop visits every occurrence and each visit costs O(lines), giving quadratic behavior: roughly 4.3 s for a 240 KB file and far worse near the 2 MiB edit cap, all synchronous inside the Durable Object isolate. Note the duplicate/uniqueness check runs only after findText returns, so it does not bound this work. Consider making boundary lookup cheap (occurrences are returned in increasing offset order, so the line index can be advanced incrementally, or line starts can be binary-searched), and/or capping the number of unmappable candidates tried before failing closed.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Confirmed on |
Requested by @aron-cf
Closes #51
What was wrong
applyEditsToNormalizedContentinpackages/computer/src/tools/fs/edit-diff.tsused the fully fuzzy-normalized file asbaseContentwhenever any exact match missed. It then both wrote and diffed that normalized copy. As reproduced in #51, one trailing space inoldTexttherefore stripped unrelated trailing spaces and folded smart punctuation across the file while the returned diff hid those changes.The first source-mapping implementation also grapheme-segmented the full file before every fuzzy edit. A file near the 2 MiB edit limit could therefore create millions of short-lived strings inside a Durable Object isolate even when the match touched one small line.
What changed
Fuzzy matching now maps normalized match boundaries back to offsets in the original text and always splices replacements into that original content. Exact and fuzzy edits can share a batch, and uniqueness and overlap checks still run in the matching space. The diff and patch compare the actual original and written content.
Unicode source maps are now built lazily for candidate lines. Lines whose NFKC form is unchanged map directly without grapheme segmentation; lines that need a source map validate only the prefix needed for each boundary and cache the result. Ambiguous boundaries still fail closed.
The package README and tool-interface guide document the fuzzy lookup and byte-preservation contract, and the existing patch changeset covers the user-facing fix.
Testing
Workspace/createAIToolsread-back regression that also verifies the returned diff and patch.npm run check— passed.npm run typecheck --workspace @cloudflare/computer— passed.npm test --workspace @cloudflare/computer— all five suites passed: 1,025 unit, 6 proxy, 5 Worker backend, 23 script runner, and 4 stub-soak tests.npm run build --workspace @cloudflare/computer— passed.Demo
Demo URL (expires after 60 mins): https://computer-fuzzy-edit-feedback-96.halved-naranja.workers.dev
Open it and press Run fuzzy edits. It verifies byte preservation and a fuzzy edit after a 256 KiB unrelated line, and it runs the packed
@cloudflare/computerbuild from this branch. The/demoendpoint was also verified directly withpassed: true.🤖 generated by the pr-agent — please review carefully