acpmux: send --attach and send_message take files of any size and number, by reference
Leo, 2026-10-26: "why the FUCK SHOULD THERE BE A FUCKING SIZE CAP ANYWAYS JUST FUCKING / I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE"
Base feat-cmux-next. It builds on #25793 (resumable uploads, upload.diskFull with host and path, merged as e6adb7f3d3d).
After #25730 the prompt path had no cap, but acpmux send --attach and cmux-subsessions send_message attachments still refused a message: 8 MiB a file, 10 MiB and 16 files a message, and "one message is at most 15 MiB" (attach.rs).
Change
The senders (attach::blocks_for) never refuse a file for its size or their number:
- Files that fit one message with the text go inline as before (
fits_inline: at most 16 files, each at most the inline image size of 3.75 MiB, 10 MiB together, and the whole message as base64 within one JSON-RPC message). Old receivers keep working for these. - Otherwise each file is uploaded to the receiving chat first with
_acpmux/file_put_begin/_chunk/_finish(512 KiB chunks read from disk as they go, so memory stays flat), and the prompt names it by reference (#25730). The daemon that runs the chat puts an image the agent can take inline and gives every other file, and an image too large to show, by its path. A failed upload is cancelled. - Before any byte is sent it checks that the uploads will be read. The local daemon must list
promptUploads; if it does not, the error names its build and says to restart it. The begin carriesforPrompt: true, which a peer withoutpromptUploadsrefuses (upload.unsupported). So no upload is left behind for a prompt that would be refused,--no-waitincluded. - Nothing is left behind:
- The sender sends every file's bytes before it finishes any. If a file fails, it cancels every upload it began.
file_put_cancelwithownerTokendeletes an upload begun for a prompt whole, finished or not, and only with the matching token, here or through a peer. Any other upload is cancelled only while unfinished, as before.file_put_beginwithforPromptwrites a random token to the upload's.ownerfile and answers it asownerToken. The sender's references carry it in_meta.acpmux.upload.owned, and only an upload whose.ownermatches counts as the prompt's own (file_uploads::owned_dir). So a reference can't mark someone else's upload as its own, andowned: truedeletes nothing. The tokens are read off the runtime. The daemon deletes owned uploads when the prompt is refused before the chat takes it:OwnedUploadscovers the checks, andSavedcovers the chat's own refusal or an unanswered drop. A prompt the chat takes, a duplicate, and a held prompt keep them.- The outbox deletes the owned uploads of a send that it answered with an earlier delivery of the same prompt id. It keeps every upload the earlier delivery names, by reference or by a link resolved to its path (split on
/and\alike, so a Windows peer's path counts too), whatever the spelling of its id, and each cancel carries the owner token. - A message an agent pair holds keeps its owned uploads together with its inline folder (
held_dirs). They are deleted when the held message is dropped, or when its release never ran. - A prompt the chat took and then withdrew from its queue, or refused when its turn came, deletes its owned uploads too.
- Deleting an upload takes the sweep's lock and then that upload's own lock (
file_uploads::remove_uploads).
- An agent a remote device drives (a Web turn, or a last turn that was one) sends no files with a message.
send_messageasks the daemon first (_acpmux/subsession/sendwithattachCheck: true), so it reads no path for a send that would be refused.subsession_sendrefuses any attachments from such a caller withattach.remotebefore it reads or resolves them, andreceive/resolveget the caller's Web state. This closes a hole that is already on base:send_messageread a path as the agent's own user and sent it inline, withweb=false.- The daemon applies the same check to every call from a connection that names its caller session, which is the token
acpmux sendand the MCP server send from an agent's environment (server/requests.rscaller_sends_no_files). It refusesattach.remoteforsession/promptand_acpmux/send_queue(the--no-waitoutbox) with file or upload-ref blocks, and for the upload calls (file_put,_begin,_chunk,_finish,_cancel).send_messagewith an emptyattachmentslist is a text message and skips the check. Soacpmux send --attachfrom such an agent's shell uploads and sends nothing. The CLI's attach errors now carry their reason.
- The daemon applies the same check to every call from a connection that names its caller session, which is the token
- A peer's chat gets the upload on the peer (
peer/id). send_messagesends the reference blocks too:_acpmux/subsession/sendaccepts them and resolves them before the receiver's turn (hub/subsession_send.rs), assession/promptdoes.
On the receiving side the MAX_FILE_BYTES, MAX_MESSAGE_BYTES and MAX_FILES refusals are gone (check_sizes deleted). check_wire now checks only that a raw protocol request with inline files fits one JSON-RPC message on the next hop, so it gets an answer instead of being dropped by a hop. Neither sender can reach that check, since both switch to references first. Its message now says to send the files as uploads.
The send_message schema loses maxItems and the cap sentence. --attach help, the README and the skill say "any size and number".
Not in this PR: the app composer's own attachment limit (home.composer.attach.tooMany, Swift), which belongs to the composer owner.
Risk: contained to sends with attachments. Small attachments take the same inline path as before. Larger ones need the receiving daemon to resolve references (#25730, promptUploads). An older peer is refused with upload.unsupported, naming the peer and acpmux host update, where before it was refused for size.
Tests
send_attach_cli.rssend_attach_sends_files_of_any_size_and_number_by_reference, against a real daemon:acpmux send --attachwith a 24 MiB binary, a 6 MiB PNG and 20 screenshots (22 files). The screenshots reach the agent as images; the binary and the large PNG arrive asresource_linkwith their paths. SHA-256 is intact and the files are owner-only.- 5 MiB of text plus an 8 MiB file (past one frame) also goes through.
send_attach.rsa_peer_that_does_not_save_attachments_is_refused_before_anything_is_sent: a begin withforPromptto a peer withoutpromptUploadsis refusedupload.unsupportedand reaches nothing; a plain begin still goes. It fails on the previous head281386808ce(run 40439544930).send_attach.rsa_refused_prompt_leaves_none_of_its_own_uploads_behind: covers a prompt refused by its checks, one the chat refuses (steer.unavailable), one that runs (which keeps its upload), and a cancel of a finished upload. It fails on its parent7393c04a7ad(run 40445698527).send_attach.rs: inline files past the old caps (8 MiB + 1, two halves of 10 MiB, 20 files) are saved, including through--no-wait(outbox) and to a peer's chat. A web client still cannot attach.send_attach_family.rs: a 6 MiB PNG uploaded to a child and sent by reference throughsubsession/sendreaches its agent by path, bytes intact.send_attach.rsa_withdrawn_queued_prompt_leaves_none_of_its_own_uploads_behind. Units:a_taken_prompt_that_never_ran_leaves_none_of_its_own_uploads, anda_held_message_keeps_its_files_until_it_is_dropped_or_its_release_never_ran, which now carries an owned upload. All three fail on 77fdf1aa9a4 (run 40452127624).subsessions_safety.rsan_agent_a_web_turn_drove_cannot_send_files_with_a_message: an inline file, an upload reference and theattachCheckare all refusedattach.remote, and nothing reaches the child. Before the fix the inline send was accepted and started a turn in the child (run 40459662099).subsessions_safety.rsan_agent_a_web_turn_drove_sends_no_files_from_its_shell_or_its_toolsruns theacpmuxbinary as that agent, with its caller token:acpmux send --attachwith a small file (inline), a 5 MiB file (by upload) and--no-waiteach fail with the remote-device reason;acpmux subsession-mcpsend_messagewith a path that does not exist answers theattach.remoterefusal, not file-not-found, so the path is never read;- the child gets only its start prompt, and no upload folder is made for it.
On the previous heade19104ef2cbthe inlineacpmux send --attachsucceeded (run 40465938084).
send_attach.rsa_refused_prompt_leaves_none_of_its_own_uploads_behindalso sendsowned: trueand a wrong token for an existing upload: the upload stays. A cancel with no token or a wrong one leaves a finished upload, and one with its token deletes it.a_cancel_through_a_peer_deletes_only_with_the_owner_tokenchecks the same through a peer link.send_attach.rsa_repeated_queued_send_deletes_only_its_own_new_uploads: a repeat of a deliveredsend_queueprompt id deletes the upload it began. It keeps the delivered prompt's upload, even named in upper case, an upload it names with a forged token, and an upload the delivered prompt links to by path. With the link not counted, it fails (run 40470385633).subsessions_safety.rs:send_messagewithattachments: []from the Web-driven agent is sent as a text message.- On
e5eeb6e9401(tests only, run 40469083026) all four new checks fail: the cancel without a token deleted the upload (here and through the peer), the repeat deleted the delivered prompt's upload, and the empty list was refused. - Unit:
small_files_go_inline_and_the_rest_by_reference(each threshold; every inline message the senders make passescheck_wire).
Results (cmux-ci test, 32-vCPU Linux):
- The tests on #25730's head (
edc86435fa5, test commit only; run 40433931184): 5 failed.send_attach_cli:22 attachments: a message takes at most 16.send_attach: 3 (past the old caps, queued, peer).send_attach_family: the by-reference block is refused as not an attachment. - Unit
a_link_names_its_upload_on_any_platform: a POSIX path and a Windows path each name their upload. Onf6a414c2f74the Windows one is missed (run 40472624765). - Head
a0de7dc2085: fmt, clippy, the prompt_uploads units and send_attach pass (run 40472623355).f6a414c2f74, which differs only in that function, passes the full acpmux suite except the six base failures (run 40471492169), and the attach, upload, outbox and Web-turn tests (run 40470388422). 474370107ab(run 40469087813, full acpmux suite, fmt and clippy): fmt and clippy are clean, and every attach, upload, prompt-upload and Web-turn test passes. The only failures are the six that fail the same way on base9c6435d(run 40453283684):own_servers_run_under_the_daemon_prefix,hermetic_daemon_guardĆ2,daemon_chat_open,ephemeral_run,pooled_sessions_end_with_the_daemon_and_after_a_crash.
Size: 654+/144ā in source and 485+/55ā in tests. That is over the usual 800 lines, because the review rounds added the owned-upload lifecycle, its owner token and the Web-control fix. The fix changes the same subsession_send lines, so it could not land on its own.
Agent session: agent session
š¤ Generated with Claude Code