acpmux: a prompt names its files as uploads, of any size, by reference
The coordinator, relaying Leo, 2026-10-26: "Leo's message with 8 screenshots (about 6.6 MB) failed with "Not sent: no answer from acpmux". ... fix it so 20 screenshots send in seconds: attachments go as blobs or files referenced from the prompt, not inlined base64 in one frame."
Why the send failed (not the daemon)
Both the 8-screenshot send and its "text-only" Retry (prompt 3baa5aa2-…, 739 characters) died in the app before reaching acpmux:
- The sending Mac's unified log (
com.cmuxterm.app.next/page-host.chromium) showspage host binding payload refusedat 15:03:02.968 PDT (the first send) and at 15:10:34.992 PDT, 1 s after the Retry was sent (22:10:33.931Z). The Retry re-sends the failed prompt's attachments too (direct.tsfailedPromptskeeps{input, attachments, promptId}), so it was the same ~9 MB frame. ChromiumPageHostBridge.swift:137drops a page call over 4 MiB with only that log line and never answers, so the page'stransport.sendnever settles and the row fails at the 60 sdeliveryTiming.failMs.- That Mac's daemon has no record of
3baa5aa2in any session log, and in the same minute it took a 1-image prompt from the same pane in 31 ms. Its log shows no slow handler, lock or store stall at that time. - The daemon accepts a prompt to a busy session in milliseconds. On a daemon with 98 live sessions, with a turn running
sleep 150, a rawsession/promptgot_acpmux/prompt_acceptedin 1.4 ms (text steer), 93 ms (8 x 820 KB images inline, 9 MB) and 207 ms (20 images inline, 22 MB).
So large prompts need to stop riding in one frame. Every acpmux hop also caps one message at rpc::MAX_MESSAGE_BYTES (15 MiB), so 20 inline screenshots could not pass a peer link either.
Leo, later: "why the FUCK SHOULD THERE BE A FUCKING SIZE CAP ANYWAYS JUST FUCKING / I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE"
Change
A prompt block may name a finished upload of its own chat instead of carrying the bytes. The file is uploaded first with the existing resumable _acpmux/file_put_begin/_chunk/_finish (chunks of at most 512 KiB, so no frame grows with the file). The prompt itself stays a few hundred bytes. The two ref shapes:
{"type": "image", "mimeType": "image/png", "_meta": {"acpmux": {"upload": {"uploadId": ID, "fileName": NAME}}}}(nodata, nouri){"type": "resource_link", "uri": "acpmux-upload:ID", "name": NAME, "_meta": {"acpmux": {"upload": {"uploadId": ID, "fileName": NAME}}}}
The daemon that runs the chat resolves them (src/prompt_uploads.rs) before anything reads the prompt (server/requests.rs session/prompt), off the async runtime (spawn_blocking):
- An image that fits the agent's inline image size (about 3.75 MiB of bytes, up to 10 MiB of inline images per prompt) becomes an ordinary inline
imageblock, so transcripts, queue and ledger see what an inline sender would have sent. - Every other file, of any type and any size, goes to the agent by path: a
resource_linkwithuri: file://<path of the finished upload>, its name, MIME type (sniffed from the first bytes) and size. A text block (_meta.acpmux.context: true) lists the paths so the agent can open them. - No size or count cap: the 1 GiB
file_uploads::MAX_BYTESis gone from begin/chunk/finish; writing past the size declared at begin isfilePut.pastSize. The one-shotfile_putkeeps its old limit (it is one frame).
Rules:
- Only a finished upload in the chat's own uploads folder is read (
file_uploads::finished_pathandopen_finished: UUIDv7 id, plain file name, real folders, opened no-follow and owner-only, one hard link). Not there isupload.missing; there but unreadable isupload.unreadable. Aresource_linkwhoseuriis notacpmux-upload:<uploadId>(or that has the uri and no reference), animagewith auri, or aresourceblock isupload.invalid. - One message: inline images are counted as base64 against the rest of the prompt, the path note lists files while the message has room, and a prompt whose links alone pass one message is
upload.tooMany. - A remote web client cannot reference uploads (
upload.remote; it cannot upload either), and a person's waiting prompt that a web client edited is refused at its turn rather than handing their files to a turn the web client controls. - A prompt that waits (queued, held, restored) is refused with
upload.missingat its turn if a file it links to was swept meanwhile. Using an upload (a prompt naming it, a turn starting with it) writes a.usedmarker in its folder, under the same lock the sweep holds while it decides and deletes; the sweep removes only uploads made and last used before its cutoff, and a prompt whose marker cannot be written is refused (upload.unreadable). A message with uploads never joins another agent message's turn. - A queue edit cannot carry a reference (
attach.edit). - A resend under a known prompt id answers as a duplicate even when its uploads were swept (
hub.knows_prompt), and a queued or held prompt restored after a restart reads its uploads at dispatch (hub/turns.rs), refusing that one prompt if they are gone. - Advertised as
promptUploadsininitialize. A chat on a peer that does not list it is refused (upload.unsupported) before anything is sent; otherwise the prompt is forwarded as it is and the peer resolves it from its own uploads.
Not in this PR: binary upload frames, file_put_status resume, finish sha256 and the disk-full reason (next PR, flag uploadFrames); model-sized copies per harness; the app sender. The contract doc is #25774. About 950 changed lines, about 500 of them tests.
Risk: contained to prompts that carry _meta.acpmux.upload, and to chunked uploads over 1 GiB that were refused before. Every other prompt takes the same path as before (prompt_uploads::any is false).
Tests
tests/prompt_uploads.rs (8 integration tests):
fifty_uploaded_screenshots_reach_a_busy_chat_by_reference: 50 x 800 KiB PNGs uploaded in chunks, then named in one prompt to a chat mid-turn. No frame reaches 1 MiB and the prompt is accepted (queued) in under 1 s. When the turn ends the agent gets 12 inline and 38 by path, every one SHA-256 intact.a_large_image_and_any_other_file_go_by_path: a 6 MiB PNG and a binary file both arrive asresource_linkwith their paths, bytes intact.a_reference_outside_the_chats_finished_uploads_is_refused: another chat's upload, an unfinished upload, a traversal name, a bad id, a missing name, a mismatcheduri, aresourceblock, a web client, a queue edit; one prompt id sent twice runs once, and its resend after the uploads are swept still answers.an_upload_of_any_size_begins_and_takes_its_bytes: a 2 GB upload, past the old 1 GiB cap, begins and takes bytes; past its declared size isfilePut.pastSize.a_waiting_prompt_whose_upload_was_swept_is_refused_at_its_turn,an_upload_whose_use_cannot_be_marked_is_refused.a_peers_chat_reads_the_upload_on_the_peer,a_peer_that_cannot_read_uploads_is_refused_before_anything_is_sent.- Unit: the sweep keeps an old upload that was used recently; a resolved link under Web control (a web client's queue edit of a person's prompt) is refused
upload.remote.
The 2 GB end-to-end test (streamed, SHA-256 at the agent's path, accept time, peak RSS) is in the binary-frames PR, #25793's follow-up, since it streams the way the app will.
Results (cmux-ci test, 32-vCPU Linux):
feat-cmux-nextplus the first tests,30a880d68cfe(run 40414404415): 4 failed. The agent gets an image with no bytes (image:e3b0c442…, the SHA-256 of nothing), the bad references run, the old peer is sent the prompt.- With the caps still in (
588df63166ecplus this head's tests, run 40420600748): 3 failed. 50 references are refused, the 6 MiB image is refused, aresourceblock is accepted. - With the caps (base of the earlier no-cap run, run 40421912687): the 2 GB upload is refused
filePut.tooLarge("a file is at most 1024 MiB"). - This head
80a5e1e00930(run 40429351425):prompt_uploads8 passed,send_attach12,file_put7,durable_agents57, libfile_uploads/prompt_uploads/queue_edit/digest13,cargo clippy -p acpmux --all-targets -D warningsclean.
50 screenshots: uploaded and accepted in 251.810141ms, accept 66.1471ms
2 GB (same resolver, chunked upload, run 40421147638): uploaded in 15.3 s, accepted in 514 µs, sha256 7c0b9333…ecb41 intact at the agent's path, peak RSS 59 MiB
Agent session: agent session
🤖 Generated with Claude Code