Problem
The runtime budgets tokens, never bytes. The only byte check in the attachment pipeline is the per-attachment cap (MAX_IMAGE_DATA_LENGTH, 14 MiB base64) in packages/runtime/src/runtime/schemas.ts. Around it:
- A
kind: 'user' message accepts an unbounded array of ≤14 MiB attachments — DeliveredAttachmentSchema is applied per element; nothing caps the count or the summed bytes per message, on either transport (parseDeliveredMessage serves both dispatch() and the direct HTTP route).
session.prompt({ images }) / task() images go through assertImagesWithinLimit (persisted-images.ts) — per-image only, count unbounded.
- Tool-result images have no size check whatsoever:
persistToolResultContent → persistCanonicalAttachments → AttachmentStore.put enforces integrity (digest/size match) but no ceiling.
parseJsonBody (errors.ts) buffers the entire request body as one string before any validation runs. Workers accepts request bodies far larger than a 128 MB isolate can parse, so N × 14 MiB attachments OOM at JSON.parse before the schema ever sees them.
Consequence: a single well-formed message can take down a Durable Object isolate, and image-heavy workloads accumulate unboundedly toward the memory ceiling with no admission-time backpressure (see the companion issues for what happens after admission).
Proposed direction
- Aggregate per-message caps (count + summed bytes) enforced in
parseDeliveredMessage so both transports reject identically with a structured 413-shaped error before anything durable exists.
- The same aggregate applied in
assertImagesWithinLimit for prompt/task images.
- Tool-result images: same caps, but failing as a tool error outcome (the model authored the oversized output and should get the chance to retry smaller), never a submission failure.
parseJsonBody: reject on content-length over a generous ceiling before buffering, and read chunked bodies through a byte-counting loop with the same cap.
- Compaction's token estimator should also stop counting user-message image blocks as zero tokens (
compaction.ts counts only text in user messages while tool-result images count ~1200), so the keep-window math stops being blind to exactly the content that dominates memory.
Framework constants first; per-agent configurability can come later (admission runs pre-render, so any config must be static).
Related: the post-admission halves of the same design gap are tracked separately (submission-payload byte residency; model-context image materialization).
Problem
The runtime budgets tokens, never bytes. The only byte check in the attachment pipeline is the per-attachment cap (
MAX_IMAGE_DATA_LENGTH, 14 MiB base64) inpackages/runtime/src/runtime/schemas.ts. Around it:kind: 'user'message accepts an unbounded array of ≤14 MiB attachments —DeliveredAttachmentSchemais applied per element; nothing caps the count or the summed bytes per message, on either transport (parseDeliveredMessageserves bothdispatch()and the direct HTTP route).session.prompt({ images })/task()images go throughassertImagesWithinLimit(persisted-images.ts) — per-image only, count unbounded.persistToolResultContent→persistCanonicalAttachments→AttachmentStore.putenforces integrity (digest/size match) but no ceiling.parseJsonBody(errors.ts) buffers the entire request body as one string before any validation runs. Workers accepts request bodies far larger than a 128 MB isolate can parse, so N × 14 MiB attachments OOM atJSON.parsebefore the schema ever sees them.Consequence: a single well-formed message can take down a Durable Object isolate, and image-heavy workloads accumulate unboundedly toward the memory ceiling with no admission-time backpressure (see the companion issues for what happens after admission).
Proposed direction
parseDeliveredMessageso both transports reject identically with a structured 413-shaped error before anything durable exists.assertImagesWithinLimitfor prompt/task images.parseJsonBody: reject oncontent-lengthover a generous ceiling before buffering, and read chunked bodies through a byte-counting loop with the same cap.compaction.tscounts only text in user messages while tool-result images count ~1200), so the keep-window math stops being blind to exactly the content that dominates memory.Framework constants first; per-agent configurability can come later (admission runs pre-render, so any config must be static).
Related: the post-admission halves of the same design gap are tracked separately (submission-payload byte residency; model-context image materialization).