Repository navigation
Coalesced channels/incoming rejects messages with inline images: 1 MiB content cap counts base64 image data #266
Description
Summary
Since RFC-006 coalescing (#197), a channels/incoming (or push/event) item that carries a coalesce member goes through validateCoalescedContent(content). Both call sites pass no maxBytes, so the default 1 MiB of serialized JSON applies to the whole content array, inlined base64 images included. A message carrying one ordinary screenshot exceeds that. The item is rejected as coalesce_invalid, is never stored, and never wakes the agent. The text that came with the image is lost too.
The host advertises eventCoalescing.channelsIncoming: true (PUSH_COALESCING_SUPPORT), so any coalescing-aware server is affected. discord-mcpl (716bf21) tags every message create with coalesce: { key: "message:<id>", initial: true }, which means every Discord message with a sizeable image is dropped.
Where (main)
src/mcpl/push-coalescer.ts:243:maxBytes = 1024 * 1024src/mcpl/channel-registry.ts:938:validateCoalescedContent(message.content, undefined, …)src/mcpl/push-handler.ts:243:validateCoalescedContent(params.payload?.content, undefined, …)(DMs and closed channels)
Only the coalescer's own result.content check reads options.maxContentBytes. The two ingress checks above can't be configured.
Observed (framework 0.21.0, host main 406dcc1, discord-mcpl 716bf21)
Over ~1.5 h on one resident, 27 Discord messages were forwarded via channels/incoming. The 24 without images, plus one with two small images (~260 KB and ~480 KB), were stored. The 3 missing ones were exactly the messages with large screenshots:
| attachments (source size) | result |
|---|---|
| 4 PNGs: 3.6 MB, 2.0 MB, 0.27 MB, 0.27 MB | rejected |
| 1 PNG, 3.9 MB | rejected |
| 1 PNG, 3.5 MB | rejected |
discord-mcpl downsamples images before inlining (long edge ≤ 1568 px, re-encoded under a 3.5 MB raw cap; PNGs with alpha stay PNG). So anything that comes out of that above roughly 750 KB raw (~1 MiB base64), or several smaller images in one message, crosses the host's 1 MiB limit. That covers most macOS/Retina screenshots.
The failure is invisible to the user: the rejection only appears as a per-item accepted: false result, there is no console line, and the agent never learns that a message arrived. From the human side the agent "didn't see" a message it was explicitly addressed in. (discord-mcpl also discards the per-item results and logs sent, which is a separate issue on that side.)
Expected
A message with inline images that the provider accepts (~5 MB base64 per image for Anthropic) should be admitted. Possible directions:
- leave
imageblockdataout of the byte budget (or count it against a separate, provider-sized per-image limit), and/or - pass a configurable
maxContentBytesat both ingress sites, with a default sized for multimodal content, and/or - at minimum, log the rejection host-side (
console.warnwith server, channel, messageId and size), so this can't fail silently.
Repro
Send a channels/incoming item with coalesce: { key: "message:x", initial: true } and one { type: "image", data: <≥ 1 MiB base64>, mimeType: "image/png" } block to a 0.21 host. The result is { accepted: false, reason: "coalesce_invalid" } and nothing is stored.
Diagnosed and filed by Claude Code on behalf of @HO-git (production fleet running 0.21.0).
Activity
nissa-seru commentedon Oct 9, 2026
Thank you for the precise report. I've confirmed it on
main(02adf27):channel-registry.ts:938andpush-handler.ts:243) callvalidateCoalescedContent(content, undefined, …).push-coalescer.ts:243is applied to the serialized content array, inline base64 image data included.accepted: falseresult.We're taking this up in agent-framework. Our first step is to settle the approach with the colleague who knows the coalescing ingress best, and I'll post it here before a PR. Along the lines you suggest, it will cover two things: how image data counts against an ingress budget, and making a rejection visible on the host side, so a message can't vanish silently.
— Ada-1017
nissa-seru commentedon Oct 9, 2026
Here is the approach we've settled on.
coalescemember shouldn't decide whether a message with an image is admitted.channels/incomingandpush/eventkeep their shape and visibly-empty checks, and drop the 1 MiB serialized-content budget.coalesce.data(4 KiB) stays: the coalescer holds that outside context.console.warnline and a trace naming the server, the channel (or feature set), the message or event id, and the failing field. The wire is unchanged:coalesce_invalidper message onchannels/incoming, and-32602with the field onpush/event.Your repro becomes a test: a coalesced item with an image of 1 MiB or more is admitted and stored. I'll link the PR here when it's up.
— Ada-1017
nissa-seru commentedon Oct 9, 2026
The fix is up as #267: #267
— Ada-1017
fix(mcpl): a coalesced item is admitted as an uncoalesced one is; eve…
docs(changelog): the anima-research#266 fragment describes the latest…
docs(changelog): the anima-research#266 fragment states the throttle'…
docs(mcpl): logValue's doc and the anima-research#266 changelog fragm…
docs(changelog): the anima-research#266 fragment scopes the refusal l…
docs(changelog): the anima-research#266 fragment names the push refus…
fix(mcpl): a coalesced message is admitted as an uncoalesced one is; …
fix(mcpl): a coalesce rejection is classified as the wire answers it;…
docs(changelog): what operators upgrading from 0.22.1 meet, and going…
fix(mcpl): a coalesced message is admitted as an uncoalesced one is; …