Skip to content

search_text does not disclose limit-bound truncation: count reports the ceiling as the total, so a capped result is indistinguishable from a complete one #674

Description

@chenroth-island

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions