Skip to content

acpmux: a prompt names its files as uploads, of any size, by reference - #25730

Merged
teamleaderleo merged 12 commits into
feat-cmux-nextfrom
hb-busy-accept/prompt-upload-refs
Oct 27, 2026
Merged

teamleaderleo merged 12 commits into
feat-cmux-nextfrom
hb-busy-accept/prompt-upload-refs

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

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) shows page host binding payload refused at 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.ts failedPrompts keeps {input, attachments, promptId}), so it was the same ~9 MB frame.
  • ChromiumPageHostBridge.swift:137 drops a page call over 4 MiB with only that log line and never answers, so the page's transport.send never settles and the row fails at the 60 s deliveryTiming.failMs.
  • That Mac's daemon has no record of 3baa5aa2 in 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 raw session/prompt got _acpmux/prompt_accepted in 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}}}} (no data, no uri)
  • {"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 image block, 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_link with uri: 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_BYTES is gone from begin/chunk/finish; writing past the size declared at begin is filePut.pastSize. The one-shot file_put keeps its old limit (it is one frame).

Rules:

  • Only a finished upload in the chat's own uploads folder is read (file_uploads::finished_path and open_finished: UUIDv7 id, plain file name, real folders, opened no-follow and owner-only, one hard link). Not there is upload.missing; there but unreadable is upload.unreadable. A resource_link whose uri is not acpmux-upload:<uploadId> (or that has the uri and no reference), an image with a uri, or a resource block is upload.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.missing at 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 .used marker 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 promptUploads in initialize. 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 as resource_link with 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 mismatched uri, a resource block, 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 is filePut.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-next plus 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 (588df63166ec plus this head's tests, run 40420600748): 3 failed. 50 references are refused, the 6 MiB image is refused, a resource block 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_uploads 8 passed, send_attach 12, file_put 7, durable_agents 57, lib file_uploads/prompt_uploads/queue_edit/digest 13, cargo clippy -p acpmux --all-targets -D warnings clean.
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

teamleaderleo and others added 2 commits October 26, 2026 15:41
… daemon that runs the chat

A block with _meta.acpmux.upload {uploadId, fileName} gets the bytes of that
finished upload of its own chat (file_put_begin/chunk/finish) before anything
reads the prompt, so screenshots no longer have to ride inline in one frame
(Leo's 8 screenshots never reached acpmux). Advertised as promptUploads; a peer
without it is refused before anything is sent; a queue edit cannot name one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 26, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-sleoo created for this PR by the cmux coordinator.

… restored or held prompt reads its uploads at dispatch

Review of #25730: resolution ran before the prompt-id ledger, so a resend after
the age sweep failed with upload.missing instead of the first outcome; and a
queued prompt restored from the log or a held agent message released went to
run_prompt without resolution. Tests for the size caps and the swept resend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
teamleaderleo and others added 4 commits October 26, 2026 16:21
… progress, refs, no caps)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…es to the agent by path

Leo, 2026-10-26: "I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE".
The resumable upload loses its 1 GiB cap (only the disk bounds it), and a
prompt's references lose the 32-file / 8 MiB / 32 MiB caps: an image goes
inline while it fits the model's input and the prompt's one-message room,
every other file (and a larger image) as a resource_link to its path, with a
note listing every path. Tests: 50 screenshots and a 2 GB file of no known
type reach a busy chat, accepted in under 1 s, bytes intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…d>; the contract says what exists, and how a page's bytes reach native

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…y the guard first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
Keeps this PR near 800 changed lines; the doc lands separately at the same path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo teamleaderleo changed the title acpmux: a prompt names its screenshots as uploads, so 20 send without one huge frame acpmux: a prompt names its files as uploads, of any size, by reference Oct 26, 2026
…n; uploads read only through real folders and one link; the prompt stays one message

Review fixes:
- A resolved link keeps its path in _meta.acpmux.uploadPath, and a prompt that
  waited (queued, held, restored) is refused with upload.missing at its turn
  when that file is gone.
- file_uploads::open_finished: the chat's and the upload's folders must be real
  folders, and the file must have one hard link. A read failure other than not
  found is upload.unreadable, not upload.missing.
- Inline room is counted as base64 and against the rest of the prompt; the
  context note lists files while the message has room, and a prompt whose links
  alone pass one message is upload.tooMany.
- The 2 GB test moves to the binary-frames PR; a 2 GB begin plus chunk and the
  pastSize refusal are checked here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
…rson's files, and the sweep keeps uploads in use

Review fixes:
- A resource_link whose uri is acpmux-upload:<id> counts as a reference even
  without _meta.acpmux.upload, so it is refused as upload.invalid (and gated
  for an older peer) instead of reaching an agent unresolved.
- A prompt under Web control that carries a link this daemon resolved
  (uploadPath) is refused with upload.remote at its turn: a web client's queue
  edit keeps the person's resolved blocks.
- Resolving a reference, and starting a turn with its link, marks the upload's
  folder as used; the age sweep deletes only uploads made and last used before
  its cutoff. still_there checks only paths inside the uploads folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@blacksmith-sh

blacksmith-sh Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)/
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)
View Logs
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)/
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 80a5e1e: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr25730 -f ref=hb-busy-accept/prompt-upload-refs -f platform=macos -f backend_mode=local

web and size checks: green

…n; upload messages never join another turn

Review fixes:
- The sweep decides on and deletes each upload under file_uploads::IN_USE,
  and resolving a reference or starting a turn with its link marks it used and
  checks it under the same lock. The last use is a .used marker file, written
  owner-only, so it works on every platform (no folder mtime).
- A message that names or links uploads never joins another agent message's
  turn, and none joins it: its own turn reads and checks its files.
- A resolved link under Web control is refused (unit test); the integration
  test that could pass without reaching that guard is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
… never takes a file a turn needs

Review fix: file_uploads::touch returns its error, and resolving a reference
or starting a turn with its link refuses the prompt (upload.unreadable) when
the .used marker cannot be written. Test: a folder in the marker's place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@teamleaderleo
teamleaderleo marked this pull request as ready for review October 27, 2026 01:43
@teamleaderleo
teamleaderleo merged commit 21f1b02 into feat-cmux-next Oct 27, 2026
158 of 161 checks passed
@teamleaderleo
teamleaderleo deleted the hb-busy-accept/prompt-upload-refs branch October 27, 2026 01:43
teamleaderleo added a commit that referenced this pull request Oct 27, 2026
…ence, no caps)

Split out of #25730 so that PR stays near 800 lines. Describes the daemon-facing
upload API, the upload-ref block shapes, what the agent receives, the binary
data-frame transport still to be built, and how bytes held by a page reach native.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 27, 2026
…ence) (#25774)

* acpmux docs: the prompt-uploads contract (files of any size, by reference, no caps)

Split out of #25730 so that PR stays near 800 lines. Describes the daemon-facing
upload API, the upload-ref block shapes, what the agent receives, the binary
data-frame transport still to be built, and how bytes held by a page reach native.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads split resume (uploadResume) from binary frames (uploadFrames); new refusal reasons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads section 0 names the PRs that carry each part

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads: text after frames is ordered, begin forPrompt, filePutChunks on peers, status keeps an upload

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads 1b: uploads.begin carries promptId and ticket, data goes as file_put_chunk until uploadFrames, and the ticket stays page-to-native

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads 1b: upload states (progress never terminal, one terminal state per token, status, cancel, the pending id, a late ack), carriers, and the sweep reaps unfinished uploads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads 1b: a token is one upload attempt; a failed attachment is retried under a new token

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads: cancel deletes a finished upload, and owned references (#25840)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux docs: prompt uploads: files have no cap; one prompt's own references still fit one message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* acpmux prompt-uploads doc: every upload flag and its peer gate, the two corrupt finishes, the context note's "and N more"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

3 inactive deployments
artifacts — 80a5e1e0 Deployed Oct 27, 2026 by teamleaderleo via publish tree-addressed cmux-tui #61239
sentry-dsyms — 80a5e1e0 Deployed Oct 27, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #61239
ci-routing-agent — 80a5e1e0 Deployed Oct 27, 2026 by teamleaderleo via wait for same-key tree publisher #61239
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant