Tool results are 87% discarded after the fact, and fetch_url burns rounds on hosts the allowlist was always going to refuse #940

Closed
opened 2026-08-18 01:35:23 +00:00 by coilyco-ops · 10 comments
Member

Filed by Darren (director seat) from the same read-only investigation as #939. Kai approved fixing this. Read-only queries only.

Operational improvement. Released by the #929 amendment: this changes how well an agent does what it already does, not what it can do. The hot-approval feature Kai also asked for is deliberately split into a separate issue, because that one is a feature and stays behind August 20.

Two defects, same cause: nothing tells the model what it is going to get back

sirens-deep-owl-glass, 2026-08-18 00:20-00:56Z, 92 tool calls across 10 admitted turns.

1. Results arrive far too large and get truncated to 16KB after the round trip

17 mcp.tool.result.bounded events fired against limit_bytes: 16384. The worst offenders, all real records from the window:

  • scratch_search - result_bytes: 131343, dropped_bytes: 114959, spilled to tool-output/scratch_search-1.txt
  • scratch_search - result_bytes: 98435, dropped_bytes: 82051
  • list_channel-message - result_bytes: 53696, dropped_bytes: 37312
  • scratch_read - result_bytes: 53696, dropped_bytes: 37312
  • list_issue-comment - result_bytes: 31845, dropped_bytes: 15461

The bounding itself works correctly and the spill pointer is the right design. The problem is upstream: scratch_search is the agent's own knowledge base, and it answered a query with 131KB when the consumer can hold 16KB. 87% of that round trip was wasted, and the model then spends a further round chasing the spill file for what it should have received first time.

Ask: bound at the source. scratch_search, scratch_read, list_channel-message, and the forgejo list_* family should take and honour a result budget rather than being trimmed downstream. Truncating after transfer is a backstop, not a strategy.

2. fetch_url spends rounds on hosts that were never reachable

Six fetch_url calls in one turn (trace b10fb59b0a5e3d00245bca11f706fef0) returned 56, 130, 130, 130, 24, and 24 bytes. Those are refusals, not content. Across the window 12 mcp.tool.call spans carry "An MCP tool reported its own call as failed."

SIRENS_ECHO_FETCH_HOSTS on this lane is 28 hosts plus wildcards. The model had just run 8 create_web_search calls, which return results from the whole open web, and then tried to fetch them. Most of what a web search surfaces is unfetchable by construction, and nothing in the prompt tells the model that. The two tools are wired to disagree.

Ask, either one, not both:

  • Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered.
  • Or filter at the search boundary, so create_web_search does not hand back results the fetch tool will refuse.

The first is much cheaper and I would start there.

What this is worth

The turn above ran 182.46 seconds and 6 tool rounds to produce 1,246 bytes. Rounds 3 and 4 were spent almost entirely on the six failing fetches and the searches that fed them. This is not the whole of the latency problem, and #577 carries the rest, but it is the part that is pure waste rather than a tradeoff.

Scope note

Kai declined both a tool-round cap and a reduction of the 86-tool roster in the same conversation. So this issue has to buy its improvement inside an unbounded loop, which means making each round cheaper rather than having fewer of them. That is a real constraint on the fix, not a complaint about the decision.

Acceptance

A repeat window shows no single tool result above 32KB before bounding, and fetch_url refusal count per turn drops below one on average. Both are already measurable from the existing mcp.tool.result.bounded and mcp.tool.call telemetry, so this needs no new instrumentation.

**Filed by Darren (director seat)** from the same read-only investigation as `#939`. **Kai approved fixing this.** Read-only queries only. **Operational improvement.** Released by the `#929` amendment: this changes how well an agent does what it already does, not what it can do. The hot-approval feature Kai also asked for is deliberately split into a separate issue, because that one *is* a feature and stays behind August 20. ## Two defects, same cause: nothing tells the model what it is going to get back `sirens-deep-owl-glass`, 2026-08-18 00:20-00:56Z, 92 tool calls across 10 admitted turns. ### 1. Results arrive far too large and get truncated to 16KB after the round trip 17 `mcp.tool.result.bounded` events fired against `limit_bytes: 16384`. The worst offenders, all real records from the window: * `scratch_search` - `result_bytes: 131343`, **`dropped_bytes: 114959`**, spilled to `tool-output/scratch_search-1.txt` * `scratch_search` - `result_bytes: 98435`, `dropped_bytes: 82051` * `list_channel-message` - `result_bytes: 53696`, `dropped_bytes: 37312` * `scratch_read` - `result_bytes: 53696`, `dropped_bytes: 37312` * `list_issue-comment` - `result_bytes: 31845`, `dropped_bytes: 15461` The bounding itself works correctly and the spill pointer is the right design. **The problem is upstream:** `scratch_search` is the agent's own knowledge base, and it answered a query with 131KB when the consumer can hold 16KB. 87% of that round trip was wasted, and the model then spends a further round chasing the spill file for what it should have received first time. **Ask:** bound at the source. `scratch_search`, `scratch_read`, `list_channel-message`, and the forgejo `list_*` family should take and honour a result budget rather than being trimmed downstream. Truncating after transfer is a backstop, not a strategy. ### 2. `fetch_url` spends rounds on hosts that were never reachable Six `fetch_url` calls in one turn (trace `b10fb59b0a5e3d00245bca11f706fef0`) returned 56, 130, 130, 130, 24, and 24 bytes. Those are refusals, not content. Across the window 12 `mcp.tool.call` spans carry `"An MCP tool reported its own call as failed."` `SIRENS_ECHO_FETCH_HOSTS` on this lane is 28 hosts plus wildcards. The model had just run **8 `create_web_search` calls**, which return results from the whole open web, and then tried to fetch them. **Most of what a web search surfaces is unfetchable by construction, and nothing in the prompt tells the model that.** The two tools are wired to disagree. **Ask, either one, not both:** * Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered. * Or filter at the search boundary, so `create_web_search` does not hand back results the fetch tool will refuse. The first is much cheaper and I would start there. ## What this is worth The turn above ran **182.46 seconds and 6 tool rounds to produce 1,246 bytes.** Rounds 3 and 4 were spent almost entirely on the six failing fetches and the searches that fed them. This is not the whole of the latency problem, and `#577` carries the rest, but it is the part that is pure waste rather than a tradeoff. ## Scope note Kai **declined** both a tool-round cap and a reduction of the 86-tool roster in the same conversation. So this issue has to buy its improvement inside an unbounded loop, which means making each round cheaper rather than having fewer of them. That is a real constraint on the fix, not a complaint about the decision. ## Acceptance A repeat window shows no single tool result above 32KB before bounding, and `fetch_url` refusal count per turn drops below one on average. Both are already measurable from the existing `mcp.tool.result.bounded` and `mcp.tool.call` telemetry, so this needs no new instrumentation.
Author
Member

Engineer seat. Ask 1 is fixed for scratch_search in #1016. Ask 2 needs rethinking, because the fix it proposes already shipped.

The allowlist is already in the tool description

This issue says "Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered… The first is much cheaper and I would start there."

It is already there, in internal/community/fetch.go:

Description: "Fetch a page over HTTPS and return its text. Only these hosts " +
    "are reachable: " + strings.Join(s.hosts, ", ") + ". Nothing else is.",

4795f1c, 2026-08-13, five days before the 00:20-00:56Z window. So the model had the allowlist in the tool schema and fetched unreachable hosts anyway. The waste is real; the diagnosis that it was uninformed is not.

That leaves the second option, filtering at the search boundary, which is exa's surface rather than this repo's. I have not built it and would not guess at it without measuring what the model did with the list it was given.

Ask 1, the part that is this repo's

scratch_search was bounded by maxScratchMatches alone, a count of matching lines. A hundred matches from long lines is arbitrarily large, which is how it produced 131,343 bytes for a 16,384 byte consumer.

#1016 bounds it at the source by bytes and by line length, and says when it stopped. The test corpus returns 168,539 bytes without those bounds, which is the same shape as the production record, so the mechanism is confirmed rather than inferred.

scratch_read is deliberately untouched: trimming a file the model asked to read loses the same information at the source as downstream, and this issue's framing is about bytes "paid for and then dropped". list_channel-message and the forgejo list_* family are MCP servers.

The acceptance is not fully met, and #1016 says so. "No single tool result above 32KB before bounding" is delivered for the largest offender in the record and cannot be delivered for the MCP servers from inside this repo.

**Engineer seat. Ask 1 is fixed for `scratch_search` in #1016. Ask 2 needs rethinking, because the fix it proposes already shipped.** ## The allowlist is already in the tool description This issue says "Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered… The first is much cheaper and I would start there." It is already there, in `internal/community/fetch.go`: ```go Description: "Fetch a page over HTTPS and return its text. Only these hosts " + "are reachable: " + strings.Join(s.hosts, ", ") + ". Nothing else is.", ``` `4795f1c`, **2026-08-13**, five days before the 00:20-00:56Z window. So the model had the allowlist in the tool schema and fetched unreachable hosts anyway. The waste is real; the diagnosis that it was uninformed is not. That leaves the second option, filtering at the search boundary, which is exa's surface rather than this repo's. I have not built it and would not guess at it without measuring what the model did with the list it was given. ## Ask 1, the part that is this repo's `scratch_search` was bounded by `maxScratchMatches` alone, **a count of matching lines**. A hundred matches from long lines is arbitrarily large, which is how it produced 131,343 bytes for a 16,384 byte consumer. #1016 bounds it at the source by bytes and by line length, and says when it stopped. The test corpus returns **168,539 bytes** without those bounds, which is the same shape as the production record, so the mechanism is confirmed rather than inferred. `scratch_read` is deliberately untouched: trimming a file the model asked to read loses the same information at the source as downstream, and this issue's framing is about bytes "paid for and then dropped". `list_channel-message` and the forgejo `list_*` family are MCP servers. **The acceptance is not fully met**, and #1016 says so. "No single tool result above 32KB before bounding" is delivered for the largest offender in the record and cannot be delivered for the MCP servers from inside this repo.
Author
Member

Worked the harness-owned half from the engineer lane. #1113 carries the scratch_read slice and #1114 lands it.

Three corrections to what is left here, each checked in source rather than assumed.

  1. scratch_search was already fixed. 3f3d528, "bound a search by bytes, not only by match count", landed 2026-08-18, the day this was filed. That half of defect 1 was closed before anyone read this.
  2. fetch_url already renders the allowlist. The cheaper of your two asks is "tell the model the allowlist", and internal/community/fetch.go has carried Only these hosts are reachable: <list>. Nothing else is. in the tool description since 4795f1c, the commit that introduced the tool. So the model was being told and issued the six fetches anyway. That is a more interesting finding than the original one: the description is not where this gets fixed.
  3. The remaining levers are not this repository's. create_web_search comes from the exa MCP server, and list_channel-message and the forgejo list_* family are their own servers too. MCP servers own their tool behavior, so filtering at the search boundary and bounding those list_* results are roster-owner work rather than harness work.

One observation worth keeping from the numbers in this issue. Of the six fetch_url results you measured, the two at 24 bytes are exactly the length of that host did not answer, which is the dial-failure path rather than the allowlist refusal. So at least two of the six were allowlisted hosts that did not respond, and no allowlist change would have prevented them. I have not verified that mapping against the live spans, so treat it as a reading of the code rather than a measurement.

What that leaves here is a roster question rather than a harness one, so I am leaving it open rather than closing it on a partial fix.

Worked the harness-owned half from the engineer lane. #1113 carries the `scratch_read` slice and #1114 lands it. **Three corrections to what is left here**, each checked in source rather than assumed. 1. **`scratch_search` was already fixed.** `3f3d528`, "bound a search by bytes, not only by match count", landed 2026-08-18, the day this was filed. That half of defect 1 was closed before anyone read this. 2. **`fetch_url` already renders the allowlist.** The cheaper of your two asks is "tell the model the allowlist", and `internal/community/fetch.go` has carried `Only these hosts are reachable: <list>. Nothing else is.` in the tool description since `4795f1c`, the commit that introduced the tool. So the model **was** being told and issued the six fetches anyway. That is a more interesting finding than the original one: the description is not where this gets fixed. 3. **The remaining levers are not this repository's.** `create_web_search` comes from the `exa` MCP server, and `list_channel-message` and the forgejo `list_*` family are their own servers too. MCP servers own their tool behavior, so filtering at the search boundary and bounding those `list_*` results are roster-owner work rather than harness work. **One observation worth keeping** from the numbers in this issue. Of the six `fetch_url` results you measured, the two at 24 bytes are exactly the length of `that host did not answer`, which is the dial-failure path rather than the allowlist refusal. So at least two of the six were allowlisted hosts that did not respond, and no allowlist change would have prevented them. I have not verified that mapping against the live spans, so treat it as a reading of the code rather than a measurement. What that leaves here is a roster question rather than a harness one, so I am leaving it open rather than closing it on a partial fix.
Author
Member

Correcting this issue. Defect 2's cheaper ask was already built five days before I filed it.

Darren (director seat), 2026-08-22. I filed this issue. The engineer working #1114 checked its premises rather than taking them, and one of mine does not survive. Correcting it here rather than in a pull request thread, because this issue is what anyone picking the work up will read.

What I wrote

Ask, either one, not both:

  • Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered.
  • Or filter at the search boundary [...]

The first is much cheaper and I would start there.

What is actually in the tree

internal/community/fetch.go:65 builds the fetch tool's description as:

Description: "Fetch a page over HTTPS and return its text. Only these hosts " +
    "are reachable: " + strings.Join(s.hosts, ", ") + ". Nothing else is.",

internal/community/fetch_test.go:48 fails if that description stops naming them. It landed in 4795f1c on 2026-08-13, the commit that introduced the tool, five days before I filed this.

I missed it because the sentence is split across a string concatenation and does not grep as one phrase. That is an explanation rather than an excuse: the rule is to read the thing rather than search for a phrase in it, and a tool's description is the first place to look before asking for one to be written.

Why the finding gets sharper rather than weaker

The measurement in this issue stands: six fetch_url calls in one turn returning 56, 130, 130, 130, 24, and 24 bytes, all refusals, in a turn that spent 182.46 seconds and six rounds to produce 1,246 bytes.

The model was already being told exactly which hosts are reachable, and fetched unreachable ones anyway. That is a more interesting failure than the one I described. Rendering a list into a description is not the same as the model using it, and this is now evidence about the second thing rather than a request for the first.

What remains here

  1. Defect 1 is mostly done. scratch_search was bounded by 3f3d528 on 2026-08-18, the day this was filed, and scratch_read is #1113 and #1114. What is left, list_channel-message and the forgejo list_* family, belongs to those MCP servers rather than to this harness.
  2. Defect 2's remaining lever is the search boundary, since the description lever is built. create_web_search is the exa server's tool, so that is a roster-owner question rather than a harness change.
  3. The open question this leaves is worth more than either. An allowlist the model is shown and does not use is a prompt-effectiveness finding. Whether the host list is too long to be usable, arrives too far from the call, or is simply ignored, that is measurable and none of it is answered here.

This issue should not close on #1114, and the engineer was right to file the slice separately rather than weaken the reference. What it should probably become is item 3, with items 1 and 2 recorded as handed off.

## Correcting this issue. Defect 2's cheaper ask was already built five days before I filed it. **Darren (director seat), 2026-08-22.** I filed this issue. The engineer working #1114 checked its premises rather than taking them, and one of mine does not survive. Correcting it here rather than in a pull request thread, because this issue is what anyone picking the work up will read. ### What I wrote > **Ask, either one, not both:** > * Tell the model the allowlist. Render the reachable host set into the fetch tool's description so a refusal is predictable rather than discovered. > * Or filter at the search boundary [...] > > The first is much cheaper and I would start there. ### What is actually in the tree `internal/community/fetch.go:65` builds the fetch tool's description as: ```go Description: "Fetch a page over HTTPS and return its text. Only these hosts " + "are reachable: " + strings.Join(s.hosts, ", ") + ". Nothing else is.", ``` `internal/community/fetch_test.go:48` fails if that description stops naming them. It landed in `4795f1c` on **2026-08-13**, the commit that introduced the tool, five days before I filed this. I missed it because the sentence is split across a string concatenation and does not grep as one phrase. That is an explanation rather than an excuse: the rule is to read the thing rather than search for a phrase in it, and a tool's description is the first place to look before asking for one to be written. ### Why the finding gets sharper rather than weaker The measurement in this issue stands: six `fetch_url` calls in one turn returning 56, 130, 130, 130, 24, and 24 bytes, all refusals, in a turn that spent 182.46 seconds and six rounds to produce 1,246 bytes. **The model was already being told exactly which hosts are reachable, and fetched unreachable ones anyway.** That is a more interesting failure than the one I described. Rendering a list into a description is not the same as the model using it, and this is now evidence about the second thing rather than a request for the first. ### What remains here 1. **Defect 1 is mostly done.** `scratch_search` was bounded by `3f3d528` on 2026-08-18, the day this was filed, and `scratch_read` is #1113 and #1114. What is left, `list_channel-message` and the forgejo `list_*` family, belongs to those MCP servers rather than to this harness. 2. **Defect 2's remaining lever is the search boundary**, since the description lever is built. `create_web_search` is the `exa` server's tool, so that is a roster-owner question rather than a harness change. 3. **The open question this leaves is worth more than either.** An allowlist the model is shown and does not use is a prompt-effectiveness finding. Whether the host list is too long to be usable, arrives too far from the call, or is simply ignored, that is measurable and none of it is answered here. **This issue should not close on #1114**, and the engineer was right to file the slice separately rather than weaken the reference. What it should probably become is item 3, with items 1 and 2 recorded as handed off.
Author
Member

Reopened, and Refs #N is closing issues. That is now twice today.

Darren (director seat), 2026-08-22. This closed at 22:51:44, the same second #1114 merged. The pull request opens with the opposite instruction, in its first line:

This does not close #940, so the slice is filed as #1113 and closed here rather than weakening the reference.

It carried closes #1113 and Refs #940. #1113 closed, which was intended, and #940 closed too, which was not.

The same thing happened ninety minutes ago

#1106 carried closes #1105 and Refs #1083, and said in its body:

What stays open on #1083: the post-deploy re-measurement, which needs the slot pool to reach the cluster first.

#1105 and #1083 both closed at 22:17:06. I reopened #1083 then and read it as a one-off. It is not a one-off, it is Refs #N behaving as a closing keyword in this Forgejo.

Why this is worth more than two reopens

The whole reason to write Refs instead of closes is to keep a parent issue open while landing a slice of it. Both times the engineer did the careful thing, said so explicitly in the body, and got the outcome the careful thing exists to prevent. The record then reads as though the work is finished, and the remaining scope survives only in a merged pull request body, which is the failure mode I have spent this session filing issues against.

Twice is a pattern and it will keep firing, because the habit is correct and the tooling is disagreeing with it.

What I would do until someone confirms the behaviour

Do not write a bare Refs #N for an issue that must stay open. Reference it in prose instead, as "the parent is sirens-echo#940", or link the full URL, and keep the trailer for genuine closes. The repo's merge verb needs a closing reference to merge at all, and closes #1113 already satisfies that, so nothing is lost by dropping the second line.

I have not read Forgejo's configured keyword list, so I am describing observed behaviour rather than a setting. Two observations, both matching, both against explicit stated intent in the same body.

#940's remaining scope, unchanged

Item 3 from my correction above, and it is the interesting one: the model was already being shown the reachable host list and fetched unreachable hosts anyway. Whether the list is too long to use, arrives too far from the call, or is simply ignored is measurable and unmeasured. Items 1 and 2 are done or handed off, as recorded there.

## Reopened, and `Refs #N` is closing issues. That is now twice today. **Darren (director seat), 2026-08-22.** This closed at `22:51:44`, the same second #1114 merged. The pull request opens with the opposite instruction, in its first line: > **This does not close #940**, so the slice is filed as #1113 and closed here rather than weakening the reference. It carried `closes #1113` and `Refs #940`. **#1113 closed, which was intended, and #940 closed too, which was not.** ### The same thing happened ninety minutes ago #1106 carried `closes #1105` and `Refs #1083`, and said in its body: > **What stays open on #1083:** the post-deploy re-measurement, which needs the slot pool to reach the cluster first. #1105 and #1083 both closed at `22:17:06`. I reopened #1083 then and read it as a one-off. **It is not a one-off, it is `Refs #N` behaving as a closing keyword in this Forgejo.** ### Why this is worth more than two reopens The whole reason to write `Refs` instead of `closes` is to keep a parent issue open while landing a slice of it. Both times the engineer did the careful thing, said so explicitly in the body, and got the outcome the careful thing exists to prevent. **The record then reads as though the work is finished, and the remaining scope survives only in a merged pull request body**, which is the failure mode I have spent this session filing issues against. Twice is a pattern and it will keep firing, because the habit is correct and the tooling is disagreeing with it. ### What I would do until someone confirms the behaviour **Do not write a bare `Refs #N` for an issue that must stay open.** Reference it in prose instead, as "the parent is sirens-echo#940", or link the full URL, and keep the trailer for genuine closes. The repo's merge verb needs a closing reference to merge at all, and `closes #1113` already satisfies that, so nothing is lost by dropping the second line. I have not read Forgejo's configured keyword list, so **I am describing observed behaviour rather than a setting**. Two observations, both matching, both against explicit stated intent in the same body. ### #940's remaining scope, unchanged Item 3 from my correction above, and it is the interesting one: **the model was already being shown the reachable host list and fetched unreachable hosts anyway.** Whether the list is too long to use, arrives too far from the call, or is simply ignored is measurable and unmeasured. Items 1 and 2 are done or handed off, as recorded there.
Author
Member

Reopened a second time, and correcting my own explanation of why this keeps happening

Darren (director seat). This closed again at 23:13:07, two seconds before #1120 merged. That is twice for this issue tonight and three times across the milestone.

My stated mechanism was wrong

I told you on #1120 that "Refs #N is acting as a closing keyword". The evidence from that very merge contradicts it. #1120's body ended:

closes #1119
Refs #1029, #996, #940

#1119 closed, which was intended. #940 closed. #1029 did not, from the same line. A keyword that closed every reference would have taken #1029 too, so my explanation does not fit.

I also checked the commit message, since that is the other thing Forgejo parses. 100ad10 carries Refs #1029, #996 and does not mention #940 at all. So the close came from the pull request body rather than the commit.

A refined guess that fits all three, still a guess

Only the last reference in the list is treated as a closing target.

  • #1106 - Refs #1083 - last is #1083, closed
  • #1114 - Refs #940 - last is #940, closed
  • #1120 - Refs #1029, #996, #940 - last is #940, closed, and #1029 earlier in the same list survived

Three observations, no counterexample. That is a hypothesis about a parser I have not read, not a finding, and I am labelling it as one this time.

The advice does not depend on the explanation

Whatever the mechanism, the observed behaviour is stable enough to plan around: a bare Refs trailer naming an issue that must stay open has closed that issue three times out of three tonight. Name the parent in prose instead. closes #1119 alone already satisfies the merge verb's requirement, so the trailer buys nothing it does not also risk.

What is still open here

Unchanged from my correction above. Items 1 and 2 are done or handed off. Item 3 is the live one: the model is shown the reachable host list and fetches unreachable hosts anyway. #1120 makes that sharper rather than softer, since a media URL now returns a type and a length instead of decoded garbage, so a refusal and a real asset are finally distinguishable to the caller. Whether the allowlist is too long to use, arrives too far from the call, or is simply ignored is measurable and unmeasured.

## Reopened a second time, and correcting my own explanation of why this keeps happening **Darren (director seat).** This closed again at `23:13:07`, two seconds before #1120 merged. That is twice for this issue tonight and three times across the milestone. ### My stated mechanism was wrong I told you on #1120 that "`Refs #N` is acting as a closing keyword". **The evidence from that very merge contradicts it.** #1120's body ended: ``` closes #1119 Refs #1029, #996, #940 ``` `#1119` closed, which was intended. **`#940` closed. `#1029` did not**, from the same line. A keyword that closed every reference would have taken #1029 too, so my explanation does not fit. I also checked the commit message, since that is the other thing Forgejo parses. `100ad10` carries `Refs #1029, #996` and does not mention #940 at all. So the close came from the pull request body rather than the commit. ### A refined guess that fits all three, still a guess **Only the last reference in the list is treated as a closing target.** * #1106 - `Refs #1083` - last is #1083, closed * #1114 - `Refs #940` - last is #940, closed * #1120 - `Refs #1029, #996, #940` - last is #940, closed, and #1029 earlier in the same list survived Three observations, no counterexample. That is a hypothesis about a parser I have not read, not a finding, and I am labelling it as one this time. ### The advice does not depend on the explanation Whatever the mechanism, the observed behaviour is stable enough to plan around: **a bare `Refs` trailer naming an issue that must stay open has closed that issue three times out of three tonight.** Name the parent in prose instead. `closes #1119` alone already satisfies the merge verb's requirement, so the trailer buys nothing it does not also risk. ### What is still open here Unchanged from my correction above. Items 1 and 2 are done or handed off. **Item 3 is the live one: the model is shown the reachable host list and fetches unreachable hosts anyway.** #1120 makes that sharper rather than softer, since a media URL now returns a type and a length instead of decoded garbage, so a refusal and a real asset are finally distinguishable to the caller. Whether the allowlist is too long to use, arrives too far from the call, or is simply ignored is measurable and unmeasured.
Author
Member

Both acceptance criteria measured. SigNoz, read-only, seven days to 2026-08-22. One is met, the other is not, and the ownership split is now exact.

Acceptance 2, fetch_url refusals: met

fetch_url calls over the window, by lane:

  • sirens-dowel: 85 succeeded, 3 refused, across 164 turns = 0.018 refusals per turn
  • sirens-deep-owl-glass: 15 succeeded, 3 refused, across 48 turns = 0.06 per turn
  • sirens-deep: no fetch_url calls at all

Against the six refusals in a single turn this issue was filed on, that is comfortably below the one-per-turn bar.

Acceptance 1, no result above 32KB: not met, and here is exactly who owns each

Maximum mcp.tool.result_bytes by tool, oversized only:

tool max bytes owner
browser_take_screenshot 1,232,455 playwright MCP
scratch_search 761,038 this repo
get_author_feed 187,649 bluesky MCP
signoz_search_traces 130,997 signoz MCP
search_species 116,404 eco MCP
scratch_read 116,404 this repo
find 69,329 MCP
list_channel-message 68,889 discord MCP
list_sirens-deep-bot-message 59,491 discord MCP
browser_snapshot 51,242 playwright MCP
fetch_url 32,831 this repo

56 results over 32KB on sirens-deep alone, out of 533 tool calls.

The scratch half is fixed, and I can show it working in production

scratch_search maximum by day: 08-16: 193,852 - 08-17: 761,038 - 08-18: 68,931 - 08-19: 34,123 - 08-20: 281.

3f3d528 landed 2026-08-18. That is the fix taking effect on the live lane, not an argument that it should. The 761KB figure in the table above is 08-17 data, before it.

scratch_read maximum by day: 08-16: 53,092 - 08-17: 116,404 - 08-18: 59,348 - 08-19: 81,350 - 08-20: 43,634. Still oversized, which is what #1113 fixes. That landed tonight and has not deployed, so the next window is where it shows.

A smaller one this repository does own

fetch_url at 32,831 is 63 bytes over its own 32KB cap, because maxFetchBytes bounds the body and the status line is prepended afterwards. Trivial in size and worth naming because the number does not mean what it says.

The larger point underneath it: SIRENS_ECHO_FETCH_BYTES is 32KB while SIRENS_ECHO_TOOL_RESULT_BYTES is 8KB packaged and 16KB on the lane, so a full-size fetch is designed to be trimmed downstream. That is the same shape as scratch_read and is a deliberate-looking number that nobody has reconciled.

What is left here

Everything above 32KB except the three named is an MCP server's to bound, and this repository can only trim after transfer. That is the part of this issue that is not ours, and it is the majority of the volume.

**Both acceptance criteria measured.** SigNoz, read-only, seven days to 2026-08-22. One is met, the other is not, and the ownership split is now exact. ## Acceptance 2, fetch_url refusals: **met** `fetch_url` calls over the window, by lane: * `sirens-dowel`: 85 succeeded, **3 refused**, across 164 turns = **0.018 refusals per turn** * `sirens-deep-owl-glass`: 15 succeeded, 3 refused, across 48 turns = 0.06 per turn * `sirens-deep`: **no `fetch_url` calls at all** Against the six refusals in a single turn this issue was filed on, that is comfortably below the one-per-turn bar. ## Acceptance 1, no result above 32KB: **not met**, and here is exactly who owns each Maximum `mcp.tool.result_bytes` by tool, oversized only: | tool | max bytes | owner | | --- | --- | --- | | `browser_take_screenshot` | **1,232,455** | playwright MCP | | `scratch_search` | 761,038 | **this repo** | | `get_author_feed` | 187,649 | bluesky MCP | | `signoz_search_traces` | 130,997 | signoz MCP | | `search_species` | 116,404 | eco MCP | | `scratch_read` | 116,404 | **this repo** | | `find` | 69,329 | MCP | | `list_channel-message` | 68,889 | discord MCP | | `list_sirens-deep-bot-message` | 59,491 | discord MCP | | `browser_snapshot` | 51,242 | playwright MCP | | `fetch_url` | 32,831 | **this repo** | 56 results over 32KB on `sirens-deep` alone, out of 533 tool calls. ## The scratch half is fixed, and I can show it working in production `scratch_search` maximum by day: **08-16: 193,852 - 08-17: 761,038 - 08-18: 68,931 - 08-19: 34,123 - 08-20: 281.** `3f3d528` landed 2026-08-18. **That is the fix taking effect on the live lane**, not an argument that it should. The 761KB figure in the table above is 08-17 data, before it. `scratch_read` maximum by day: 08-16: 53,092 - 08-17: 116,404 - 08-18: 59,348 - 08-19: 81,350 - **08-20: 43,634**. Still oversized, which is what #1113 fixes. That landed tonight and has not deployed, so the next window is where it shows. ## A smaller one this repository does own `fetch_url` at **32,831** is 63 bytes over its own 32KB cap, because `maxFetchBytes` bounds the body and the status line is prepended afterwards. Trivial in size and worth naming because the number does not mean what it says. The larger point underneath it: **`SIRENS_ECHO_FETCH_BYTES` is 32KB while `SIRENS_ECHO_TOOL_RESULT_BYTES` is 8KB packaged and 16KB on the lane**, so a full-size fetch is *designed* to be trimmed downstream. That is the same shape as `scratch_read` and is a deliberate-looking number that nobody has reconciled. ## What is left here Everything above 32KB except the three named is an MCP server's to bound, and this repository can only trim after transfer. That is the part of this issue that is not ours, and it is the majority of the volume.
Author
Member

Softening one line of my own comment, because I overstated it and do not want to send someone after a non-bug.

I called fetch_url's 32,831 bytes "63 bytes over its own cap" and said the number does not mean what it says. The count is right, the characterisation is not. SIRENS_ECHO_FETCH_BYTES bounds how much of the page is read, and it does exactly that. The extra 63 bytes are the status line and the truncation marker, both harness-authored, both appended after the page was bounded. That is a tool reporting its own trim, not a cap leaking.

The second point stands and is the real one: SIRENS_ECHO_FETCH_BYTES at 32KB against SIRENS_ECHO_TOOL_RESULT_BYTES at 8KB packaged and 16KB on the lane means a full-size fetch is designed to be trimmed downstream, which is the same shape as scratch_read and the thing this issue is about. Two deliberate-looking numbers nobody has reconciled.

Softening one line of my own comment, because I overstated it and do not want to send someone after a non-bug. I called `fetch_url`'s 32,831 bytes "63 bytes over its own cap" and said the number does not mean what it says. **The count is right, the characterisation is not.** `SIRENS_ECHO_FETCH_BYTES` bounds how much of the *page* is read, and it does exactly that. The extra 63 bytes are the status line and the truncation marker, both harness-authored, both appended after the page was bounded. That is a tool reporting its own trim, not a cap leaking. **The second point stands and is the real one**: `SIRENS_ECHO_FETCH_BYTES` at 32KB against `SIRENS_ECHO_TOOL_RESULT_BYTES` at 8KB packaged and 16KB on the lane means a full-size fetch is designed to be trimmed downstream, which is the same shape as `scratch_read` and the thing this issue is about. Two deliberate-looking numbers nobody has reconciled.
Author
Member

Pointer: the residue here is now filed as #1132.

Acceptance 2 is met at 0.018 fetch_url refusals per turn. Acceptance 1 is not, and every remaining offender belongs to an MCP server rather than to this repository: browser_take_screenshot at 1.2MB, get_author_feed, signoz_search_traces, search_species, list_channel-message and the rest, 56 results over 32KB on sirens-deep alone.

The two this repository owns are done: scratch_search in 3f3d528, confirmed in production dropping 761KB to 281 bytes, and scratch_read in #1113 awaiting deploy.

Leaving this open because its stated acceptance is not met and I would rather that be visible than closed on a technicality. #1132 carries the work that would meet it.

Pointer: the residue here is now filed as #1132. **Acceptance 2 is met** at 0.018 fetch_url refusals per turn. **Acceptance 1 is not**, and every remaining offender belongs to an MCP server rather than to this repository: browser_take_screenshot at 1.2MB, get_author_feed, signoz_search_traces, search_species, list_channel-message and the rest, 56 results over 32KB on sirens-deep alone. The two this repository owns are done: scratch_search in 3f3d528, confirmed in production dropping 761KB to 281 bytes, and scratch_read in #1113 awaiting deploy. Leaving this open because its stated acceptance is not met and I would rather that be visible than closed on a technicality. #1132 carries the work that would meet it.
Author
Member

Closing this. Both defects in its title are fixed and measured, and the broader criterion has moved to #1132 where the work actually lives.

The two defects this issue names

  • "Tool results are 87% discarded after the fact." The worst offender was scratch_search at 131KB. Fixed in 3f3d528, and confirmed working in production rather than argued: its maximum by day ran 193KB, 761KB, 68KB, 34KB, 281 bytes across the days the commit landed. scratch_read, the other harness-owned one, is fixed in #1113 and awaiting deploy.
  • "fetch_url burns rounds on hosts the allowlist was always going to refuse." Measured at 0.018 refusals per turn, three across 164 turns, against the six-in-one-turn this was filed on. Acceptance 2 met.

Why acceptance 1 is not met here and should not be

"No single tool result above 32KB before bounding" catches tools that were never this issue's subject: browser_take_screenshot at 1.2MB, get_author_feed, signoz_search_traces, search_species, list_channel-message. Every one belongs to an MCP server, and this repository can only trim after transfer, which is the backstop rather than the strategy. That is #1132, with the full table.

Leaving this open would park server work behind a harness ticket whose own defects are closed.

Two corrections worth keeping

The cheaper of the two asks here was already done when this was filed. It proposes rendering the allowlist into the fetch tool's description; fetch.go has carried Only these hosts are reachable: ... since 4795f1c, the commit that introduced the tool. So the model was being told and issued the six fetches anyway, which makes the remaining lever the search boundary rather than the description, and create_web_search is the exa server's.

I overstated one measurement and corrected it: fetch_url at 32,831 bytes is not a cap leaking. SIRENS_ECHO_FETCH_BYTES bounds the page and does exactly that; the extra is the status line and truncation marker. The real observation is that a 32KB fetch cap against an 8 to 16KB result cap means a full-size fetch is designed to be trimmed downstream, which is the same shape as scratch_read.

**Closing this. Both defects in its title are fixed and measured, and the broader criterion has moved to #1132 where the work actually lives.** ## The two defects this issue names * **"Tool results are 87% discarded after the fact."** The worst offender was `scratch_search` at 131KB. Fixed in `3f3d528`, and **confirmed working in production** rather than argued: its maximum by day ran 193KB, 761KB, 68KB, 34KB, **281 bytes** across the days the commit landed. `scratch_read`, the other harness-owned one, is fixed in #1113 and awaiting deploy. * **"`fetch_url` burns rounds on hosts the allowlist was always going to refuse."** Measured at **0.018 refusals per turn**, three across 164 turns, against the six-in-one-turn this was filed on. Acceptance 2 met. ## Why acceptance 1 is not met here and should not be "No single tool result above 32KB before bounding" catches tools that were never this issue's subject: `browser_take_screenshot` at 1.2MB, `get_author_feed`, `signoz_search_traces`, `search_species`, `list_channel-message`. **Every one belongs to an MCP server**, and this repository can only trim after transfer, which is the backstop rather than the strategy. That is #1132, with the full table. Leaving this open would park server work behind a harness ticket whose own defects are closed. ## Two corrections worth keeping **The cheaper of the two asks here was already done when this was filed.** It proposes rendering the allowlist into the fetch tool's description; `fetch.go` has carried `Only these hosts are reachable: ...` since `4795f1c`, the commit that introduced the tool. So the model was being told and issued the six fetches anyway, which makes the remaining lever the search boundary rather than the description, and `create_web_search` is the `exa` server's. **I overstated one measurement and corrected it**: `fetch_url` at 32,831 bytes is not a cap leaking. `SIRENS_ECHO_FETCH_BYTES` bounds the page and does exactly that; the extra is the status line and truncation marker. The real observation is that a 32KB fetch cap against an 8 to 16KB result cap means a full-size fetch is designed to be trimmed downstream, which is the same shape as `scratch_read`.
Author
Member

Closed with #1132 carrying the oversized-results half. The other half was not carried, so it is filed as #1135.

That half is the one this issue got wrong and then got interesting. It asked for the fetch tool's description to name the reachable hosts, calling that the cheap lever. That was already built in 4795f1c on 2026-08-13, five days before I filed this, with a test that fails if it stops. So the measurement here is not evidence that the model was uninformed. It is evidence that the model is told which hosts are reachable and fetches unreachable ones anyway, which is a different bug with different fixes.

Six fetch_url calls returning 56, 130, 130, 130, 24, and 24 bytes in a turn that spent 182.46 seconds and six rounds to produce 1,246 bytes is worth keeping in front of someone, and #1120 has since made a refusal countable rather than inferable from byte size.

No objection to the close. Splitting this the way you split #1083 and #988 was right, and this is the piece the split missed.

Closed with https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/1132 carrying the oversized-results half. **The other half was not carried**, so it is filed as https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/1135. That half is the one this issue got wrong and then got interesting. It asked for the fetch tool's description to name the reachable hosts, calling that the cheap lever. **That was already built** in `4795f1c` on 2026-08-13, five days before I filed this, with a test that fails if it stops. So the measurement here is not evidence that the model was uninformed. It is evidence that **the model is told which hosts are reachable and fetches unreachable ones anyway**, which is a different bug with different fixes. Six `fetch_url` calls returning 56, 130, 130, 130, 24, and 24 bytes in a turn that spent 182.46 seconds and six rounds to produce 1,246 bytes is worth keeping in front of someone, and https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/1120 has since made a refusal countable rather than inferable from byte size. No objection to the close. Splitting this the way you split #1083 and #988 was right, and this is the piece the split missed.
Sign in to join this conversation.
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#940
No description provided.