Describe the bug
When a search_text response is bound by limit rather than by the corpus, nothing in the response says so. count is set to len(enriched), so the array and the count corroborate each other at the ceiling: a truncated result is byte-indistinguishable from a complete one.
The byte budget already handles this correctly — it sets _truncated_by_budget and leaves count exact. The limit path has no equivalent.
This is the disclosure half of #672. That issue is about the ceiling being low (1000); this one is about truncation being undetectable at any ceiling. Raising the limit without adding disclosure just moves the silent cliff, so this is arguably the more important of the two — as noted there, the flag matters more than the number.
Why this bar specifically
search_text is described as "the alt grep backbone", and agents reach for it in place of grep/ripgrep. ripgrep's contract is that a result set is either complete or the process tells you why not; it never returns a plausible prefix silently. An agent cannot verify a search result against anything except a second tool, so a partial answer that looks whole doesn't degrade quality gracefully — it produces confident wrong conclusions ("this symbol has 1000 references" when it has 8963, or a refactor that misses two thirds of the call sites).
To Reproduce
Any query whose real match count exceeds the effective limit:
gortex call search --arg operation=text --arg query=<common term> \
--arg 'limit:=100000' --arg 'max_bytes:=200000000' --index <repo> --format json
Response on a 63,820-file TypeScript repo, git grep ground truth 8963:
{ "query": "Typography", "count": 1000 }
No flag, no cursor, and count reports the ceiling as the total.
Expected behavior
The response states that the limit bound the result, and does not present the ceiling as an exact total.
Environment:
- OS: macOS 26.5.1 (25F80)
- Go version: 1.26.5
- Gortex version: v0.63.7+0988412 / built from
main@27b83069
Additional context
I have this working locally and it is small — internal/mcp/tools_search_text.go, additive, no behavior change for complete results. Keep the requested limit before the clamp:
requestedLimit := limit
if max := searchTextMaxLimit(); limit > max {
limit = max
}
then disclose at response construction:
if limit > 0 && len(enriched) >= limit {
resp["_truncated_by_limit"] = true
resp["_limit_applied"] = limit
resp["count_is_exact"] = false
if requestedLimit > limit {
resp["_limit_requested"] = requestedLimit // the clamp chose this, not the caller
}
}
Landing exactly on the effective limit is the signal. It can fire on a corpus that happens to hold exactly limit matches — a spurious "verify this" is the safe direction to be wrong in, versus silent loss.
Verified on the same repo. Truncated:
{
"query": "Typography",
"count": 1000,
"count_is_exact": false,
"_truncated_by_limit": true,
"_limit_applied": 1000,
"_limit_requested": 100000
}
Complete result, unchanged and no added noise:
{ "query": "getNetworkPostureContext", "count": 80 }
Two notes on scope:
- I did not try to make
count exact when truncated. Counting past the limit means doing the work the limit exists to avoid, so declaring the count inexact seemed the honest and cheap option. If you would rather have an exact total, that is a bigger change and a separate decision.
tools_find_declaration.go:71 and tools_graph_query.go:56 carry the same clamp idiom. I have not touched them; a node listing capped at 1000 is far less likely to mislead than a text sweep, but the same disclosure would apply if you want it uniform.
Happy to open this as a PR, either on its own or together with the GORTEX_SEARCH_TEXT_MAX_LIMIT change from #672 — they are independent and this one stands alone.
Describe the bug
When a
search_textresponse is bound bylimitrather than by the corpus, nothing in the response says so.countis set tolen(enriched), so the array and the count corroborate each other at the ceiling: a truncated result is byte-indistinguishable from a complete one.The byte budget already handles this correctly — it sets
_truncated_by_budgetand leavescountexact. The limit path has no equivalent.This is the disclosure half of #672. That issue is about the ceiling being low (1000); this one is about truncation being undetectable at any ceiling. Raising the limit without adding disclosure just moves the silent cliff, so this is arguably the more important of the two — as noted there, the flag matters more than the number.
Why this bar specifically
search_textis described as "the alt grep backbone", and agents reach for it in place ofgrep/ripgrep. ripgrep's contract is that a result set is either complete or the process tells you why not; it never returns a plausible prefix silently. An agent cannot verify a search result against anything except a second tool, so a partial answer that looks whole doesn't degrade quality gracefully — it produces confident wrong conclusions ("this symbol has 1000 references" when it has 8963, or a refactor that misses two thirds of the call sites).To Reproduce
Any query whose real match count exceeds the effective limit:
Response on a 63,820-file TypeScript repo,
git grepground truth 8963:{ "query": "Typography", "count": 1000 }No flag, no cursor, and
countreports the ceiling as the total.Expected behavior
The response states that the limit bound the result, and does not present the ceiling as an exact total.
Environment:
main@27b83069Additional context
I have this working locally and it is small —
internal/mcp/tools_search_text.go, additive, no behavior change for complete results. Keep the requested limit before the clamp:then disclose at response construction:
Landing exactly on the effective limit is the signal. It can fire on a corpus that happens to hold exactly
limitmatches — a spurious "verify this" is the safe direction to be wrong in, versus silent loss.Verified on the same repo. Truncated:
{ "query": "Typography", "count": 1000, "count_is_exact": false, "_truncated_by_limit": true, "_limit_applied": 1000, "_limit_requested": 100000 }Complete result, unchanged and no added noise:
{ "query": "getNetworkPostureContext", "count": 80 }Two notes on scope:
countexact when truncated. Counting past the limit means doing the work the limit exists to avoid, so declaring the count inexact seemed the honest and cheap option. If you would rather have an exact total, that is a bigger change and a separate decision.tools_find_declaration.go:71andtools_graph_query.go:56carry the same clamp idiom. I have not touched them; a node listing capped at 1000 is far less likely to mislead than a text sweep, but the same disclosure would apply if you want it uniform.Happy to open this as a PR, either on its own or together with the
GORTEX_SEARCH_TEXT_MAX_LIMITchange from #672 — they are independent and this one stands alone.