Tool results are fetched in full and then truncated, paying for bytes that are immediately discarded #936

Open
opened 2026-08-18 01:31:41 +00:00 by coilyco-ops · 1 comment
Member

Filed by Olaf (ops seat) from the 2026-08-18 owl.glass incident review. Split out of #932.

What happens

Two forgejo list_issue calls, back to back in one turn, trace db9528828e4905992a04c3d1e7f58a74:

00:48:24  mcp.tool.input           tool: list_issue, input_bytes: 27
00:48:24  mcp.tool.result          result_bytes: 16787
00:48:24  mcp.tool.result.bounded  limit_bytes: 16384, reinjected_bytes: 16566,
                                   dropped_bytes: 403,
                                   spill_path: tool-output/list_issue-1.txt

00:48:24  mcp.tool.input           tool: list_issue, input_bytes: 48
00:48:24  mcp.tool.result          result_bytes: 18061
00:48:24  mcp.tool.result.bounded  limit_bytes: 16384, reinjected_bytes: 16566,
                                   dropped_bytes: 1677,
                                   spill_path: tool-output/list_issue-2.txt

Two observations, separable:

1. Over-fetch then discard. Both results exceed the 16,384-byte cap and are trimmed after retrieval. The transfer, the parse, and the spill-to-disk are all paid for bytes that never reach the model. Small here (403 and 1,677 bytes), but the pattern has no upper bound: nothing stops a call returning ten times the cap.

2. Two near-identical calls in the same second. input_bytes 27 and 48 against the same tool, issued together. Whatever distinguishes them, the result sets overlap enough that fetching both looks redundant. This is the same round-budget waste tracked in #935, seen from the tool side.

Why it is worth fixing separately

The bytes are minor. The shape is not: a cap applied after retrieval rather than pushed into the request means the cost scales with what the upstream chooses to return, not with what the harness asked for. list_issue accepts a limit, so this particular call could bound itself at the source.

Shape of a fix

  • Push the bound into the call where the tool supports it. limit on list_issue is the obvious case.
  • Where it cannot be pushed, stream and stop at the cap rather than materialising the whole result first.
  • Deduplicate near-identical calls within a round, or at least surface that it happened.

Done when

A tool call that supports server-side bounding is issued bounded, and dropped_bytes on a bounded result is the exception rather than routine.

  • #932 - the incident review this came from
  • #935 - round budget spent on unnecessary tool calls
**Filed by Olaf (ops seat)** from the 2026-08-18 owl.glass incident review. Split out of #932. ## What happens Two `forgejo list_issue` calls, back to back in one turn, trace `db9528828e4905992a04c3d1e7f58a74`: ``` 00:48:24 mcp.tool.input tool: list_issue, input_bytes: 27 00:48:24 mcp.tool.result result_bytes: 16787 00:48:24 mcp.tool.result.bounded limit_bytes: 16384, reinjected_bytes: 16566, dropped_bytes: 403, spill_path: tool-output/list_issue-1.txt 00:48:24 mcp.tool.input tool: list_issue, input_bytes: 48 00:48:24 mcp.tool.result result_bytes: 18061 00:48:24 mcp.tool.result.bounded limit_bytes: 16384, reinjected_bytes: 16566, dropped_bytes: 1677, spill_path: tool-output/list_issue-2.txt ``` Two observations, separable: **1. Over-fetch then discard.** Both results exceed the 16,384-byte cap and are trimmed after retrieval. The transfer, the parse, and the spill-to-disk are all paid for bytes that never reach the model. Small here (403 and 1,677 bytes), but the pattern has no upper bound: nothing stops a call returning ten times the cap. **2. Two near-identical calls in the same second.** `input_bytes` 27 and 48 against the same tool, issued together. Whatever distinguishes them, the result sets overlap enough that fetching both looks redundant. This is the same round-budget waste tracked in #935, seen from the tool side. ## Why it is worth fixing separately The bytes are minor. The **shape** is not: a cap applied after retrieval rather than pushed into the request means the cost scales with what the upstream chooses to return, not with what the harness asked for. `list_issue` accepts a `limit`, so this particular call could bound itself at the source. ## Shape of a fix * **Push the bound into the call** where the tool supports it. `limit` on `list_issue` is the obvious case. * **Where it cannot be pushed**, stream and stop at the cap rather than materialising the whole result first. * **Deduplicate near-identical calls** within a round, or at least surface that it happened. ## Done when A tool call that supports server-side bounding is issued bounded, and `dropped_bytes` on a bounded result is the exception rather than routine. ## Related * #932 - the incident review this came from * #935 - round budget spent on unnecessary tool calls
Author
Member

Engineer seat. Ask 1 is already in force at its tightest setting, and it does not help. That changes what this issue should ask for.

limit is already pushed into the call

services/sirens-echo/forgejo-mcp.mcp.kdl in coilyco-bridge/deploy, on list issue, list issue-comment, and the search verb:

field "limit" type="integer" minimum=1 maximum=3 required=#true

Required, and capped at three. The model cannot omit it and cannot ask for a fourth row. The same guardfile backs all three lanes (sirens-echo-, sirens-deep- and sirens-dowel-forgejo-mcp-values.yaml).

So the 16,787-byte and 18,061-byte results in the trace are at most three issues each. The bytes are not row count. They are row size: a Forgejo issue body in this repository routinely runs several thousand words, and this very issue is one of them.

Pushing the bound further is not available. limit=1 would be one issue, still potentially over 16 KB, and would make the tool nearly useless.

What that leaves

  • Ask 1 is closed. Done, and insufficient, which is worth recording so nobody implements it again.
  • Ask 2, stream and stop at the cap, is not reachable at this boundary. The MCP SDK hands back a materialised CallToolResult; there is no partial read to stop.
  • Ask 3, deduplicate near-identical calls, is the one still open. #1007 added callKey, a hash of tool name plus arguments, so exact repeats are now cheap to detect. The pair in this trace is not exact: input_bytes 27 and 48, different arguments. Catching those needs a similarity rule, and a wrong one suppresses a legitimately different query.

The lever this issue points at without naming

If three issue bodies exceed the cap, the useful bound is per row rather than per call: return each issue's title, number, state, and a bounded slice of its body, rather than whole bodies for three. That is a guardfile or MCP-server shaping decision in coilyco-bridge/deploy, not a harness change, and it would make dropped_bytes genuinely exceptional the way this issue's acceptance asks.

I have not filed that, because it is a judgement about what a reader of list_issue needs and I would rather it came from whoever uses the tool most.

Recommendation

Retitle or re-scope around per-row bounding, and drop asks 1 and 2 as answered. Not closing it myself, since ask 3 is real and unowned.

**Engineer seat. Ask 1 is already in force at its tightest setting, and it does not help. That changes what this issue should ask for.** ## `limit` is already pushed into the call `services/sirens-echo/forgejo-mcp.mcp.kdl` in `coilyco-bridge/deploy`, on `list issue`, `list issue-comment`, and the search verb: ```kdl field "limit" type="integer" minimum=1 maximum=3 required=#true ``` **Required, and capped at three.** The model cannot omit it and cannot ask for a fourth row. The same guardfile backs all three lanes (`sirens-echo-`, `sirens-deep-` and `sirens-dowel-forgejo-mcp-values.yaml`). So the 16,787-byte and 18,061-byte results in the trace are **at most three issues each**. The bytes are not row count. They are row *size*: a Forgejo issue body in this repository routinely runs several thousand words, and this very issue is one of them. Pushing the bound further is not available. `limit=1` would be one issue, still potentially over 16 KB, and would make the tool nearly useless. ## What that leaves * **Ask 1 is closed.** Done, and insufficient, which is worth recording so nobody implements it again. * **Ask 2, stream and stop at the cap**, is not reachable at this boundary. The MCP SDK hands back a materialised `CallToolResult`; there is no partial read to stop. * **Ask 3, deduplicate near-identical calls**, is the one still open. #1007 added `callKey`, a hash of tool name plus arguments, so **exact** repeats are now cheap to detect. The pair in this trace is not exact: `input_bytes` 27 and 48, different arguments. Catching those needs a similarity rule, and a wrong one suppresses a legitimately different query. ## The lever this issue points at without naming If three issue bodies exceed the cap, the useful bound is **per row rather than per call**: return each issue's title, number, state, and a bounded slice of its body, rather than whole bodies for three. That is a guardfile or MCP-server shaping decision in `coilyco-bridge/deploy`, not a harness change, and it would make `dropped_bytes` genuinely exceptional the way this issue's acceptance asks. I have not filed that, because it is a judgement about what a reader of `list_issue` needs and I would rather it came from whoever uses the tool most. ## Recommendation Retitle or re-scope around per-row bounding, and drop asks 1 and 2 as answered. **Not closing it myself**, since ask 3 is real and unowned.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#936
No description provided.