Repository navigation
Conversation
li-xiu-qi
requested review from
MelodyVAR,
Uking-xxx,
ZouR-Ma and
liuedcson
as code owners
October 20, 2026 15:46
…dered text search_files could return the wrong source text when a matched line itself contains a :<digits>: shape. The native grep tool resolves path, line and text as structured values, then the tool boundary flattens them into "path:line: text" and the Step adapter parses that string back into a row. Its greedy path group re-read the :<digits>: shape inside the matched text as the line number and reloaded that line, or reported "(no matches)" when the false line number was past EOF. Tuning that regex only moves the ambiguity to paths, since both fields may contain the delimiter. So the re-parse is removed instead: - grep reports the rows it already resolved in details.matchRows (details are extension metadata and are not sent to the model). - search_files reads those rows and only falls back to parsing rendered text for custom/remote search operations that emit plain text. - the git and POSIX grep backends request NUL-separated output (-z / -Z) so their own parse carries the field boundary. git grep -z escapes ambiguous path bytes into the private use area (0xF000 + byte), decoded back here; grep -Z is GNU-only, so it is gated and the colon form stays supported. Both directions are now correct: matched text containing :<digits>:, and a path containing it.
li-xiu-qi
force-pushed
the
fix/search-files-greedy-line-parse
branch
from
October 20, 2026 17:19
34515bc to
570e018
Compare
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.
Fixes #186
Problem
search_filescan display the wrong source line when a matched line contains a:<digits>:shape. That shape is not an artificial edge case: a line thatechoes a compiler error (
src/app.ts:42: error), a log prefix (12:30:45: retrying), or a line that quotes grep output all contain it, so a single buildlog is enough to hit this.
What the layers know. The grep backends resolve every match exactly — the
file, the line number, and the line text come out of the backend as separate
values. What reaches the Step adapter is not that tuple but a rendered string,
path:line: text.parseNativeSearchRowsinstep/tool-profile.tsthenre-derives the tuple from that string with a greedy
^(.*):(\d+): (.*)$.What goes wrong. Take a 45-line log named
many.logwhose line 1 isliterally
src/example.ts:42: build failed, and search forbuild failed.Line 1 is the only match, so the only correct output is line 1's own text. The
rendered row is:
A greedy
(.*)takes as much as it can, so the split lands on the last:<digits>:in the row, not the real one:many.log:1: src/example.ts42build failedrenderSearchBlocksthen loads line 42 ofmany.logand prints that text as ifline 1 had matched — the reader gets content that never matched. When the false
line number is past EOF, there is nothing to load, the row is dropped, and the
tool reports
(no matches)for a match the backend did find. Those are twodifferent failures: wrong text shown, or the match lost entirely.
Why the string round-trip is the defect. Tightening the regex is not a fix.
The path and the matched text can both contain the delimiter, so any
regex-based split only decides which of the two gets misread; changing the
greediness moves the bug from one field to the other. The tuple is already known
one layer below. Flattening it to text and parsing it back is the step that can
fail, and it is the step that should not exist.
Changes
The row tuple stops being serialized and parsed. It crosses the tool boundary as
data, and the grep backends stop guessing where the path ends. Three places, and
each one is required.
packages/coding-agent/src/core/tools/grep.ts— the answer leaves as data.GrepToolDetailsgainsmatchRows?: GrepMatchRow[](path/line/text), filled in the same loop that renders the text, so the two viewscannot disagree.
detailsis local extension metadata and is not sent to themodel, so model-visible output is unchanged.
git grepgets-zand the POSIX backend gets-Zwhere GNU grep supportsit, so the backend's own field boundary is a NUL byte instead of a guess.
parseGrepLinedecodespath\0line\0textandpath\0line:text, and reversesthe private use area escaping
git grep -zapplies to ambiguous path bytes(a
:in a path arrives as\uF03A). The colon form is still accepted forBSD grep and custom backends.
packages/coding-agent/src/step/tool-profile.ts— the answer is read, notre-derived.
searchRowsFromDetailsreadsdetails.matchRows;executeSearchFilesuses itfirst and falls back to
parseNativeSearchRowsonly when a search operationreports no structured rows (custom / remote backends that emit plain text).
detailskeepsmatchesas the rendered match count (existing contract) andadds
matchRowsin the same shape, so a consumer has one format to read.packages/coding-agent/test/step-tool-profile.test.ts:<digits>:renders the real line (the reported case).:<digits>:renders the real line (skipped on Windows, where:cannot appear in a filename).details.matchRowscarries the resolved row, so consumers never parse text.Why all three. Structured rows alone would not help while the git/POSIX
backends still split
path:line:texton the colon — a file namedreport:7:notes.txtis already wrong before the row reachessearch_files.NUL output alone would not help the Step adapter, which would still parse the
rendered string. A regex change alone only moves the ambiguity. Together, no
step between the backend and the renderer has to guess.
Behavior and compatibility
parseNativeSearchRowsis module-private, and the fallback keeps working forbackends that only emit text.
GrepToolDetails.matchRowsis a new optional field;matches,backend,truncation,filesMatchedandtruncatedkeep their meaning.sessions, or provider responses.
grep -Zis skipped on platforms whose grep does not support it (BSD /macOS), where the colon form is used as before.
Validation
Repro. The two logs from Problem:
many.log(45 lines, line 1 issrc/example.ts:42: build failed) andshort.log(2 lines, line 1 issrc/example.ts:99: build failed). Line 1 is the only match in both, so theexpected output is line 1's own text.
Before (main @ be88ee7) — one search, both symptoms:
After (this branch) — both rows carry the text that matched, and the past-EOF
case returns a row instead of an empty result:
No rendered row is parsed on this path any more, so nothing is left to mis-split.
Checks.
row payload.
vitest run test/step-tool-profile.test.ts test/grep-fallback.test.ts—38/38 pass.
npm run check— green (biome, tsgo with zero errors, repo check scripts).git grep -n -zemits threeNUL-terminated fields —
many.log,1,src/example.ts:42: build failed—and
grep -r -n -Zemits./weird:42:.txtNUL2:hit here; both parse to theright row, with the colon inside the path or the text intact.
:in filenames.Limitations
path) resolve correctly.
grep(macOS) has no-Z, so that backend keeps the colon form there;the git backend covers the same case on those platforms via
-z.