The brain already documents this as a live asymmetry (resource-text-dominates-the-system-prompt): "the TS agent caps this (8k/resource, 24k total) as of 2026-07-28, the Python agent does NOT." What the ledger was missing is the row — the knowledge existed, the debt was not tracked, so it could never surface at plan or build time in this region.
Why this is P1 rather than a nit
The uncapped half is the half that runs. npm run dev starts dev:agent, which is
npm run dev:agent:py (package.json:11) — the TypeScript agent is the accepted-lagging
port. So the fix landed on the backend nobody is exercising, and the default path still
carries the defect.
The failure is not gradual. chat_node walks state["resources"], pulls each entry's whole
cached body out of the download cache (chat.py:74, cache written at download.py:78) and
interpolates the lot into the system prompt (chat.py:109) — on every turn, not just the
turn a resource was added. Prompt size therefore tracks how much has been read, not how long
the conversation is. The brain's measurement, taken on real pages: a single Wikipedia article
runs ~115–124k tokens, which on a 30k TPM budget produces
429 Request too large for gpt-4o ... Limit 30000, Requested 124252
That is not throttling — a request larger than the entire per-minute allowance can never succeed, so waiting and retrying never clears it. Symptom to recognise: "the agent 429s on the first message once a resource is attached."
The fix is a port, not a design
Both constants already exist and are already tuned, with the reasoning written down next to them:
MAX_RESOURCE_CHARS = 8000at download time (download.ts:17), with a[truncated]marker appended so the model knows the source is partial.MAX_TOTAL_RESOURCE_CHARS = 24000as an aggregate budget at injection time (chat.ts:21) — per-resource truncation alone still scales with resource count.
Mirror both in download.py and chat_node's resource loop. Two caps rather than one,
because they fail differently: the per-resource cap bounds a single huge page, the aggregate
cap bounds many medium ones. Keep the [truncated] marker — a silently shortened source is
worse than a declared one, since the model will cite it as if complete.
While in chat_node, note the loop already skips "ERROR" entries (chat.py:72); the
truncation belongs alongside that filter, not in a second pass.