acpmux: a chat whose records together pass one message still opens (attach reply fits)
Leo, 2026-10-31: "this is completely hardlocked bc of message size". His chat showed "message too large to send: 16907300 bytes, the limit is 15728640" with Retry, and every Retry failed the same way.
What was wrong
That error was not the send. It was the chat's open: _acpmux/attach returns a page of records and the turnOpener (the prompt of the turn the page starts inside). Each may keep one record of up to paging::MAX_RECORD_BYTES (14 MiB), and nothing bounded the two together. A large prompt (inline images) and a large record in the page made a 16.9 MB reply. server/mod.rs turns any reply over rpc::MAX_MESSAGE_BYTES (15 MiB) into message.too_large, so the pane's handshake failed at every open and every Retry, and the chat never opened again.
Fix
hub::fit_reply(hub/paging.rs): when a reply of records would pass one message, the largest records'msgbecome the existingomittedstub ({"_meta":{"acpmux":{"omitted":{"bytes","reason":"too_large"}}}}), largest first, until the reply is underMAX_MESSAGE_BYTES - 1 MiB. Every record keeps its seq, kind, dir and time. It coverseventsandturnOpener._acpmux/attachruns its reply through it.- A record's envelope fits too: a record given up as too large also drops a
kindpast 1 KiB (an agent names its ownsessionUpdate, which becomes the kind). - If the reply still does not fit, the
sessionfields go, largest first, each to its empty value and named insession._meta.acpmux.omitted. First the detail's own fields (configOptions,models,availableCommands,rulesJson,pending, ...; copied whole from the agent), then any other field (a title, tags), and last the chat's identity fields. OnlysessionId, acpmux's own id, always stays, because an agent's own id reachessessionReceipt. The byte count is exact, and a value is only replaced by a smaller one. record_bytes, which every page uses for itsmaxBytes, now counts the record as written, escapes and all. It used the kind's length, and a kind of 169 NULs is 1 KiB of JSON, so 14,000 such records passed as a 4 MiB page and were over 15 MiB.native_history's own copy of that loop now calls the shared one; its tests still cover it.
#28685 (zlib frames) does not cover this: MAX_MESSAGE_BYTES still bounds the JSON text there.
Follow-ups, separate PRs: the pane stops retrying a message.too_large and says which attachment to take out; the composer sends attachments by reference (prompt-uploads.md) instead of inline base64.
Tests
Red first (the test alone at 4f6678c3c75), then the fix:
cargo test -p acpmux --test hub_integration an_attach_page_whose_records_together_pass_one_message_still_opens
-
base + test (
4f6678c3c75, run 41160310328): FAILED,the chat opens: "message too large to send: 18878940 bytes, the limit is 15728640", the same refusal as Leo's chat. -
head
4197118dc90(run 41160921241): ok, 1 passed;cargo test -p acpmux --lib native_history: 9 passed. -
head
f3a14738538(after the review's finding):--test hub_integration an_attach_page: 2 passed;--lib -- paging native_history: 17 passed, includinga_reply_fits_one_message_whatever_holds_its_bytes(a 9 MiBconfigOptionsplus a 5 MiBmodels: the record is stubbed, thenconfigOptionsgoes, andmodelsstays). -
head
938619a0503(after four review rounds):--lib -- paging native_history: 17 passed, includinga_reply_fits_one_message_whatever_holds_its_byteswith a 15 MiB title, a 15 MiB agent session id insessionReceipt, a 15 MiB event kind, and a NUL kind'srecord_bytes.--test hub_integration prompt_supersede3 runs: 8 passed each. At1cf35acabd3(the commit before) the wholecargo test -p acpmux --libpassed 2495, failing only that test's receipt assertion, which938619acorrects. The whole--test hub_integrationpassed 623, failing onlyprompt_supersede::a_prompt_queued_before_a_restart_is_still_replaced(restart queue order, no pages), which passed in all three reruns.
Review (codex gpt-6-astra): SHIP at 938619a05039e4d8928cd6fe0fd90bb84a4481e8. Earlier rounds blocked on the session detail (4197118), summary fields (f3a1473), sessionReceipt (e10cf5b), an event's kind (ae68a05) and envelopes counted by length (d350d6b), each fixed in the next commit.
Reds: cmux-next checks fails only on Tier routing (tests/test_cmux_next_base_tier.py: test_a_feat_cmux_next_push_defers_the_base_tier, test_a_push_whose_scheme_compile_is_the_dev_landing_build_keeps_it, test_coalescing_off_or_another_branch_runs_the_base_tier_on_the_push), and the same three error at the base tip d20e58e9359c. The panic ratchet's RED hits are all in crates this PR does not touch (cmux-automations, cmux-tui-core, cmux-chats-mirror). In acpmux it reports only the warn-only tracked lints (arithmetic, casts).
(The first test commit bd6a252dab4 sized its prompt so one record already passed 14 MiB; 4f6678c3c75 fixed the premise.)
Observed / Not run
- Observed: the cmux-ci runs above (Linux, 32 vCPU).
- Not run: Leo's own chat (its machine can't be reached from here); a fleet NEXT build opening a chat with a 20 MB attachment is the follow-up proof.
Changelog
Fixed: a chat holding very large messages opens again instead of failing with "message too large to send" on every Retry.
Agent session: agent session
š¤ Generated with Claude Code