Describe the bug
search_text clamps the requested limit to 1000 (internal/mcp/tools_search_text.go:52). The clamp bounds the matches array and the reported count, and sets no truncation flag — so a caller cannot tell a capped result from a complete one. There is no cursor in the response to page past it, and path filtering is applied after truncation, so it cannot recover the remainder either.
This matters more for search_text than for the two siblings sharing the same clamp (tools_find_declaration.go:71, tools_graph_query.go:56): it is the literal-search backbone agents use in place of grep, where an unbounded sweep is the normal request. The byte budget (max_bytes) already bounds response size, and does so detectably via _truncated_by_budget.
(Repo and workspace names below are anonymized.)
To Reproduce
- Track a large TypeScript monorepo (63,820 files here, called
repo-ts below).
- Run:
gortex call search --arg operation=text --arg query=Typography \
--arg 'limit:=100000' --arg 'max_bytes:=20000000' \
--index ~/work/repo-ts --format json
- Response:
count: 1000, 1000 matches, no _truncated_by_budget, no cursor.
- Ground truth:
git grep -nP Typography | grep -cE '^[^:]*:[0-9]+:' → 8963.
Expected behavior
Either the requested limit is honored, or the response says it was bound — a flag, or a count that stays exact the way the byte budget keeps it exact. Silent loss of 7,963 of 8,963 matches, with a count that corroborates the wrong number, is the one failure mode a grep replacement cannot have.
Environment:
- OS: macOS 26.5.1 (25F80)
- Go version: 1.26.5
- Gortex version: v0.63.7+0988412
Additional context
Measured losses, all with the flag unset:
| query |
returned |
real |
lost |
repo-ts / Typography |
1000 |
8963 |
7963 |
repo-ts / useState |
1000 |
5071 |
4071 |
repo-go / (4-char term) |
952 |
3415 |
2463 |
repo-ts / useEffect |
1000 |
3156 |
2156 |
repo-agent / Installer |
1000 |
2271 |
1271 |
repo-infra / namespace |
998 |
2088 |
1090 |
Note 952 and 998 — the cap is approached rather than always hit exactly, so even len(matches) == 1000 is not a reliable client-side detector.
Three workarounds checked and ruled out:
limit:=100000 — clamped before the search runs.
- Pagination — no cursor emitted; response keys are only
count, matches, query.
path slicing — filterTextMatchesByPath runs after GrepTextForRepos has already truncated, so a slice returns whatever survived the global 1000, not the subtree's real total.
Git archaeology, in case it is simply vestigial: the clamp arrived in 16b624dd (2026-05-20), the commit that first added search_text, and is unchanged across the 3,329 commits since. budget-by-default with graceful degradation, pagination, sparse fieldsets (b36c307b) landed 2026-05-09 — eleven days earlier — so the byte budget already existed when the clamp was written.
Happy to send a PR. The shape I would propose is a GORTEX_SEARCH_TEXT_MAX_LIMIT override defaulting to 1000, so existing behavior is unchanged and an explicit unbounded sweep becomes expressible.
Update: validated with a local patch, including the "what if it is unbounded" case
I built the proposed change (env override, default still 1000) and measured it, specifically to answer the reasonable objection that lifting the ceiling invites huge responses. Short version: the ceiling was pure loss — removing it costs nothing, and max_bytes takes over as the binding constraint while remaining detectable.
Correctness: the clamp was the only thing missing those matches
Same three queries, GORTEX_SEARCH_TEXT_MAX_LIMIT=200000:
| query |
before |
after |
git grep |
false positives |
Typography |
1000 |
8681 |
8963 |
0 |
useState |
1000 |
5058 |
5071 |
0 |
useEffect |
1000 |
3134 |
3156 |
0 |
Every remaining difference is content the graph deliberately does not index, verified by diffing the hit sets: .claude/ and .cursor/ markdown (265 + 14 of the 282), one vendored *.generated.ts, and Nx __tmpl__ template files. extra = 0 on all three — the result is a strict subset of grep, never a superset, so nothing is invented.
Performance: honoring the request is faster than grep, at every size tested
Warm, median of 3, whole-repo scope:
| query |
matches |
gortex |
git grep |
|
Typography |
8681 |
0.72s |
1.63s |
2.3x faster |
useState |
5058 |
0.75s |
1.71s |
2.3x faster |
useEffect |
3134 |
0.69s |
1.64s |
2.4x faster |
So the cap was not buying latency. It discarded 7,681 matches on Typography to save nothing measurable.
The pathological case: export, 109,604 real hits
This is the query I would worry about as a maintainer, so I ran it with the limit at 200,000 and max_bytes at the default-ish 20 MB:
count = 108747
returned = 108143
_truncated_by_budget = True
response = 21.1 MB
elapsed = 8.7s
daemon RSS = 1.3 GiB -> 1.9 GiB, returned afterward
Three things this shows:
- Unbounded
limit does not mean an unbounded response. max_bytes bound it, exactly as designed.
- And it bound it detectably —
_truncated_by_budget: True was set, which is precisely what the 1000 clamp fails to do. The two ceilings are not redundant with each other: one is honest and one is not.
- Memory stayed bounded — roughly 600 MiB of transient allocation for a 108k-match response, released afterward. No OOM, no daemon instability.
In other words, removing the clamp does not remove the guardrail; it removes the silent guardrail and leaves the honest one in charge. A caller who asks for 200,000 matches and hits the byte budget is told so; today a caller who asks for 200,000 and gets 1000 is not.
Suggested shape (unchanged, now measured)
if max := searchTextMaxLimit(); limit > max {
limit = max
}
with searchTextMaxLimit() reading GORTEX_SEARCH_TEXT_MAX_LIMIT and defaulting to searchTextMaxLimitDefault = 1000. Zero behavior change unless opted into. Happy to open the PR against main with the comment explaining why the byte budget is the appropriate ceiling and this one is not.
If you would rather keep a hard ceiling, raising the default to something like 100,000 and setting a truncation flag when it binds would also fix the correctness problem — the flag matters more than the number.
Environment for the above: macOS 26.5.1, Go 1.26.5, patch applied to main at 27b83069; five-repo workspace, 2,158,904 nodes / 10,388,123 edges; the TypeScript repo measured is 63,820 files.
Describe the bug
search_textclamps the requestedlimitto 1000 (internal/mcp/tools_search_text.go:52). The clamp bounds thematchesarray and the reportedcount, and sets no truncation flag — so a caller cannot tell a capped result from a complete one. There is no cursor in the response to page past it, andpathfiltering is applied after truncation, so it cannot recover the remainder either.This matters more for
search_textthan for the two siblings sharing the same clamp (tools_find_declaration.go:71,tools_graph_query.go:56): it is the literal-search backbone agents use in place ofgrep, where an unbounded sweep is the normal request. The byte budget (max_bytes) already bounds response size, and does so detectably via_truncated_by_budget.(Repo and workspace names below are anonymized.)
To Reproduce
repo-tsbelow).count: 1000, 1000 matches, no_truncated_by_budget, no cursor.git grep -nP Typography | grep -cE '^[^:]*:[0-9]+:'→ 8963.Expected behavior
Either the requested
limitis honored, or the response says it was bound — a flag, or acountthat stays exact the way the byte budget keeps it exact. Silent loss of 7,963 of 8,963 matches, with acountthat corroborates the wrong number, is the one failure mode a grep replacement cannot have.Environment:
Additional context
Measured losses, all with the flag unset:
repo-ts/Typographyrepo-ts/useStaterepo-go/ (4-char term)repo-ts/useEffectrepo-agent/Installerrepo-infra/namespaceNote 952 and 998 — the cap is approached rather than always hit exactly, so even
len(matches) == 1000is not a reliable client-side detector.Three workarounds checked and ruled out:
limit:=100000— clamped before the search runs.count,matches,query.pathslicing —filterTextMatchesByPathruns afterGrepTextForReposhas already truncated, so a slice returns whatever survived the global 1000, not the subtree's real total.Git archaeology, in case it is simply vestigial: the clamp arrived in
16b624dd(2026-05-20), the commit that first addedsearch_text, and is unchanged across the 3,329 commits since.budget-by-default with graceful degradation, pagination, sparse fieldsets(b36c307b) landed 2026-05-09 — eleven days earlier — so the byte budget already existed when the clamp was written.Happy to send a PR. The shape I would propose is a
GORTEX_SEARCH_TEXT_MAX_LIMIToverride defaulting to 1000, so existing behavior is unchanged and an explicit unbounded sweep becomes expressible.Update: validated with a local patch, including the "what if it is unbounded" case
I built the proposed change (env override, default still 1000) and measured it, specifically to answer the reasonable objection that lifting the ceiling invites huge responses. Short version: the ceiling was pure loss — removing it costs nothing, and
max_bytestakes over as the binding constraint while remaining detectable.Correctness: the clamp was the only thing missing those matches
Same three queries,
GORTEX_SEARCH_TEXT_MAX_LIMIT=200000:git grepTypographyuseStateuseEffectEvery remaining difference is content the graph deliberately does not index, verified by diffing the hit sets:
.claude/and.cursor/markdown (265 + 14 of the 282), one vendored*.generated.ts, and Nx__tmpl__template files.extra = 0on all three — the result is a strict subset of grep, never a superset, so nothing is invented.Performance: honoring the request is faster than grep, at every size tested
Warm, median of 3, whole-repo scope:
git grepTypographyuseStateuseEffectSo the cap was not buying latency. It discarded 7,681 matches on
Typographyto save nothing measurable.The pathological case:
export, 109,604 real hitsThis is the query I would worry about as a maintainer, so I ran it with the limit at 200,000 and
max_bytesat the default-ish 20 MB:Three things this shows:
limitdoes not mean an unbounded response.max_bytesbound it, exactly as designed._truncated_by_budget: Truewas set, which is precisely what the 1000 clamp fails to do. The two ceilings are not redundant with each other: one is honest and one is not.In other words, removing the clamp does not remove the guardrail; it removes the silent guardrail and leaves the honest one in charge. A caller who asks for 200,000 matches and hits the byte budget is told so; today a caller who asks for 200,000 and gets 1000 is not.
Suggested shape (unchanged, now measured)
with
searchTextMaxLimit()readingGORTEX_SEARCH_TEXT_MAX_LIMITand defaulting tosearchTextMaxLimitDefault = 1000. Zero behavior change unless opted into. Happy to open the PR againstmainwith the comment explaining why the byte budget is the appropriate ceiling and this one is not.If you would rather keep a hard ceiling, raising the default to something like 100,000 and setting a truncation flag when it binds would also fix the correctness problem — the flag matters more than the number.
Environment for the above: macOS 26.5.1, Go 1.26.5, patch applied to
mainat27b83069; five-repo workspace, 2,158,904 nodes / 10,388,123 edges; the TypeScript repo measured is 63,820 files.