feat(fs_write): fuzzy str_replace with 3-strategy fallback chain#3725
feat(fs_write): fuzzy str_replace with 3-strategy fallback chain#3725sandikodev wants to merge 1 commit intoaws:mainfrom
Conversation
a6fe223 to
cff84ae
Compare
cff84ae to
e7a39c2
Compare
Analysis & Evolution of This PRThis PR went through several rounds of analysis and refinement. Here is the full chronological record. Root Cause AnalysisThe failure mode we investigated: The pipeline from model to No preprocessing anywhere in this pipeline. The model builds What Was ImplementedStrategy 1: Exact match — unchanged, fast path. Strategy 2: Line-trimmed match — compares lines after Strategy 3: Block-anchor match — uses first+last line as anchors, scores middle lines with Levenshtein similarity (O(n) space), picks the best candidate above a 0.6 threshold. Handles minor edits in surrounding context lines. File freshness check — Empty Tool description updated — Bugs Found and Fixed During Code Review
Relationship to PR #3724PR #3724 added read-before-write guidance to the tool description and improved the error message. This PR supersedes it — all of that is included here plus the actual fuzzy implementation that makes the guidance accurate. |
…le freshness check The current str_replace implementation uses exact byte matching only. When the model's old_str has minor differences from the file (indentation drift, whitespace, or small context edits), the match fails and the model either retries wastefully or falls back to destructive shell commands. Implement str_replace_fuzzy() with a 3-strategy fallback chain inspired by opencode and cline's diff-apply approaches: 1. Exact match — unchanged behaviour for the common case 2. Line-trimmed match — compares lines after trim(), then replaces using byte offsets (prefix-sum table) into the original content. Handles indentation drift (tab vs spaces, different indent levels). 3. Block-anchor match — uses first+last line as anchors, scores middle lines with Levenshtein similarity, picks the best candidate above a 0.6 threshold. Handles minor edits in surrounding context lines. Also adds file freshness checking: - fs_read now records the file mtime into FileLineTracker.last_read_mtime whenever it reads a file - fs_write str_replace checks the current mtime against the recorded value before writing; if the file was modified externally it returns a clear error asking the model to re-read before retrying Also: - validate() rejects empty old_str before reaching fuzzy matching - tool_index.json description updated to reflect fuzzy tolerance and reinforce read-before-write / no-sed-fallback guidance Key correctness properties: - Strategies 2 and 3 return byte ranges — replacement is always at the correct position even if matched text appears elsewhere in the file - block_anchor_match skips first==last anchors (false positive guard) - similarity_score respects actual content window bounds - levenshtein uses O(n) rolling-row space, char count for denominator - build_line_offsets prefix-sum gives O(1) offset lookup - strip_empty_boundary_lines handles both leading and trailing empty lines 11 tests cover all strategies, edge cases, and error messages.
e7a39c2 to
fc482ca
Compare
Problem
The current
str_replaceimplementation uses exact byte matching only. When the model'sold_strhas minor differences from the actual file content — indentation drift, whitespace normalization, or small edits in surrounding context — the match fails with:The model then either:
old_str(wastes tokens)sed -ior other shell commands (dangerous — no preview, no validation, can corrupt files)This is a systematic productivity killer: every failed
str_replacecosts tokens, breaks flow, and risks file corruption.Root Cause Analysis
Compared to opencode and cline, Kiro CLI's
str_replacehas no fallback strategies. Both tools implement multi-strategy matching to handle the inevitable gap between model output and exact file content.Solution
Implement
str_replace_fuzzy()with a 3-strategy fallback chain:Strategy 1: Exact match (unchanged)
Fast path — if
old_strmatches exactly, use it directly.Strategy 2: Line-trimmed match
Compare lines after
trim(), then replace the original (indented) text. Handles the most common failure mode: indentation drift between model output and actual file.Strategy 3: Block-anchor match (Levenshtein similarity)
Use first+last line as anchors, score middle lines with Levenshtein distance, pick the best candidate above a 0.6 similarity threshold. Handles minor edits in surrounding context lines.
All strategies are unambiguous-only: if multiple candidates score equally, the match is rejected and a clear error is returned.
Testing
8 new tests cover:
fs_read, warns againstsed)By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.