fix(tangle): bound tool output as content, not as metadata - #311
Merged
Conversation
`validatedSandboxPromptResult` held every field but `response`/`text`/ `finalText` to the metadata bound, CONTRACT_MAX_STRING_LENGTH (16,384 characters per string). `toolInvocations` carries whatever a tool returned, and one webfetch of a paper or an API page is routinely tens or hundreds of kilobytes, so it belongs with the response text rather than with the fields that describe the turn. `isBoundedEventContentJson` was written for exactly this material — its own comment says content "may contain a single large transcript or tool result" — and the whole record already passes it at CONTRACT_MAX_JSON_BYTES immediately above. The Sandbox SDK serializes each tool value up to MAX_SERIALIZED_TOOL_VALUE_BYTES (4 MiB), so producer and consumer disagreed by 256x: every tool result between 16 KiB and 4 MiB was written by one side and refused by the other. The cost of that disagreement was not a failed tool call. This validator runs inside the terminal result read, AFTER the live stream has drained and the usage receipt has been credited, so the refusal converted a finished, fully paid turn into an unreconcilable retained execution that a supervisor reports as a child that did no work at all. Measured 2026-09-11 in one Discovery Lab worktree: 143 of 199 children across 16 pursuits, every one at `iterations: 0` and `ms: 0`, and the literature enumerate and extract stages that fetch papers were the ones that died. Their six-stage sourcing graph blocked at whatever stage it reached in 24 of 24 invocations and produced zero charters. Selection evidence: across 45 surviving children and 845 tool results the length distribution decays smoothly and stops dead at exactly 16,384, with none at or above it and no pile-up at the bound. Tool output stays bounded — the content check above still refuses an unbounded result — and every other field keeps its metadata limit. Tests: one added; it fails on the previous code with the exact production error, `value exceeds its JSON bound`. Provider suite 263 pass, typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
validatedSandboxPromptResultholds every field exceptresponse/text/finalTextto the metadata bound,CONTRACT_MAX_STRING_LENGTH= 16,384 characters per string.toolInvocationscarries whatever a tool returned. Onewebfetchof a paper or an API page is routinely tens or hundreds of kilobytes. It is content, not a field that describes the turn.Meanwhile the Sandbox SDK serializes each tool value up to
MAX_SERIALIZED_TOOL_VALUE_BYTES= 4 MiB (runtime-api-BcLo4lBU.js:63). Producer and consumer disagree by 256×: every tool result between 16 KiB and 4 MiB is written by one side and refused by the other.Why it was expensive
The refusal does not fail the tool call. This validator runs inside the terminal result read — after the live stream drained and after the usage receipt was credited — so it converted a finished, fully paid turn into an unreconcilable retained execution, which a supervisor then reports as a child that did no work at all.
Measured 2026-09-11 in one Discovery Lab worktree:
iterations: 0,ms: 0, under the single reasonretained provider execution requires reconciliation before replacement.Selection evidence: across 45 surviving children and 845 tool results, the result-length distribution decays smoothly and stops dead at exactly 16,384, with none at or above it and no pile-up at the bound (which truncation would produce).
Direct falsification against the shipped validator — the wall is exactly where the constant is:
Change
Add
toolInvocationsto the fields excluded from the metadata check.isBoundedEventContentJsonwas written for exactly this material — its comment says content "may contain a single large transcript or tool result" — and the whole record already passes it atCONTRACT_MAX_JSON_BYTESimmediately above.Tool output stays bounded: the content check still refuses an unbounded result. Every other field keeps its metadata limit.
Tests
One added to
tangle-events.test.ts. It fails on the previous code with the exact production error,value exceeds its JSON bound. It pins the wall (MAX_STRING_LENGTH + 1now passes), a realistic 200 KB fetch, the retained upper bound (2 MiB still refused), and thattraceIdkeeps the metadata limit.Provider suite 263 pass, typecheck clean. Patch version 1.1.8 → 1.1.9.
🤖 Generated with Claude Code