acpmux: send --attach and send_message take files of any size and number, by reference

Merged
TE
11d ago
+1154-201

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 carries forPrompt: true, which a peer without promptUploads refuses (upload.unsupported). So no upload is left behind for a prompt that would be refused, --no-wait included.
  • 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_cancel with ownerToken deletes 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_begin with forPrompt writes a random token to the upload's .owner file and answers it as ownerToken. The sender's references carry it in _meta.acpmux.upload.owned, and only an upload whose .owner matches counts as the prompt's own (file_uploads::owned_dir). So a reference can't mark someone else's upload as its own, and owned: true deletes nothing. The tokens are read off the runtime. The daemon deletes owned uploads when the prompt is refused before the chat takes it: OwnedUploads covers the checks, and Saved covers 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_message asks the daemon first (_acpmux/subsession/send with attachCheck: true), so it reads no path for a send that would be refused. subsession_send refuses any attachments from such a caller with attach.remote before it reads or resolves them, and receive/resolve get the caller's Web state. This closes a hole that is already on base: send_message read a path as the agent's own user and sent it inline, with web=false.
    • The daemon applies the same check to every call from a connection that names its caller session, which is the token acpmux send and the MCP server send from an agent's environment (server/requests.rs caller_sends_no_files). It refuses attach.remote for session/prompt and _acpmux/send_queue (the --no-wait outbox) with file or upload-ref blocks, and for the upload calls (file_put, _begin, _chunk, _finish, _cancel). send_message with an empty attachments list is a text message and skips the check. So acpmux send --attach from such an agent's shell uploads and sends nothing. The CLI's attach errors now carry their reason.
  • A peer's chat gets the upload on the peer (peer/id).
  • send_message sends the reference blocks too: _acpmux/subsession/send accepts them and resolves them before the receiver's turn (hub/subsession_send.rs), as session/prompt does.

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.rs send_attach_sends_files_of_any_size_and_number_by_reference, against a real daemon:
    • acpmux send --attach with 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 as resource_link with 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.rs a_peer_that_does_not_save_attachments_is_refused_before_anything_is_sent: a begin with forPrompt to a peer without promptUploads is refused upload.unsupported and reaches nothing; a plain begin still goes. It fails on the previous head 281386808ce (run 40439544930).
  • send_attach.rs a_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 parent 7393c04a7ad (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 through subsession/send reaches its agent by path, bytes intact.
  • send_attach.rs a_withdrawn_queued_prompt_leaves_none_of_its_own_uploads_behind. Units: a_taken_prompt_that_never_ran_leaves_none_of_its_own_uploads, and a_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.rs an_agent_a_web_turn_drove_cannot_send_files_with_a_message: an inline file, an upload reference and the attachCheck are all refused attach.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.rs an_agent_a_web_turn_drove_sends_no_files_from_its_shell_or_its_tools runs the acpmux binary as that agent, with its caller token:
    • acpmux send --attach with a small file (inline), a 5 MiB file (by upload) and --no-wait each fail with the remote-device reason;
    • acpmux subsession-mcp send_message with a path that does not exist answers the attach.remote refusal, 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 head e19104ef2cb the inline acpmux send --attach succeeded (run 40465938084).
  • send_attach.rs a_refused_prompt_leaves_none_of_its_own_uploads_behind also sends owned: true and 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_token checks the same through a peer link.
  • send_attach.rs a_repeated_queued_send_deletes_only_its_own_new_uploads: a repeat of a delivered send_queue prompt 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_message with attachments: [] 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 passes check_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. On f6a414c2f74 the 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 base 9c6435d (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