Skip to content

Commit 884e825

Browse files
icecrasher321claude
andcommitted
fix(file): credit runs across a repeat, count matching lines not matches
The tool promised "each match" while the query is distinct on file and line, so several matches on one line return one row. An agent reading the contract would have expected otherwise; it now says each matching line once. A repetition of a non-fixed atom was scored at what one copy guarantees, but from two copies on its own tail and head meet: every match of `(?:a(?:x|y)bc){2}` contains `bca`, which neither copy contains alone. That run is now credited, so patterns the index can serve are no longer rejected. Scores are capped alongside the strings they measure. Joining two capped strings yields twice the cap, which `concatenate` could already exceed — the gate never noticed, since it only compares against three, but the bound is documented and now holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a0ab9a5 commit 884e825

6 files changed

Lines changed: 32 additions & 7 deletions

File tree

apps/docs/content/docs/integrations/file.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Extract the text content of one or more workspace files from selected file objec
7070

7171
### File Search
7272

73-
Search every active workspace file for lines matching a regular expression, and return each match with its file ID and line number.
73+
Search every active workspace file for lines matching a regular expression, and return each matching line once with its file ID and line number.
7474

7575
#### Input
7676

apps/sim/blocks/blocks/file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
908908
- Get Content is how you read file text. It accepts file objects or canonical file IDs and returns a "contents" array with one extracted text string per file (PDF, DOCX, CSV, etc. are parsed automatically).
909909
- To read the text of files produced by another block, chain into Get Content: set its file input to the upstream file output, e.g. <file.files>, <agent.files>, or <start.files>. Never assume Read (or any file-object output) already contains the text.
910910
- Get Content's "contents" can be large; it is persisted through the execution large-value system automatically, so prefer it over inlining file text any other way.
911-
- Search finds text across all active workspace files and returns structured results with fileId, lineNumber, and text. Queries are case-insensitive until they contain an uppercase letter being searched for; in a regular expression, uppercase inside an escape or character class such as \\D or [A-Z] does not affect this.
911+
- Search finds text across all active workspace files and returns one result per matching line — not per match — with fileId, lineNumber, and text. Queries are case-insensitive until they contain an uppercase letter being searched for; in a regular expression, uppercase inside an escape or character class such as \\D or [A-Z] does not affect this.
912912
- Search reads the query as a line-oriented regular expression: quantifiers, character classes, \\d \\w \\s, alternation, groups, "^" and "$" anchors, and \\b word boundaries. Lookaround, backreferences and patterns spanning a line break are not supported, and a pattern needs at least 3 consecutive literal characters that every match will contain. Set Match to "Exact match" to search for the query text verbatim instead.
913913
- Match is a builder setting, not an agent one: the agent writes the query, and Match decides how every query from that block is read.
914914
- Search is eventually consistent. Check "complete" and "indexStatus" when pending, failed, skipped, or partially indexed files matter to the task.

apps/sim/lib/workspace-files/search/regex.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ describe('analyzeFileSearchRegex', () => {
3232
expect(run('ab{3}cd')).toBe(6)
3333
})
3434

35+
/**
36+
* `(?:a(?:x|y)bc){2}` matches `axbcaybc`, whose `bca` spans the join between
37+
* the two copies — a run neither copy contains on its own.
38+
*/
39+
it('credits the run a repetition creates where its copies meet', () => {
40+
expect(run('(?:a(?:x|y)bc){2}')).toBe(3)
41+
expect(run('(?:ab(?:x|y)c){3}')).toBe(3)
42+
expect(run('(?:a(?:x|y)bc){1}')).toBe(2)
43+
})
44+
3545
/** `(?:ab){2,5}` cannot match without `abab` in it, so the run is 4, not 2. */
3646
it('credits a variable repeat with the copies its minimum forces', () => {
3747
expect(run('(?:ab){2,5}')).toBe(4)

apps/sim/lib/workspace-files/search/regex.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ function runLength(text: string): number {
9090
return [...text].length
9191
}
9292

93+
/**
94+
* A join of two capped strings is twice the cap, so scores are capped as well —
95+
* it keeps `best` inside the same bound the strings are held to, and a run
96+
* longer than the cap is still a run longer than the gate it is compared to.
97+
*/
98+
function boundedRun(text: string): number {
99+
return Math.min(FILE_SEARCH_PATTERN_LITERAL_CAP, runLength(text))
100+
}
101+
93102
function head(text: string): string {
94103
return text.length > FILE_SEARCH_PATTERN_LITERAL_CAP
95104
? text.slice(0, FILE_SEARCH_PATTERN_LITERAL_CAP)
@@ -123,7 +132,7 @@ function concatenate(left: LiteralGuarantee, right: LiteralGuarantee): LiteralGu
123132
exact: left.exact !== null && right.exact !== null ? head(left.exact + right.exact) : null,
124133
prefix: head(left.exact !== null ? left.exact + right.prefix : left.prefix),
125134
suffix: tail(right.exact !== null ? left.suffix + right.exact : right.suffix),
126-
best: Math.max(left.best, right.best, runLength(joined)),
135+
best: Math.max(left.best, right.best, boundedRun(joined)),
127136
zeroWidth: left.zeroWidth && right.zeroWidth,
128137
}
129138
}
@@ -189,11 +198,17 @@ function repeat(atom: LiteralGuarantee, min: number, max: number): LiteralGuaran
189198
zeroWidth: false,
190199
}
191200
}
201+
/**
202+
* An atom with no fixed string still repeats back to back, so from two copies
203+
* on, its own tail and head meet: every match of `(?:a(?:x|y)bc){2}` contains
204+
* `bca`, which neither copy contains alone.
205+
*/
206+
const acrossCopies = min >= 2 ? boundedRun(tail(atom.suffix) + head(atom.prefix)) : 0
192207
return {
193208
exact: null,
194209
prefix: atom.prefix,
195210
suffix: atom.suffix,
196-
best: atom.best,
211+
best: Math.max(atom.best, acrossCopies),
197212
zeroWidth: atom.zeroWidth,
198213
}
199214
}

apps/sim/tools/file/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ interface FileSearchResponse extends ToolResponse {
2121
*/
2222
const TOOL_DESCRIPTIONS: Record<FileSearchMode, string> = {
2323
regex:
24-
'Search every active workspace file for lines matching a regular expression, and return each match with its file ID and line number.',
24+
'Search every active workspace file for lines matching a regular expression, and return each matching line once with its file ID and line number.',
2525
exact:
26-
'Search every active workspace file for lines containing an exact piece of text, and return each match with its file ID and line number.',
26+
'Search every active workspace file for lines containing an exact piece of text, and return each matching line once with its file ID and line number.',
2727
}
2828

2929
const QUERY_DESCRIPTIONS: Record<FileSearchMode, string> = {

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)