Skip to content

Coalesced channels/incoming rejects messages with inline images: 1 MiB content cap counts base64 image data #266

Closed

Description

@HO-git

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 * 1024
  • src/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 image block data out of the byte budget (or count it against a separate, provider-sized per-image limit), and/or
  • pass a configurable maxContentBytes at both ingress sites, with a default sized for multimodal content, and/or
  • at minimum, log the rejection host-side (console.warn with 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

  1. nissa-seru commented on Oct 9, 2026

    @nissa-seru
    Contributor

    Thank you for the precise report. I've confirmed it on main (02adf27):

    • Both ingress checks (channel-registry.ts:938 and push-handler.ts:243) call validateCoalescedContent(content, undefined, …).
    • So the 1 MiB default in push-coalescer.ts:243 is applied to the serialized content array, inline base64 image data included.
    • The only trace of a rejection is the per-item accepted: false result.

    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

  2. nissa-seru commented on Oct 9, 2026

    @nissa-seru
    Contributor

    Here is the approach we've settled on.

    • Admission parity. A plain coalesced occurrence enters context exactly as an uncoalesced message does: the coalescer only remembers where it landed. So the coalesce member shouldn't decide whether a message with an image is admitted.
      • The coalesced ingress checks on channels/incoming and push/event keep their shape and visibly-empty checks, and drop the 1 MiB serialized-content budget.
      • Render results drop it too, since RFC-006 §5.2 gives rendered content the same checks as a pushed payload.
      • The bound on coalesce.data (4 KiB) stays: the coalescer holds that outside context.
    • Visible rejections. Every coalesce rejection on the host gets a console.warn line 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_invalid per message on channels/incoming, and -32602 with the field on push/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

  3. nissa-seru commented on Oct 9, 2026

    @nissa-seru
    Contributor

    The fix is up as #​267: #267

    • Inline images. A coalesced message with inline images is now admitted exactly as an uncoalesced one is. The 1 MiB serialized-content budget turned out to apply in four places, including the coalescer's own admission hooks and render results.
    • Host-side visibility. Every coalesce rejection is now named on the host, in a log line and a trace.
    • Your repro is the first test.

    — Ada-1017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions