warnings is the last key in the response, so a truncating consumer destroys the caveats first and the rows arrive looking complete #304

Open
opened 2026-08-15 15:48:26 +00:00 by coilyco-ops · 0 comments
Member

Filed by Darren (director seat) at Kai's direction, 2026-08-15. Cross-cutting finding from coilyco-gaming/sirens-echo#449, filed on every MCP server in the portfolio. This repo is the confirmed instance rather than a speculative audit — the ordering was measured here.

The failure, concretely

Echo told a member:

Currently 0 wooden hull planks are listed for sale on the server. No active store shelves or priced trade history exists for this item.

The tool result it built that from carried its own caveat:

22405 older trades arrive as 3152 hourly rollups; party, item, store,
and unit-price views cover detailed rows only
No markets matched item='wooden hull plank' across 528 ledger rows

The surface said it searched 528 of 22,933 rows. The reply said none exists. It cost a day of investigation into a query path that was fine.

The mechanism, measured on this server

The consuming harness bounds tool results at a byte cap with a head slice — keep the front, discard the tail, append a byte-count notice:

return result[:cut] + fmt.Sprintf(truncationNotice, cut, len(result)), cut, true

This server's live response, fetched read-only during the #449 investigation, in key order:

view fetchedAtISO sourceBaseUrl live totalOffers totalStores
cheapest resale arbitrage supplyGaps marketSummaries warnings
                                                     ^^^^^^^^ last

So whenever an eco response exceeds the cap, its warnings are the first thing removed, deterministically. Not plausible, guaranteed by the ordering plus the slice direction. A truncated eco result reaches the model as rows with no caveat.

Two things make it worse than an ordinary truncation bug:

  • The notice is in bytes, not in meaning. The model learns "this was cut" and never learns "the search covered 528 of 22,933 rows". Only the second changes the answer.
  • The cap is per-consumer. Echo caps at 8192, Deep at 16384. The same query is honest on one profile and silently caveat-free on the other, and nothing in either log says so.

To be exact about scope: on the originating turn nothing was truncated. The filtered response was a few hundred bytes and the model saw the caveat and discarded it, which is a doctrine defect that stays open on sirens-echo#449. This issue is the other cause — the large-response case, which needs no model misbehaviour at all.

Relationship to the invariants already filed here

#266 (null never zero) and #267 (limit bounds every array, truncation always warns) already cover three of the four rules from the other direction, and #267 is the reason this is tractable: get_crafting_atlas returns roughly 45 KB at limit=1 because limit bounds one array of six. Those are the responses that overrun a consumer cap in the first place.

Rule 1 is the new one and has no home yet:

Coverage and caveat metadata serialize first, never last. A field that explains the bound of a result must not sit where a head slice removes it.

If #267 lands and every array is bounded, responses arrive under the cap, nothing is cut, and the warnings survive because nothing removed them. That attacks it at the source. Rule 1 is the belt to that suspenders: it holds even when a response is large for a reason nobody anticipated, and it does not require the harness to learn what a warning means across servers that share no schema.

What to do here

  • Move warnings, and any other coverage or provenance field, ahead of the bulk payload in every tool response. Python dicts preserve insertion order and json.dumps follows it, so this is a construction-order change rather than a schema change.
  • Confirm the same for fetchedAtISO, live, totalOffers and totalStores, which already lead and should stay leading.
  • Sweep all 25 tools, matching #267's scope, rather than fixing the market surface alone.

Acceptance

  • No tool response places coverage or caveat fields after bulk arrays.
  • A response truncated at any consumer cap still carries its caveat in the surviving head.
  • Verified against the largest response each tool can emit, not a typical one.

Source: coilyco-gaming/sirens-echo#449 and its seven-comment thread, which establishes the head-slice mechanism and the per-profile cap difference. Related: #266, #267. The consumer-side doctrine half stays open on sirens-echo#449 and is not what this issue asks for.

**Filed by Darren (director seat) at Kai's direction, 2026-08-15.** Cross-cutting finding from coilyco-gaming/sirens-echo#449, filed on every MCP server in the portfolio. **This repo is the confirmed instance rather than a speculative audit** — the ordering was measured here. ## The failure, concretely Echo told a member: > Currently 0 wooden hull planks are listed for sale on the server. No active store shelves or priced trade history exists for this item. The tool result it built that from carried its own caveat: ``` 22405 older trades arrive as 3152 hourly rollups; party, item, store, and unit-price views cover detailed rows only No markets matched item='wooden hull plank' across 528 ledger rows ``` The surface said it searched 528 of 22,933 rows. The reply said none exists. It cost a day of investigation into a query path that was fine. ## The mechanism, measured on this server The consuming harness bounds tool results at a byte cap with a **head slice** — keep the front, discard the tail, append a byte-count notice: ```go return result[:cut] + fmt.Sprintf(truncationNotice, cut, len(result)), cut, true ``` This server's live response, fetched read-only during the #449 investigation, in key order: ``` view fetchedAtISO sourceBaseUrl live totalOffers totalStores cheapest resale arbitrage supplyGaps marketSummaries warnings ^^^^^^^^ last ``` **So whenever an eco response exceeds the cap, its warnings are the first thing removed, deterministically.** Not plausible, guaranteed by the ordering plus the slice direction. A truncated eco result reaches the model as rows with no caveat. Two things make it worse than an ordinary truncation bug: * **The notice is in bytes, not in meaning.** The model learns "this was cut" and never learns "the search covered 528 of 22,933 rows". Only the second changes the answer. * **The cap is per-consumer.** Echo caps at 8192, Deep at 16384. The same query is honest on one profile and silently caveat-free on the other, and nothing in either log says so. To be exact about scope: on the *originating* turn nothing was truncated. The filtered response was a few hundred bytes and the model saw the caveat and discarded it, which is a doctrine defect that stays open on sirens-echo#449. **This issue is the other cause** — the large-response case, which needs no model misbehaviour at all. ## Relationship to the invariants already filed here #266 (null never zero) and #267 (`limit` bounds every array, truncation always warns) already cover three of the four rules from the other direction, and #267 is the reason this is tractable: `get_crafting_atlas` returns roughly 45 KB **at `limit=1`** because `limit` bounds one array of six. Those are the responses that overrun a consumer cap in the first place. **Rule 1 is the new one and has no home yet:** > Coverage and caveat metadata serialize first, never last. A field that explains the bound of a result must not sit where a head slice removes it. If #267 lands and every array is bounded, responses arrive under the cap, nothing is cut, and the warnings survive because nothing removed them. That attacks it at the source. Rule 1 is the belt to that suspenders: it holds even when a response is large for a reason nobody anticipated, and it does not require the harness to learn what a warning means across servers that share no schema. ## What to do here * Move `warnings`, and any other coverage or provenance field, ahead of the bulk payload in every tool response. Python dicts preserve insertion order and `json.dumps` follows it, so this is a construction-order change rather than a schema change. * Confirm the same for `fetchedAtISO`, `live`, `totalOffers` and `totalStores`, which already lead and should stay leading. * Sweep all 25 tools, matching #267's scope, rather than fixing the market surface alone. ## Acceptance * No tool response places coverage or caveat fields after bulk arrays. * A response truncated at any consumer cap still carries its caveat in the surviving head. * Verified against the largest response each tool can emit, not a typical one. --- Source: coilyco-gaming/sirens-echo#449 and its seven-comment thread, which establishes the head-slice mechanism and the per-profile cap difference. Related: #266, #267. The consumer-side doctrine half stays open on sirens-echo#449 and is not what this issue asks for.
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/eco-app#304
No description provided.