Skip to content

carry of #27073: NEXT: Cloud and peer chat uploads take files of any size - #28695

Merged
teamleaderleo merged 33 commits into
feat-cmux-nextfrom
lane/carry-27073-attach-uncap
Nov 1, 2026
Merged

teamleaderleo merged 33 commits into
feat-cmux-nextfrom
lane/carry-27073-attach-uncap

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Carry of #27073 (its owner's host can't be reached; idle since 2026-10-30T04:06Z). It keeps the original commits and their authorship, merges current feat-cmux-next, and fixes what blocked the last two reviews.

Leo's words this serves: "arbitarrily large file sizes" (2026-10-25) and "why the F*** SHOULD THERE BE A F***ING SIZE CAP ANYWAYS ... I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE" (2026-10-26).

What the original does (still missing on base)

Base already streams Home bucket multipart uploads of any size (#26673), phone uploads (#26954) and native staged uploads into local chats (#26409). Uploads into Cloud and ssh-peer chats still had caps until this PR:

  • Cloud chats (CloudUploads.swift): the 32 MiB per-file and 64 MiB per-batch caps are gone, along with the "won't send that file" refusal text (cloud.upload.refused removed from Cloud.xcstrings). cmux link access upload (access_cli/uploads.rs) drops its 1 GiB MAX_FILE_BYTES and prints a progress heartbeat after each remote write. CloudAccessCLI runs the upload under a 30 s idle deadline (ProcessRunner progressIdle, with timeout now optional) and keeps only a bounded tail of its output. Each staged transfer gets an app-chosen --upload-id, and a new upload-cancel removes exactly that remote directory when the CLI fails or is cancelled.
  • Peer chats (AgentPeerUploads.swift, AgentPaneFilePut.swift): maximumPeerFileBytes and the 1 GiB guards are gone. Only the peer can refuse a file as too large. The peer-side cancel runs from a fresh task, so a cancelled put still reaches the peer.
  • Decoder (AgentPaneUpload.swift): chat.upload drops maximumFileBytes / maximumTotalBytes. A batch (AgentPaneUploadBatch) stays authorized while any of its stages lives.
  • Page (cloudUploads.ts, attachmentIntake.tsx, intakeFiles.ts): the chip's × aborts the finish while it is in flight. Finish has no page deadline because native owns the idle deadline. The page awaits the cancel until the host has removed its stage.

What this carry fixes

  1. Compile (finding 1, AgentPeerUploads.swift:76) and compile (finding 2, CloudUploads.swift:159): the Stage initializers left out expiry/cleanup. Both stagers now build their stages in one shared store (item 4), so those initializers no longer exist. Commit 77a24bb first named every field.

  2. Compile after the base merge: base's Keep recordings dropped from the macOS capture thumbnail #24473 (recording drops from the macOS capture thumbnail) read AgentPaneUpload.maximumFileBytes/maximumTotalBytes, which this PR removes. That snapshot reads the recording into memory before the save, so it keeps the same 1 GiB as its own named in-memory budget: AgentPaneRecordingSnapshot.maximumPinnedBytes / maximumPinnedTotalBytes (AgentPaneRecordingSnapshot.swift, AgentPaneDroppedPaths.swift:58, AgentPaneRecordingDropTests.swift:167). Ordinary files dropped into a local chat still go as links to their real paths, with no cap.

  3. Attachment cap guard (scripts/cmux-next/check-attachment-caps.py): it still exempted the caps this PR removes (Cloud, peer, decoder, CLI MAX_FILE_BYTES). The new test test_retained_bounds_name_only_symbols_their_file_still_declares (tests/test_attachment_caps_guard.py) fails while an exemption names a cap its file no longer declares. Those exemptions are now gone, so a removed cap can't come back unreviewed.

  4. One staged-upload store (CmuxNextAgentPane/AgentPaneStagedUploads.swift): each App stager kept its own stages, batches, expiry deadlines and cleanup tasks. That added +131 CmuxNextApp lines, which the pre-push arch ratchet refuses, plus a static mutable singleton. AgentPaneStagedUploads<Target> now owns all of it once:

    • a stage of any size;
    • batch authorization;
    • one DemandTimer for abandoned stages and batches (the peer stager used to sleep a task per stage);
    • a transfer that owns its file until it returns;
    • a cancel that stops the transfer before the staged file is removed.

    Each stager keeps only its own target checks, file layout and transfer. The peer store lives on CloudService.peerUploads, beside uploads. Net effect: CmuxNextApp is -48 lines and has 2 fewer global mutable statics.

  5. Review of this carry (Astra, BLOCK at 6ff0fa8), fixed in f6edd0e:

    • The older one-frame upload sent every file in one CLI call under no upload id. So more than 64 files were refused, and a failed or cancelled call left files on the machine. CloudAccessCLI.upload now sends each file in its own call under an id of the app's. A failure or cancel removes every file of that upload (removeUploads) before it answers.
    • A cancel that arrived after the file was delivered (the chip's × while the answer is on its way) found no stage. The store now keeps a finished stage's undo for its lifetime, and the Cloud undo removes that upload id. A finished peer put has no remote delete, so the peer path has no undo.
    • Second round (BLOCK at f6edd0e), fixed in f35ace8 / b33d5cd: removeUploads now tries every id and throws after all of them, and the undo test compares undone before and after each wrong-target cancel.

Review: codex gpt-6-astra (high, fast) SHIP at 9d4b5d8 (after a conflicting base merge with #27012, #28360 and #27768), receipts in the comments.

Not in this carry: findings 3 (backend/apps/home-server/src/fs-bucket.ts:345, /dev/fd/<dir>/child on macOS) and 4 (cmux-tui/crates/acpmux/src/cli/cloud_join.rs, a repeated join resetting meshRelay). Both are real on base, but this PR doesn't change either file. They came in through base merges (#26673 and the acpmux join work), so each goes in its own PR.

Overlap: open #27200 (cap guard / AgentChatUploads), #26958 and #27768 (intakeFiles.ts) change some of the same files for other fixes; whichever lands second takes a base merge.

Verification

  • python3 tests/test_attachment_caps_guard.py: the new test was red at 472d2cd (9 stale exemptions) and is green at the head (14 tests). python3 scripts/cmux-next/check-attachment-caps.py: clean.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations. arch-ratchet reports app_lines -48 and global_mutable_statics -2.
  • New Swift tests: AgentPaneStagedUploadsTests (begin of any size, batch order and single use, chunk ownership and order, cancel during transfer stops it before the file goes). The original's CloudUploadsTests, AgentPeerUploadsTests, ProcessRunner*Tests and uploads_tests.rs are kept.

Observed / Not run

  • Observed: the guard test red → green, the guard itself, the pre-push gate.
  • Not run here: Swift compile and suites (Linux lane VM). They come from this PR's fleet CI, and the result is in a comment below. The Rust uploads_tests.rs and scripts/ci/proofs/upload-cancel.py run through CI.

No capture: no UI change. The only visible change is that a large file now uploads where it used to be refused; the composer chip is the same.

Changelog

  • Changed: files of any size can be dropped or pasted into chats on a Cloud machine or an ssh peer.

risk: normal (upload paths; no auth or relay policy change).

Agent session: agent session

🤖 Generated with Claude Code

teamleaderleo and others added 24 commits October 28, 2026 22:04
Leo, 2026-10-26: "WHY THE FUCK SHOULD THERE BE A FUCKING SIZE CAP ANYWAYS
... I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE".

These fail on base, which caps Cloud uploads at 32 MiB a file and 64 MiB
a batch and peer uploads at 1 GiB:
- CloudUploadsTests: a staged Cloud begin past 32 MiB, and a batch past
  the 64 MiB total, are taken; no language of cloud.upload.failed or
  cloud.upload.refused carries a placeholder or a number.
- AgentPeerUploadsTests: an inline peer upload past 1 GiB is sent; a
  staged peer begin past 1 GiB, alone or as a batch's first file, and a
  batch past the 1 GiB total, are taken.
- AgentPaneFilePutTests: a chunked put past 1 GiB sends its begin.
Sizes are only declared (Data(count:) or a declared size); no bytes are
written.

To test the Cloud begin without a live Cloud machine, its admission
check moves, unchanged, into CloudUploads.beginRefusal(name:size:manifest:).
The old test that the refusal names the limits is dropped.

Split out of #27016 (Cloud and peer stager caps); the peer test drop
and the "refusal names no limit" idea come from #27016.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
Leo, 2026-10-26: "WHY THE FUCK SHOULD THERE BE A FUCKING SIZE CAP ANYWAYS
... I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE".

Cloud (CloudUploads): drop maximumFileBytes (32 MiB) and
maximumTotalBytes (64 MiB), the guard in upload(_:to:services:), and in
a staged begin the per-file size check, the manifest's per-file and
total checks and the per-session staged total. A begin still needs a
non-negative size and a manifest that starts with the file begun.

Peer (acpmux file_put): drop AgentPaneUpload.maximumFileBytes,
maximumTotalBytes and maximumPeerFileBytes (1 GiB), the four guards in
AgentPaneFilePut, the begin and manifest checks in AgentPeerUploads'
stager and the guard in AgentPeerUploads.upload, and the base64 and
total limits in AgentPaneUpload.files. A peer that refuses a file
(filePut.tooLarge) still says so.

The chunk bound in both stagers is written size - offset, so a huge
declared size cannot overflow it.

Localization: cloud.upload.refused named the limits ("up to %1$@ each,
%2$@ in all") in every locale. There are no limits to name, so the key
and CloudUploadStrings.refused are gone; a refusal from the machine
shows cloud.upload.failed ("Couldn't send the file to the Cloud
machine."), already translated in every locale, and keeps its
upload_refused code for the page.

Split out of #27016 (Cloud and peer stager caps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
…eer (red)

Review of #27073: the request decoder (AgentPaneRequest+Decoding) still
refuses a staged begin, or a manifest entry, over 1 GiB, and a manifest
of more than 64 files, before CloudUploads or AgentPeerUploads see it.
The stager tests called the stagers directly and missed it.

These fail on this head:
- AgentPeerUploadsTests.aDecodedBeginOfAnySizeReachesThePeerStager and
  CloudUploadsTests.aDecodedBeginOfAnySizeReachesTheCloudStager (2 GiB
  and 8 GiB): a chat.upload.begin decoded through AgentPaneRequest(body:),
  the bridge's own decoding, alone, as a batch's first file, and as a
  small first file whose manifest names a file over 1 GiB, must decode
  into the staged begin and be taken by the peer stager and the Cloud
  stager's begin check (DecodedStagedBegins builds the requests).
- AgentPaneUploadTests.stagedUploadAcceptsAnyNonnegativeFileSize and
  stagedUploadAccepts65LargeFiles, with
  stagedUploadRejectsNegativeFileAndBatchSizes, taken verbatim from
  #26409 so the two PRs' test hunks are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
Leo, 2026-10-26: "WHY THE FUCK SHOULD THERE BE A FUCKING SIZE CAP ANYWAYS
... I ALREADY SAID WE NEED ARBITARRILY LARGE FILES OF ANY TYPE".

AgentPaneRequest+Decoding: drop maximumUploadStageFile (1 GiB) from a
chat.upload.begin's size and each manifest entry's size, and the
64-entry manifest limit. A begin still needs a file name and a
non-negative size; a manifest still needs one or more well-formed
entries. A chunk stays at most 512 KiB.

This is #26409's change to the same file, line for line (its
3813876 and 951315e), so whichever of #26409
and #27073 lands second merges it cleanly. Cloud and peer begins do not
pass through AgentChatUploads (CloudHandlers+Move tries Cloud, then
peer, then the local stager), so the local stager's caps stay #26409's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
`cmux link access upload` (the Cloud chat's finish step) still refuses a
file over 1 GiB, so a file the stager took whole fails at the end with
upload_refused. The test pastes a sparse 1 GiB + 1 byte file and uploads
it to a fake fs-v1 machine: it must be accepted and arrive whole, in
parts of at most 8 MiB (overwrite, then appends).

Leo: "Attachments take arbitrarily large files of any type; there is no
size cap".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
`cmux link access upload` refused any file over 1 GiB (MAX_FILE_BYTES),
so a Cloud chat upload the stager took whole still failed at finish with
upload_refused. The cap is gone. The file was already streamed: copy_file
hands an open file to Remote::write, which reads and sends it in 8 MiB
fs.write parts (overwrite, then appends to a temporary name, then a
rename), so nothing reads the whole file into memory.

The uploads.rs change is taken byte-identical from #27016 (launch-polish),
so the two merge cleanly; credit to that PR.

Leo: "Attachments take arbitrarily large files of any type; there is no
size cap".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3f783e95b72165fe52b9
…ncap-cloud-peer-stager

# Conflicts:
#	Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentPaneUploadTests.swift
Cloud and peer uploads no longer declare their file and batch caps, nor the
CLI its MAX_FILE_BYTES, so their exemptions go; AgentPaneDraftAttachments'
maximumBytes was already gone on base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-68ba07270dde07b96ff4
…every field

Base's #24473 read AgentPaneUpload.maximumFileBytes and maximumTotalBytes for
the macOS capture-thumbnail recording snapshot, which this branch removes, so
the merged tree did not compile. The snapshot reads the recording into memory
before saving it, so it keeps the same 1 GiB as its own named budget
(AgentPaneRecordingSnapshot.maximumPinnedBytes / maximumPinnedTotalBytes).
The Cloud and peer Stage initializers name expiry and cleanup explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-68ba07270dde07b96ff4
The two App stagers each kept their own stages, batches, expiry deadlines and
cleanup tasks (+131 CmuxNextApp lines, which the arch ratchet refuses).
AgentPaneStagedUploads<Target> now owns that once: a stage of any size, a
batch authorized while its stages live, one DemandTimer for abandoned stages
and batches (the peer stager no longer sleeps a task per stage), a transfer
that owns its file until it returns, and a cancel that stops the transfer
before the staged file is removed. Each App stager keeps only its own target
checks, file layout and transfer. AgentPaneStagedUploadsTests covers begin of
any size, batch order, chunk ownership, and cancel during the transfer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-68ba07270dde07b96ff4
…ered file back

Review of the carry (Astra, BLOCK at 6ff0fa8):
- The older one-frame upload sent every file in one CLI call, under no upload
  id: more than 64 files were refused, and a failed or cancelled call left its
  partial file and the files already sent on the machine. CloudAccessCLI.upload
  now sends each file in its own call under an id of the app's, and a failure or
  a cancel removes every file of the upload (removeUploads) before it answers.
- A cancel that came after the transfer delivered the file (the chip's x while
  the answer was on its way) found no stage and left the file on the machine.
  AgentPaneStagedUploads keeps a finished stage's undo for its lifetime; the
  Cloud stager's undo removes that upload id. Peers have no remote delete yet.
- CloudBringBackTests expects the upload id on the CLI call (the original
  already sent it for one file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-68ba07270dde07b96ff4
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 5a7c47e: 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=pr28695 -f ref=lane/carry-27073-attach-uncap -f platform=macos -f backend_mode=local

Gallery before/after for 9d4b5d8: 16 states rendered, 0 images differ, 1069 more touched states not rendered. Images are in the run's artifact, not on this public PR.

gh run download 41158980803 -R manaflow-ai/cmux -n gallery-advisory-28695-9d4b5d8de360c661a31cf906fe98870e33ddc097 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.chat-width/standard Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.chat-width/wide Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-14 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-14 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-14 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-14 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-tab-14 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-tab-14 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-21 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-21 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-21 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-21 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-tab-21 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-tab-21 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-28 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-chat-28 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-28 Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer/font-size-new-chat-28 Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/short-edge Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% imports cloudUploads.ts (1 hops)
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% imports cloudUploads.ts (1 hops)

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Not verified: macOS jobs did not run: compile and app tests were skipped on 5a7c47eeba; ci-status is green without them.

CI passes on 5a7c47eeba (run 41264410275 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

web and size checks: green

Default changes

This pull request changes 9 defaults at 5a7c47eebafc (what everyone gets without configuring anything):

  • tunable-source.CmuxNextAgentPane/AgentPaneUpload.swift:maximumFileBytes: "5cfe6f663ddb24fb569543534f2cbd74c553f8f92cf6874bfb767748df2bcafa" -> (none)
  • tunable-source.CmuxNextAgentPane/AgentPaneUpload.swift:maximumPeerFileBytes: "f443b2319209bbfd903588c5f99d77210a39372e5dc219dfb8d30b7a6794025c" -> (none)
  • tunable-source.CmuxNextAgentPane/AgentPaneUpload.swift:maximumTotalBytes: "b8451d4304fca51003d8eccf684b340a651b50f81c33bd4363271a84dd4dd1b0" -> (none)
  • tunable-source.CmuxNextApp/AgentPeerUploads.swift:batches: "8829d8e40589db7dfd5bec20d330f92a78e0a0ddc7ef718fb7f3a9ad0ab76655" -> (none)
  • tunable-source.CmuxNextApp/AgentPeerUploads.swift:stager: "e3c5946322dd3a1b63caa0d83a239766454d8c135dc09e0becb1d2f1d47bf419" -> "49b45abf4644ef7dad7f1e60797ddc570328d57e85595eef7fca480dcd35bd9f"
  • tunable-source.CmuxNextApp/AgentPeerUploads.swift:stages: "5a4275740bbfbed536668594f8d4f003b3904b1f89d64510e2c8ebf5d2b4c8ae" -> (none)
  • tunable-source.CmuxNextApp/AgentPeerUploads.swift:upload: "c7af7ac535582330be549b5e5ab78451ce34629192ef2633ba7d54fcb09822be" -> "9025075bf84cec29a8837f3390d5566a1529f482926c5e85dbfdc823026453e5"
  • tunable-source.CmuxNextDaemon/Launch/ProcessRunner.swift:run: "ee0436289e23ce60916b4391e7ada371f392fe1d1935b63b8f2f06219eae57a1" -> "fd85888a316628139f184932acf01b821cdd687b99689d82f35a7f105427ea8b"
  • tunable-source.CmuxNextDaemon/Launch/ProcessRunner.swift:waitForDeadline: (none) -> "c0ed31c8684cda85a9e0020f9095b3eb222270881f8efca06d4aafeb324d1372"

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at 5a7c47eebafcfd1b97a6bf182f43d90ec16e87d6, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @lawrencecchen or @austinywang (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Merge receipt: merged as bc0f8f4f98e7 with these defaults changed, WITHOUT a SHIP receipt or an approving call owner review on its head.

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

…es before and after

Review of the carry (Astra, BLOCK at f6edd0e): removeUploads stopped at the
first failed removal, leaving later files on the machine; it now tries every
id and throws after. The late-cancel test expected no undo at all on its
second file, though the first had rightly been undone.

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

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high fast, direct codex exec): SHIP at 2058cd2

Round 5, a delta from 9d4b5d8 (SHIP). The fleet test compile refused AgentPaneStagedUploadsTests.swift:97: the undo closure appended to a captured var ("converting non-Sendable function value to '@mainactor @sendable () async -> Void'"). The test now logs to a @MainActor final class UndoLog. AgentPaneStagedUploads.swift imports CmuxNextWakeups and Foundation as package (the type is package), which clears the compiler's "public import not used" warning. The review found no remaining blockers; git diff --check passes.

@blacksmith-sh

blacksmith-sh Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Found 82 test failures on Blacksmith runners:

Failures

Test View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs
---/--- View Logs

...and 72 more test failures. View all on Blacksmith

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Orphaned: no activity for 12 h, and no lane holds a lease on it. The orphan sweep could not merge feat-cmux-next in: conflicts in scripts/cmux-next/check-attachment-caps.py, webviews/src/agent-session/acpmux/composer/attachmentIntake.tsx. Listed for a lane to resolve.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · took over from an earlier session · session

Base's #29626 dropped the floating-preview pin cap (APFS clone, any size), so its
AgentPaneDroppedPaths, AgentPaneRecordingSnapshot and their drop test win over this
branch's renamed 1 GiB pin budget. The webview upload signatures keep both sides:
this branch's subscribable UploadCancelled and base's per-file progress reporter.

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

Copy link
Copy Markdown
Collaborator Author

Review (codex-review-box/gpt-6-astra high, lane-place): BLOCK at b3a0750

BLOCK at b3a0750

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] check-attachment-caps.py:69: A surviving exemption still names MAX_FILE_BYTES, removed from access_cli/uploads.rs. Running python3 tests/test_attachment_caps_guard.py produces 1 failure in 19 tests. This command runs in ci-guards.yml:195, so CI fails. Remove the stale exemption.

  2. [P2] [follow-up] attachmentIntake.tsx:267: No after screenshots accompany the composer cancellation changes. Verify uploading and cancellation visually in the build.

The standalone attachment-cap guard passes. Swift, Rust, and Bun suites could not run because their toolchains are unavailable in this box.

Base removed MAX_FILE_BYTES from access_cli/uploads.rs, so the guard's
exemption named a cap that no longer exists and
tests/test_attachment_caps_guard.py failed (1 of 19).
python3 -I tests/test_attachment_caps_guard.py: 19 OK.

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

Copy link
Copy Markdown
Collaborator Author

Review (codex-review-box/gpt-6-astra high, lane-place): BLOCK at 2d4e117

BLOCK at 2d4e117

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] CloudAccessCLI.swift:115: The new 30-second idle timeout kills healthy slow uploads. Heartbeats occur only after a complete remote write returns, with chunks up to 8 MiB. A native dictation upload whose first write takes 45 seconds previously fit the 120-second timeout; now it fails at 30 seconds despite transferring bytes. Report progress during transmission or accommodate the transport’s existing deadlines.

  2. [P2] [follow-up] attachmentIntake.tsx:267: No after screenshots were supplied. Capture uploading, cancellation, and retry states for visual review.

Validation: 19 guard tests and four Node/Babel smoke checks passed. Bun, Swift, and Rust suites were unavailable in this box.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 issues found across 32 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ProcessRunnerProgressDeadlineTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ProcessRunnerProgressDeadlineTests.swift:79">
P2: This test passes even if the up-front deadline guard is removed, because a missing executable also throws a `DaemonError`. Assert the exact case so the test verifies the refusal happens before launch.</violation>
</file>

<file name="tests/test_attachment_caps_guard.py">

<violation number="1" location="tests/test_attachment_caps_guard.py:51">
P2: This check only tests that each exempted name appears somewhere in the file, not that it is declared. `maximumSize` in AttachmentMedia.swift is satisfied by the AVFoundation property assignment `generator.maximumSize = ...`, so the exemption survives after the cap is removed. Match a declaration form (for example, the preceding token is `let`, `var`, `const`, `static`, or `fn`) so the test enforces what its name says.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift:114">
P2: `chunk` performs its file write synchronously on the main actor. Route the append through `work.run(.userBlocking, mayBlock: .files, ...)` like the unlink in `remove`, so the main thread does not block on disk I/O.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift:115">
P2: A failed or partial write leaves bytes beyond `received`, and the retry appends after them via `seekToEnd()`, which corrupts the staged file. Truncate to `offset` and write at that position, and close the handle on every path.</violation>

<violation number="3" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift:159">
P2: Late-delivery cleanup failures are reported as successful cancellations and cannot be retried because the delivery is removed before this undo completes. Make undo report failure and retain or otherwise surface the delivery when remote deletion fails.</violation>

<violation number="4" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift:166">
P2: The purge loop removes stages from a snapshot taken before its awaits, so a stage that begins transfer in the meantime still gets its file unlinked. Re-check idleness at each iteration before calling `remove`.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudAccessCLI.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudAccessCLI.swift:98">
P2: If cleanup fails during a refusal, `removeUploads` throws `CancellationError` from a task that was never cancelled. That replaces the CLI's `errorCode` with a generic failure and logs a misleading error. Keep the refusal answer and ignore the cleanup result here.</violation>
</file>

<file name="webviews/src/agent-session/acpmux/cloudUploads.ts">

<violation number="1" location="webviews/src/agent-session/acpmux/cloudUploads.ts:9">
P2: Retrying a staged image's original still passes only `gone`, so it never subscribes to this cancellation signal. Removing that chip during a long native finish cannot abort the native call and delays `chat.upload.cancel` until finish returns; pass a subscribed `UploadCancelled` callback in `retryOriginal`.</violation>

<violation number="2" location="webviews/src/agent-session/acpmux/cloudUploads.ts:179">
P2: `chat.upload.cancel` now goes through the 30s step deadline and is awaited. If the host is slow to answer, each cancelled or failed file blocks the batch loop for up to 30s before the next file starts. The earlier code deliberately sent cancel straight to the host, so this reverses that choice; either keep it unbounded and documented, or confirm the wait is intended.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudUploads.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudUploads.swift:161">
P1: Cancellation reports success even when remote cleanup fails. This `try?` swallows `removeUploads`'s error, so a remote file can remain after the page receives a successful cancel; propagate or retry the cleanup failure instead.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneUpload.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneUpload.swift:23">
P3: This adds a second `10 * 60` lifetime for the same batch deadline that `AgentPaneStagedUploads.lifetime` already defines. Reuse one constant so the stage and batch expiries cannot diverge.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneFilePut.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneFilePut.swift:9">
P2: Removing the size guard routes files over 10 MiB to `filePutChunks`. A peer that only advertises `filePut` now gets `.unsupported` instead of `.tooLarge`, so the composer says the chat takes no uploads. Map `unsupportedFeature(filePutChunksFeature)` to `.tooLarge` on the chunked path, or keep the one-shot size cap for peers without that feature.</violation>
</file>

<file name="cmux-tui/crates/cmux-tui/src/link/access_cli.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/access_cli.rs:326">
P3: This `upload-cancel` match arm is 118 columns, over the crate's rustfmt `max_width = 100`, so `cargo fmt --check` will flag it. Split the pattern the way the `answer` arm is split.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentPeerUploads.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentPeerUploads.swift:62">
P2: A peer upload that finishes before the page’s cancellation arrives cannot be undone: this call supplies no `undo` closure, so the shared store forgets the delivered file and leaves it on the peer. Add a peer-side remote deletion/undo path and pass it to `finish`.</violation>
</file>

<file name="cmux-tui/crates/cmux-tui/src/link/sync_cli/remote.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/sync_cli/remote.rs:234">
P1: The upload heartbeat is emitted only after `self.call` receives the `fs.write` response, so it cannot keep the 30-second idle deadline alive while a chunk is still in flight. Emit progress before or periodically during each potentially blocking write; the added in-flight progress test currently observes no tick and fails.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/ProcessRunner.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/ProcessRunner.swift:133">
P2: `waitForDeadline` can busy-spin indefinitely when an idle-only caller supplies a zero or negative `progressIdle`: the sleep returns immediately and `waited` never reaches a deadline while progress keeps changing. Treat non-positive idle durations as an immediate timeout or reject them before entering the loop.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

// A cancel after the file arrived (the chip's x as the answer comes back) removes it from the machine.
guard done.answer.ok, !done.answer.uploadIDs.isEmpty, let socket = done.socket else { return nil }
return {
try? await CloudAccessCLI(binary: done.binary).removeUploads(socket: socket, remoteRoot: done.remoteRoot, ids: done.answer.uploadIDs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Cancellation reports success even when remote cleanup fails. This try? swallows removeUploads's error, so a remote file can remain after the page receives a successful cancel; propagate or retry the cleanup failure instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudUploads.swift, line 161:

<comment>Cancellation reports success even when remote cleanup fails. This `try?` swallows `removeUploads`'s error, so a remote file can remain after the page receives a successful cancel; propagate or retry the cleanup failure instead.</comment>

<file context>
@@ -120,95 +104,77 @@ final class CloudUploads {
+                // A cancel after the file arrived (the chip's x as the answer comes back) removes it from the machine.
+                guard done.answer.ok, !done.answer.uploadIDs.isEmpty, let socket = done.socket else { return nil }
+                return {
+                    try? await CloudAccessCLI(binary: done.binary).removeUploads(socket: socket, remoteRoot: done.remoteRoot, ids: done.answer.uploadIDs)
+                }
+            })
</file context>

"fs.write",
json!({ "path": path, "bytes_base64": encode(&first), "mode": "overwrite", "permissions": permissions }),
)?;
progress();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The upload heartbeat is emitted only after self.call receives the fs.write response, so it cannot keep the 30-second idle deadline alive while a chunk is still in flight. Emit progress before or periodically during each potentially blocking write; the added in-flight progress test currently observes no tick and fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/sync_cli/remote.rs, line 234:

<comment>The upload heartbeat is emitted only after `self.call` receives the `fs.write` response, so it cannot keep the 30-second idle deadline alive while a chunk is still in flight. Emit progress before or periodically during each potentially blocking write; the added in-flight progress test currently observes no tick and fails.</comment>

<file context>
@@ -217,6 +231,7 @@ impl Remote {
                 "fs.write",
                 json!({ "path": path, "bytes_base64": encode(&first), "mode": "overwrite", "permissions": permissions }),
             )?;
+            progress();
             return revision_of(&entry);
         }
</file context>

}

@Test func missingBothDeadlinesRefusesBeforeLaunchingAChild() async {
await #expect(throws: DaemonError.self) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test passes even if the up-front deadline guard is removed, because a missing executable also throws a DaemonError. Assert the exact case so the test verifies the refusal happens before launch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextDaemonTests/ProcessRunnerProgressDeadlineTests.swift, line 79:

<comment>This test passes even if the up-front deadline guard is removed, because a missing executable also throws a `DaemonError`. Assert the exact case so the test verifies the refusal happens before launch.</comment>

<file context>
@@ -0,0 +1,84 @@
+    }
+
+    @Test func missingBothDeadlinesRefusesBeforeLaunchingAChild() async {
+        await #expect(throws: DaemonError.self) {
+            _ = try await ProcessRunner.run(executable: URL(fileURLWithPath: "/does-not-exist"), arguments: [],
+                                            environment: nil, timeout: nil, progressIdle: nil, clock: ContinuousClock())
</file context>
Suggested change
await #expect(throws: DaemonError.self) {
await #expect(throws: DaemonError.launchFailed("process requires an elapsed or idle deadline")) {

self.assertTrue(path.is_file(), relative)
if not path.is_file():
continue
names = {token[0] for token in guard.code_tokens(path.read_text(encoding="utf-8"))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This check only tests that each exempted name appears somewhere in the file, not that it is declared. maximumSize in AttachmentMedia.swift is satisfied by the AVFoundation property assignment generator.maximumSize = ..., so the exemption survives after the cap is removed. Match a declaration form (for example, the preceding token is let, var, const, static, or fn) so the test enforces what its name says.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_attachment_caps_guard.py, line 51:

<comment>This check only tests that each exempted name appears somewhere in the file, not that it is declared. `maximumSize` in AttachmentMedia.swift is satisfied by the AVFoundation property assignment `generator.maximumSize = ...`, so the exemption survives after the cap is removed. Match a declaration form (for example, the preceding token is `let`, `var`, `const`, `static`, or `fn`) so the test enforces what its name says.</comment>

<file context>
@@ -39,6 +39,20 @@ def test_retained_bounds_only_apply_to_the_named_path_and_symbol(self):
+                self.assertTrue(path.is_file(), relative)
+            if not path.is_file():
+                continue
+            names = {token[0] for token in guard.code_tokens(path.read_text(encoding="utf-8"))}
+            for name in symbols:
+                with self.subTest(path=relative, name=name):
</file context>

guard var stage = stages[token], stage.idle, owns(stage.staged.target), stage.received == offset,
data.count <= Self.chunkBytes, data.count <= stage.size - offset else { return nil }
do {
let handle = try FileHandle(forWritingTo: stage.staged.file)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: chunk performs its file write synchronously on the main actor. Route the append through work.run(.userBlocking, mayBlock: .files, ...) like the unlink in remove, so the main thread does not block on disk I/O.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneStagedUploads.swift, line 114:

<comment>`chunk` performs its file write synchronously on the main actor. Route the append through `work.run(.userBlocking, mayBlock: .files, ...)` like the unlink in `remove`, so the main thread does not block on disk I/O.</comment>

<file context>
@@ -0,0 +1,208 @@
+        guard var stage = stages[token], stage.idle, owns(stage.staged.target), stage.received == offset,
+              data.count <= Self.chunkBytes, data.count <= stage.size - offset else { return nil }
+        do {
+            let handle = try FileHandle(forWritingTo: stage.staged.file)
+            try handle.seekToEnd(); try handle.write(contentsOf: data); try handle.close()
+        } catch { return nil }
</file context>

if let stage = stages[token], stage.target.session == target.session {
stages.removeValue(forKey: token)
try? FileManager.default.removeItem(at: stage.file)
let result = await stages.finish(token, owns: owns, work: services.work) { staged -> Result<String, AgentPaneFilePutFailure> in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A peer upload that finishes before the page’s cancellation arrives cannot be undone: this call supplies no undo closure, so the shared store forgets the delivered file and leaves it on the peer. Add a peer-side remote deletion/undo path and pass it to finish.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentPeerUploads.swift, line 62:

<comment>A peer upload that finishes before the page’s cancellation arrives cannot be undone: this call supplies no `undo` closure, so the shared store forgets the delivered file and leaves it on the peer. Add a peer-side remote deletion/undo path and pass it to `finish`.</comment>

<file context>
@@ -35,67 +32,45 @@ struct AgentPeerUploads {
-                if let stage = stages[token], stage.target.session == target.session {
-                    stages.removeValue(forKey: token)
-                    try? FileManager.default.removeItem(at: stage.file)
+                let result = await stages.finish(token, owns: owns, work: services.work) { staged -> Result<String, AgentPaneFilePutFailure> in
+                    do {
+                        return .success(try await staged.target.host.putFile(sessionId: staged.target.session, name: staged.name, file: staged.file))
</file context>

}
var seen = progress()
var waited = Duration.zero
while timeout.map({ waited < $0 }) ?? true {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: waitForDeadline can busy-spin indefinitely when an idle-only caller supplies a zero or negative progressIdle: the sleep returns immediately and waited never reaches a deadline while progress keeps changing. Treat non-positive idle durations as an immediate timeout or reject them before entering the loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/ProcessRunner.swift, line 133:

<comment>`waitForDeadline` can busy-spin indefinitely when an idle-only caller supplies a zero or negative `progressIdle`: the sleep returns immediately and `waited` never reaches a deadline while progress keeps changing. Treat non-positive idle durations as an immediate timeout or reject them before entering the loop.</comment>

<file context>
@@ -123,8 +115,32 @@ public struct ProcessRunner {
+        }
+        var seen = progress()
+        var waited = Duration.zero
+        while timeout.map({ waited < $0 }) ?? true {
+            let window = timeout.map { min(progressIdle, $0 - waited) } ?? progressIdle
+            // wakeup-allow: one idle window, stopped on child exit/cancel; progress permits the next window.
</file context>
Suggested change
while timeout.map({ waited < $0 }) ?? true {
guard progressIdle > .zero else { return }
while timeout.map({ waited < $0 }) ?? true {


/// Cancellation checks between chunks and notifications while the native finish is in flight.
export type UploadCancelled = ((index: number) => boolean) & {
subscribe?(index: number, changed: () => void): () => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Retrying a staged image's original still passes only gone, so it never subscribes to this cancellation signal. Removing that chip during a long native finish cannot abort the native call and delays chat.upload.cancel until finish returns; pass a subscribed UploadCancelled callback in retryOriginal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/cloudUploads.ts, line 9:

<comment>Retrying a staged image's original still passes only `gone`, so it never subscribes to this cancellation signal. Removing that chip during a long native finish cannot abort the native call and delays `chat.upload.cancel` until finish returns; pass a subscribed `UploadCancelled` callback in `retryOriginal`.</comment>

<file context>
@@ -4,6 +4,11 @@
 
+/// Cancellation checks between chunks and notifications while the native finish is in flight.
+export type UploadCancelled = ((index: number) => boolean) & {
+  subscribe?(index: number, changed: () => void): () => void;
+};
+
</file context>

private var remaining: [AgentPaneUploadBatchEntry]
private var owners: Set<String> = []
private var expires: Date
package static let lifetime: TimeInterval = 10 * 60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This adds a second 10 * 60 lifetime for the same batch deadline that AgentPaneStagedUploads.lifetime already defines. Reuse one constant so the stage and batch expiries cannot diverge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneUpload.swift, line 23:

<comment>This adds a second `10 * 60` lifetime for the same batch deadline that `AgentPaneStagedUploads.lifetime` already defines. Reuse one constant so the stage and batch expiries cannot diverge.</comment>

<file context>
@@ -14,6 +14,40 @@ public nonisolated struct AgentPaneUploadBatchEntry: Sendable, Equatable {
+    private var remaining: [AgentPaneUploadBatchEntry]
+    private var owners: Set<String> = []
+    private var expires: Date
+    package static let lifetime: TimeInterval = 10 * 60
+
+    package init(_ manifest: [AgentPaneUploadBatchEntry], now: Date = Date()) {
</file context>

uploads::upload(socket, &root, positional)
uploads::upload_with_id(socket, &root, positional, args.upload_id.as_deref())
}
("upload-cancel", Args { socket: Some(socket), remote_root: Some(root), upload_id: Some(id), positional, .. })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This upload-cancel match arm is 118 columns, over the crate's rustfmt max_width = 100, so cargo fmt --check will flag it. Split the pattern the way the answer arm is split.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/access_cli.rs, line 326:

<comment>This `upload-cancel` match arm is 118 columns, over the crate's rustfmt `max_width = 100`, so `cargo fmt --check` will flag it. Split the pattern the way the `answer` arm is split.</comment>

<file context>
@@ -314,7 +321,16 @@ pub(super) fn run(args: &[String]) -> i32 {
-            uploads::upload(socket, &root, positional)
+            uploads::upload_with_id(socket, &root, positional, args.upload_id.as_deref())
+        }
+        ("upload-cancel", Args { socket: Some(socket), remote_root: Some(root), upload_id: Some(id), positional, .. })
+            if positional.is_empty() =>
+        {
</file context>

… the socket deadline

Codex BLOCK on #28695: the 30 s upload idle only heard a heartbeat after a
whole 8 MiB fs.write returned, so a healthy slow link was killed. The Rust
Remote now writes a request line in 256 KiB slices and reports progress after
each slice and on each answer; the app's upload idle is 90 s, longer than the
CLI's 60 s per-read/write socket deadline (IO_DEADLINE). No size cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
@teamleaderleo
teamleaderleo merged commit bc0f8f4 into feat-cmux-next Nov 1, 2026
136 of 142 checks passed
@teamleaderleo
teamleaderleo deleted the lane/carry-27073-attach-uncap branch November 1, 2026 02:35
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex-review-box/gpt-6-astra high, lane-place): SHIP at 5a7c47e

SHIP at 5a7c47e

Visual verdict: no images; Leo tries the build

  1. [P2][follow-up] [CloudUploads.swift:161] try? discards remote cleanup failures after cancellation, so a transient failure can leave the uploaded file on the Cloud machine with no retry while the UI reports cancellation.

Follow-ups:

  • [P2][follow-up] Preserve or retry failed Cloud upload undo operations.

This branch was previously deployed

3 inactive deployments
artifacts — 5a7c47ee Deployed Nov 1, 2026 by teamleaderleo via add the Windows daemon to the tree #92932
sentry-dsyms — 5a7c47ee Deployed Nov 1, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #92932
ci-routing-agent — 5a7c47ee Deployed Nov 1, 2026 by teamleaderleo via cmux-tui tree preflight #92932
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant