Skip to content

Commit e5a8852

Browse files
feat(file): search workspace files by regular expression (#7370)
* feat(file): search workspace files by regular expression Search read its query as literal text. It now reads it as a line-oriented regular expression by default, with a Match setting on the block to go back to verbatim text. The segment store and its `gin_trgm_ops` index already support this: pg_trgm extracts trigrams from a regex source too, so `~` / `~*` plan as a bitmap index scan exactly like `LIKE` / `ILIKE`. No migration, no new index. One compiled pattern owns every mode-specific decision — how PostgreSQL matches a segment, whether the segment must hold a whole line, and where the match sits inside it — so the repository builds one query shape and the preview renderer one preview shape. Compilation happens in the application use case, not the route adapter, so every surface gets the same semantics. The supported syntax is the intersection of PostgreSQL ARE and JavaScript RegExp, because the same source drives both the indexed predicate and the client-side match location a preview centres on. Anything the two engines read differently is rejected by name rather than silently reinterpreted, and `\b` is rewritten to `\y` on the way to PostgreSQL. Safety, in four independent layers: - A pattern must contain 3 consecutive literal characters every match will include. pg_trgm indexes nothing shorter, and an unextractable pattern plans as a sequential scan across every workspace's segments. - `new RegExp` proves it compiles in JavaScript. - PostgreSQL proves it compiles in ARE; 2201B becomes a 400, not a 500. - `statement_timeout` bounds the read. This one covers exact matching too, which has always been able to reach the same scan through a punctuation-only or non-ASCII query. `mode` is a builder setting, withheld from the model like `maxResults`. The model cannot see it and the two readings disagree on every metacharacter, so `toolEnrichment` replaces the declared syntax with the active mode's — a regex sent to a block set to exact matching would otherwise be searched for verbatim and silently find nothing. Verified against PostgreSQL 17: 14 behavioural checks end to end, a live search over 150,012 segments in 14ms, 6/6 representative patterns reaching the trigram index, and the guard cutting a 12s pattern at 10.08s into an actionable message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(file): locate regex matches in PostgreSQL, never in JavaScript Preview rendering ran the user's compiled pattern with `RegExp.exec` to centre the excerpt on the match. `RegExp` matches by backtracking, and the literal-run gate admits nested quantifiers, so `(a+)+bcd` against a long segment cost 768ms at 40 leading `a`s and doubles with each one — synchronously, on the event loop, once per returned row, and entirely outside the statement timeout that bounds the query which found the row. PostgreSQL runs that same pattern in 0.49ms: its engine does not backtrack, and `regexp_instr` runs inside the read's transaction, so locating a match can never cost more than having found it. Regex mode now selects the match offsets alongside the row and `findMatchRange` returns null for it, which is the interface's contract rather than an omission. Exact mode is unchanged — scanning for a known string is linear. PostgreSQL counts characters where JavaScript slices by UTF-16 unit, so the offsets are converted by walking the segment rather than assuming either width. Also fixes two audit failures: `getErrorMessage` in place of a hand-written `instanceof Error` ternary, and regenerated tool metadata and integration docs for the search params. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(file): accept open-ended repeats, count characters, classify lock waits Three defects from review, none of which the tests caught: `{n,}` was rejected. `readQuantifierAt` reports an unbounded maximum as Infinity, and the repeat cap compared it directly, so every open-ended repeat failed as "exceeds 1000" — a form the tool's own documentation offers. Only a stated maximum is measured now, and the minimum always is, since that is what an expansion unrolls. Query bounds and literal runs were measured in UTF-16 units while claiming characters, so two astral characters read as four and slipped a gate written for three. Both now count characters, which is also what pg_trgm indexes. `lock_timeout` was set without classifying what it raises. A wait on conflicting DDL surfaced as an unclassified server error, and folding it in with the timeout arm would have told the caller to fix a pattern that is already correct. It now maps to a distinct error the caller is told to retry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(file): keep long previews honest, credit forced repeats, drop bad hints Four review findings, each reproduced before it was changed. A regex match has no length limit, so `abc.*` on a long line produces a match larger than the whole preview budget. The layout passed it through whole and let the final byte cap cut it, which removed the closing marker along with the text — 2048 bytes of output ending mid-line with nothing to say so. The match is now clipped against a budget that reserves that marker, and a clipped match always carries one. A variable repeat was scored at one occurrence when its minimum forces more: `(?:ab){2,5}` cannot match without `abab` in it, but the run was counted as 2 and the pattern rejected against a gate of 3. It now contributes the copies its minimum forces. `\Y`, `\m` and `\M` were rejected with a suggestion to write `\b`, `^` or `$`. Those are different assertions — a non-boundary, and two word edges rather than the line's — so the hint handed back different semantics as a fix. They now say no supported escape means the same thing. `\y`, `\A` and `\Z` keep theirs, which are genuine. Smart case was documented as reacting to any uppercase letter, but it reads literals only, so `\D` and `[A-Z]` do not make a search case-sensitive. The tool, block and generated docs now say what the code does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 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> * fix(file): reject overflowed repeat bounds, describe what search covers An upper bound too large for `Number` arrives as Infinity, and the exception that lets `{n,}` skip the repeat cap could not tell the two apart — so `needle{1,<400 digits>}` passed the cap that `needle{1,5000}` fails. The quantifier now records whether a bound was written at all, and a written one must be at or under the cap however large it is. The tool promised every active workspace file. It searches what the index currently holds: a file still pending, failed, or skipped as unsupported is not searched, and an agent reading "every file" would take an empty result as proof of absence. Both descriptions now say so and point at `complete` and `indexStatus`, which already carry the detail. The declared query description spoke only for regex mode, which is what the catalog and the generated docs render — so a builder using exact matching was told to write a regular expression and to obey a rule that does not apply to them. It now names both readings; the runtime schema is still enriched with whichever is in force. The docs overview said literal text, which stopped being true when regex became the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(file): close the allowlist around PostgreSQL bracket expressions `[:class:]` was rejected while `[=equivalence=]` and `[.collating.]` were forwarded unchanged. All three are PostgreSQL bracket expressions with no JavaScript counterpart, and the parser exists to admit only what both engines spell the same way — so two of them passed an allowlist whose whole point is to close, and were accepted by documentation that says POSIX classes are not supported. They are now rejected by the construct they open, each named in its own error. An ordinary class holding a literal dot, `[.]` or `[a.b]`, is untouched: the form only matches on a bracket nested inside a class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c43e842 commit e5a8852

19 files changed

Lines changed: 1803 additions & 218 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The File block is a built-in Sim block for working with files stored in the work
1616
With the File block, you can:
1717

1818
- **Read and extract content**: Load workspace file objects and extract their text content
19-
- **Search workspace content**: Find literal text across indexed active workspace files with bounded line-level results
19+
- **Search workspace content**: Match a regular expression, or an exact piece of text, against the indexed lines of active workspace files with bounded line-level results
2020
- **Fetch from URLs**: Retrieve and parse files from external URLs with custom headers
2121
- **Write and append**: Create new workspace files or append content to existing ones
2222
- **Compress and decompress**: Bundle files into a .zip archive or extract an archive into the workspace
@@ -70,13 +70,14 @@ Extract the text content of one or more workspace files from selected file objec
7070

7171
### File Search
7272

73-
Search indexed text across active workspace files using literal smart-case substring matching.
73+
Search the indexed text of active workspace files for lines matching a regular expression, and return each matching line once with its file ID and line number. Coverage is what the index currently holds, so check "complete" and "indexStatus" before concluding that something is absent.
7474

7575
#### Input
7676

7777
| Parameter | Type | Required | Description |
7878
| --------- | ---- | -------- | ----------- |
79-
| `query` | string | Yes | Literal text to find \(3-512 characters\). Uppercase Unicode letters make matching case-sensitive. |
79+
| `query` | string | Yes | A regular expression matched against each line, 3-512 characters. Supports "." "*" "+" "?" "\{n,m\}" and their lazy forms, character classes such as "\[a-z\]" and "\[^0-9\]", the classes \d \w \s and \D \W \S, alternation "\|", groups "\(...\)" and "\(?:...\)", the anchors "^" and "$", and the word boundary \b. Lookahead, lookbehind, backreferences, named groups, inline flags such as "\(?i\)", \p\{...\} and POSIX "\[\[:alpha:\]\]" classes are not supported, and a pattern cannot span a line break. The pattern must contain at least 3 consecutive literal characters that every match will include — write "error \d+" rather than "\w+ \d+". Escape any metacharacter you mean literally. Matching is case-insensitive until the pattern contains an uppercase letter you are searching for; uppercase inside an escape or a character class, such as \D or \[A-Z\], does not make it case-sensitive. When the workflow builder sets Match to exact instead, the query is matched verbatim and no metacharacter needs escaping. |
80+
| `mode` | string | No | How the query is read, chosen by the workflow builder: "regex" \(default\) as a regular expression, or "exact" as verbatim text. |
8081
| `maxResults` | number | No | Hard result cap configured by the workflow builder \(1-200, default 50\). |
8182

8283
#### Output

apps/sim/blocks/blocks/file.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,29 @@ describe('FileV5Block', () => {
7272
query: '',
7373
maxResults: '25',
7474
})
75-
).toEqual({ query: '', maxResults: 25 })
75+
).toEqual({ query: '', mode: 'regex', maxResults: 25 })
7676

7777
const query = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'query')
78+
const mode = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'mode')
7879
const maxResults = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'maxResults')
7980
expect(query?.paramVisibility).toBe('user-or-llm')
81+
expect(mode?.paramVisibility).toBe('user-only')
8082
expect(maxResults?.paramVisibility).toBe('user-only')
8183
expect(query?.canonicalParamId).toBeUndefined()
8284
expect(maxResults?.canonicalParamId).toBeUndefined()
8385
expect(maxResults?.value?.()).toBe('50')
86+
expect(mode?.value?.()).toBe('regex')
87+
})
88+
89+
it.each([
90+
[undefined, 'regex'],
91+
['exact', 'exact'],
92+
['regex', 'regex'],
93+
['glob', 'regex'],
94+
])('resolves the builder-configured match mode %s to %s', (mode, expected) => {
95+
expect(buildParams({ operation: 'file_search', query: 'needle', mode })).toMatchObject({
96+
mode: expected,
97+
})
8498
})
8599

86100
it('uses the default search cap when the builder field is cleared', () => {
@@ -90,7 +104,7 @@ describe('FileV5Block', () => {
90104
query: 'needle',
91105
maxResults: '',
92106
})
93-
).toEqual({ query: 'needle', maxResults: 50 })
107+
).toEqual({ query: 'needle', mode: 'regex', maxResults: 50 })
94108
})
95109

96110
it.each(['10.5', '10results', '0', '201'])(

apps/sim/blocks/blocks/file.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,9 @@ 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 literal text across all active workspace files and returns structured results with fileId, lineNumber, and text. Lowercase queries are case-insensitive; adding any uppercase letter makes the search case-sensitive.
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.
912+
- 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.
913+
- 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.
912914
- Search is eventually consistent. Check "complete" and "indexStatus" when pending, failed, skipped, or partially indexed files matter to the task.
913915
- Use Fetch for external file URLs. Add headers for authenticated downloads, for example Slack private file URLs require an Authorization Bearer token.
914916
- Use Write to create a new workspace file and Append to add content to an existing one. Write adds a numeric suffix when the name is taken; turn on "Overwrite Existing File" to replace the contents of the file at that exact path (folder and name) instead — a same-named file in another folder is left alone.
@@ -1007,12 +1009,26 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
10071009
condition: { field: 'operation', value: 'file_get_content' },
10081010
required: { field: 'operation', value: 'file_get_content' },
10091011
},
1012+
{
1013+
id: 'mode',
1014+
title: 'Match',
1015+
type: 'dropdown' as SubBlockType,
1016+
options: [
1017+
{ label: 'Regular expression', id: 'regex' },
1018+
{ label: 'Exact match', id: 'exact' },
1019+
],
1020+
description:
1021+
'How the query is read. Regular expressions match one line at a time and need at least 3 consecutive literal characters.',
1022+
value: () => 'regex',
1023+
condition: { field: 'operation', value: 'file_search' },
1024+
paramVisibility: 'user-only',
1025+
},
10101026
{
10111027
id: 'query',
10121028
title: 'Query',
10131029
type: 'short-input' as SubBlockType,
1014-
placeholder: 'Text to find across workspace files',
1015-
description: 'Literal search text, 3-512 characters. Leave blank for the agent to supply.',
1030+
placeholder: 'Pattern to find across workspace files',
1031+
description: 'Search pattern, 3-512 characters. Leave blank for the agent to supply.',
10161032
condition: { field: 'operation', value: 'file_search' },
10171033
required: { field: 'operation', value: 'file_search' },
10181034
paramVisibility: 'user-or-llm',
@@ -1268,6 +1284,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
12681284
}
12691285
return {
12701286
query: params.query,
1287+
mode: params.mode === 'exact' ? 'exact' : 'regex',
12711288
maxResults,
12721289
}
12731290
}
@@ -1500,7 +1517,11 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
15001517
type: 'string',
15011518
description: 'Operation to perform (read, search, get content, fetch, write, or append)',
15021519
},
1503-
query: { type: 'string', description: 'Literal workspace file search query' },
1520+
query: { type: 'string', description: 'Workspace file search query' },
1521+
mode: {
1522+
type: 'string',
1523+
description: 'How the search query is read: a regular expression (default) or an exact match',
1524+
},
15041525
maxResults: { type: 'number', description: 'Hard maximum search results (1-200)' },
15051526
readFileInput: {
15061527
type: 'json',

apps/sim/lib/internal/file/execute-tool.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,13 @@ describe('executeFileTool', () => {
148148
})
149149
expect(mocks.searchContent).toHaveBeenCalledWith({
150150
principal: expect.objectContaining({ serviceId: 'executor' }),
151-
input: { workspaceId: 'workspace-1', query: 'needle', maxResults: 25, signal: undefined },
151+
input: {
152+
workspaceId: 'workspace-1',
153+
query: 'needle',
154+
mode: 'regex',
155+
maxResults: 25,
156+
signal: undefined,
157+
},
152158
})
153159
expect(mocks.executeManage).not.toHaveBeenCalled()
154160
})
@@ -168,7 +174,7 @@ describe('executeFileTool', () => {
168174

169175
expect(mocks.searchContent).toHaveBeenCalledWith(
170176
expect.objectContaining({
171-
input: { workspaceId: 'workspace-1', query: 'needle', maxResults: 50 },
177+
input: { workspaceId: 'workspace-1', query: 'needle', mode: 'regex', maxResults: 50 },
172178
})
173179
)
174180
expect(mocks.getProvenance).toHaveBeenCalledWith(
@@ -191,6 +197,7 @@ describe('executeFileTool', () => {
191197
[{ query: 'abc\0def', maxResults: 50 }, 400],
192198
[{ query: 'needle', maxResults: 201 }, 400],
193199
[{ query: 'needle', maxResults: 0 }, 400],
200+
[{ query: 'needle', mode: 'glob' }, 400],
194201
])('rejects invalid search input before authorization', async (input, status) => {
195202
const response = await executeFileTool(request('file_search', input))
196203

@@ -199,6 +206,16 @@ describe('executeFileTool', () => {
199206
expect(mocks.searchContent).not.toHaveBeenCalled()
200207
})
201208

209+
it('forwards an explicitly configured exact-match mode', async () => {
210+
await executeFileTool(request('file_search', { query: 'needle', mode: 'exact' }))
211+
212+
expect(mocks.searchContent).toHaveBeenCalledWith(
213+
expect.objectContaining({
214+
input: expect.objectContaining({ query: 'needle', mode: 'exact' }),
215+
})
216+
)
217+
})
218+
202219
it('does not expose unexpected search infrastructure errors', async () => {
203220
mocks.searchContent.mockRejectedValueOnce(new Error('database host and query details'))
204221

apps/sim/lib/internal/file/execute-tool.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
FILE_SEARCH_MAX_RESULTS,
3232
FILE_SEARCH_MIN_QUERY_LENGTH,
3333
} from '@/lib/workspace-files/search/constants'
34+
import { FILE_SEARCH_MODES } from '@/lib/workspace-files/search/pattern'
3435

3536
const logger = createLogger('FileToolExecution')
3637

@@ -57,6 +58,7 @@ const fileSearchInputSchema = z
5758
.min(FILE_SEARCH_MIN_QUERY_LENGTH)
5859
.max(FILE_SEARCH_MAX_QUERY_LENGTH)
5960
.refine((query) => !query.includes('\0'), 'Search query cannot contain NUL characters'),
61+
mode: z.enum(FILE_SEARCH_MODES).default('regex'),
6062
maxResults: z
6163
.number()
6264
.int()
@@ -110,6 +112,7 @@ export const executeFileTool: InternalToolOperationHandler = async (request) =>
110112
input: {
111113
workspaceId,
112114
query: searchInput.data.query,
115+
mode: searchInput.data.mode,
113116
maxResults: searchInput.data.maxResults,
114117
signal: request.signal,
115118
},

apps/sim/lib/workspace-files/application/search-workspace-file-content.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
22
import { loadActiveWorkspaceContext } from '@/lib/uploads/contexts/workspace'
33
import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case'
44
import { fileOperations } from '@/lib/workspace-files/application/operations'
5-
import { searchWorkspaceFileIndex } from '@/lib/workspace-files/search/repository'
6-
import { isFileSearchCaseSensitive } from '@/lib/workspace-files/search/text'
5+
import {
6+
compileFileSearchPattern,
7+
type FileSearchMode,
8+
FileSearchPatternError,
9+
} from '@/lib/workspace-files/search/pattern'
10+
import {
11+
searchWorkspaceFileIndex,
12+
WorkspaceFileSearchUnavailableError,
13+
} from '@/lib/workspace-files/search/repository'
714

815
export interface SearchWorkspaceFileContentInput {
916
workspaceId: string
1017
query: string
18+
mode: FileSearchMode
1119
maxResults: number
1220
signal?: AbortSignal
1321
}
@@ -24,12 +32,28 @@ export const searchWorkspaceFileContent = defineAuthorizedWorkspaceFileUseCase({
2432
operation: fileOperations.searchContent,
2533
resolveContext: ({ input }: { input: SearchWorkspaceFileContentInput }) =>
2634
resolveSearchWorkspaceFileContext(input),
27-
execute: ({ input, context }) =>
28-
searchWorkspaceFileIndex({
29-
workspaceId: context.workspaceId,
30-
query: input.query,
31-
maxResults: input.maxResults,
32-
caseSensitive: isFileSearchCaseSensitive(input.query),
33-
signal: input.signal,
34-
}),
35+
execute: async ({ input, context }) => {
36+
try {
37+
return await searchWorkspaceFileIndex({
38+
workspaceId: context.workspaceId,
39+
pattern: compileFileSearchPattern(input.query, input.mode),
40+
maxResults: input.maxResults,
41+
signal: input.signal,
42+
})
43+
} catch (error) {
44+
/**
45+
* A rejected or too-expensive pattern is the caller's to fix, and the
46+
* message names the construct and the supported alternative — so it is
47+
* classified rather than left to become the surface's generic failure text.
48+
*/
49+
if (error instanceof FileSearchPatternError) {
50+
throw new OrchestrationError('validation', error.message)
51+
}
52+
/** Nothing is wrong with the query, so the caller is told to retry, not to rewrite it. */
53+
if (error instanceof WorkspaceFileSearchUnavailableError) {
54+
throw new OrchestrationError('locked', error.message)
55+
}
56+
throw error
57+
}
58+
},
3559
})

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
1+
/**
2+
* `pg_trgm` can only extract a trigram from three consecutive characters, so a
3+
* shorter query has nothing for the segment GIN index to probe and degrades to a
4+
* scan of every tenant's segments. It bounds the literal query length and, in
5+
* regex mode, the shortest literal run every match is guaranteed to contain.
6+
*/
17
export const FILE_SEARCH_MIN_QUERY_LENGTH = 3
28
export const FILE_SEARCH_MAX_QUERY_LENGTH = 512
9+
10+
/**
11+
* Caps the analyzer's bookkeeping strings so a bounded repeat cannot expand a
12+
* short pattern into a large intermediate. Only {@link FILE_SEARCH_MIN_QUERY_LENGTH}
13+
* characters are ever needed, so truncating past this loses no decision.
14+
*/
15+
export const FILE_SEARCH_PATTERN_LITERAL_CAP = 512
16+
export const FILE_SEARCH_PATTERN_MAX_REPEAT = 1000
17+
export const FILE_SEARCH_PATTERN_MAX_DEPTH = 20
18+
19+
/**
20+
* Backstop for a pattern whose trigrams the planner cannot use — a punctuation-only
21+
* or non-ASCII literal, or a regex whose guaranteed run yields no trigram. Those
22+
* plan as a sequential scan across every workspace's segments, so the search must
23+
* not be able to hold a pooled connection open indefinitely.
24+
*/
25+
export const FILE_SEARCH_STATEMENT_TIMEOUT_MS = 10 * 1000
26+
export const FILE_SEARCH_LOCK_TIMEOUT_MS = 5 * 1000
327
export const FILE_SEARCH_DEFAULT_MAX_RESULTS = 50
428
export const FILE_SEARCH_MAX_RESULTS = 200
529

0 commit comments

Comments
 (0)