Skip to content

build_context has no page_size or max_related ceiling, while list_directory has had one since #1048 #1296

Description

@sammywachtel

The inconsistency

list_directory was bounded and paginated in #1048 (closed 2026-07-16), on the reasoning that an unbounded tool result "can produce oversized MCP responses". It enforces a ceiling:

if page_size > MAX_DIRECTORY_PAGE_SIZE:      # 200
    raise ValueError(f"page_size must be <= {MAX_DIRECTORY_PAGE_SIZE}, got {page_size}")

build_context was not included in that change. Its page_size and max_related are Field(default=10) with no upper bound at all, so a caller can raise them arbitrarily.

What that looks like in practice

An agent asked to build a graph of a 149-note vault reached for build_context over a wildcard, and — finding the default of 10 gave it a slice rather than the graph — raised the parameters until it got everything. It settled on page_size=500, max_related=1000.

Measured at the reverse proxy, that returned 1,683,656 bytes. The same call appears twice in the access log, byte-for-byte identical, three and a half minutes apart, because the turn it broke was retried and issued the same query again.

For scale, from the same vault in the same hour:

tool max response
build_context 1.68 MB
list_directory 134 KB
read_note 53 KB
search_notes 4 KB

Two things compound here, and neither is obvious from the tool's signature:

  • max_related multiplies. It caps related rows per result, so raising it scales the response by the number of primaries. A caller reading "maximum number of related results" can reasonably read it as a total.
  • The defaults invite it. page_size=10 on a whole-vault question returns something that looks like a truncated answer with no clear signal about how much is missing, so raising the number is the natural next move. There is nothing to raise it to.

Suggested fix, matching what #1048 already established

Ceilings on both, validated in the same shape, constants beside the schema as MAX_DIRECTORY_PAGE_SIZE is:

MAX_CONTEXT_PAGE_SIZE = 200      # the number already proven on list_directory
MAX_MAX_RELATED = 500            # higher: an edge is hundreds of bytes, a note body thousands

Refusing rather than truncating seems right for this tool specifically: a graph silently cut to fit is a wrong answer that looks complete, and the caller has no way to tell which edges are missing. An explicit error lets them page.

It also helps if the message points somewhere. Ours reads:

page_size must be <= 200, got 500. For a whole-vault read, page through with include_content=False — the note bodies are most of the payload and the graph is not in them.

(include_content is a local addition on our fork — the equivalent upstream idea is #686's L0 tier. The general point stands without it: an error that only refuses leaves the caller guessing, and guessing is how they arrived at 500.)

Relationship to #686

#686 proposes L0/L1/L2 progressive loading across search_notes and build_context, and an April comment there reports the same symptom — "build_context across all pages for graph-wide analysis costs ~50K tokens and won't scale past ~200 pages."

This issue is narrower and independent. #686 is about what a response contains; this is about how much of it a caller can request. A ceiling is useful whether or not the tiering lands, and it is a much smaller change.

Happy to send a patch

We have this implemented and tested on a fork — the constants, the validation, and four tests including one asserting that values at the cap still work, since a bound that refuses legitimate calls is worse than no bound. Glad to open a PR against main if the approach looks right; wanted to check the shape with you first rather than arrive with a design.

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