Repository navigation
cmux link sync: push through a per-user content-addressed blob store - #34604
Conversation
The Mac hashes each file (FastCDC chunks above 4 MiB), uploads only the blobs the user's sync store lacks, and the machine daemon fetches the plan and blobs itself with a read-only grant (new blob-fetch-v1 capability, blob.fetch.start/status), verifying every chunk and file digest and writing through the fs roots and PendingWrite staging. The direct link copy stays the fallback: no capability, no --store, a store failure, a refused file, or under 1 MiB to write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
|
|
Found 1 test failure on Blacksmith runners: Failure
|
teamleaderleo
left a comment
There was a problem hiding this comment.
Review (codex gpt-6.1-sol xhigh, next-review, since=20261104T022515Z): BLOCK at 7669a36
BLOCK at 7669a36
Visual verdict: no images; Leo tries the build
-
[P1] [blocking] blob_fetch.rs:329 — Completed-job reuse breaks subsequent pushes. For one file above 1 MiB, push content A, then B, then revert and push A. The third push reuses A’s completed job without writing anything.
finish_deferredthen records B’s revision alongside A’s hash, so the push reports success and future pushes skip the incorrect content. Scope deduplication to a particular push’s retries or revalidate completed results. -
[P1] [blocking] blob_fetch.rs:496 — An abandoned fetch can overwrite a completed retry. Delay job A’s blob response, terminate its CLI, change the source to B, and complete another push before releasing A. The detached worker survives and its unconditional overwrite commits A over B, potentially after the chat starts editing. Prevent overlapping jobs from committing stale content, including when falling back to direct writes.
-
[P1] [blocking] blob_fetch.rs:554 — The response body bypasses the size limits when
Content-Lengthis absent. An accepted HTTPS store can return an arbitrarily large chunked response;response.bytes()buffers everything before checking size or digest, allowing daemon memory exhaustion. Enforce the requested size while streaming and reject excess bytes immediately.
Rust tests could not run because Rust and Cargo are unavailable.
Follow-ups:
- [P2] [follow-up] CloudStartChat.swift:151 — No after images were supplied; verify the changed Cloud copy flow in the build.
A repeated plan reuses only a running job. A finished one runs again, since a file it wrote may have changed on the machine since, and the push means the plan's bytes (review at 7669a36). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
|
Review (subagent claude-opus-5-5 medium): SHIP at ba62e65 The first review, at 7669a36, blocked on one issue: Fleet tests at this tree pass: blob_fetch, link_sync_store, sync_cli and link_sync, with no clippy findings in the files this PR changes. The app compile check passed at 7669a36; the Swift files have not changed since. Follow-ups: filed as issues and linked below. |
|
Review (codex, router-review): SHIP at ba62e65 Reviewer: codex gpt-6-astra high, fast tier. Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward. SHIP at ba62e65 Visual verdict: no images; Leo tries the build
Follow-ups:
|
…lob-store # Conflicts: # cmux-tui/crates/cmux-tui/src/link/sync_cli.rs
…round git in the store test After the merge of feat-cmux-next: a store-refused file written over the link skips a machine symlink at its path as the link path does (#34362), and the store test's repo turns off auto gc and maintenance, which raced the push's read of .git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
…d plan The panic ratchet refuses new indexing_slicing hits in cmux-tui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Review (subagent opus medium, correctness): SHIP at 2c52d77 Covers the feat-cmux-next merge resolution (store path routed through the parallel write pipeline of #35149/#35138) and the panic-ratchet indexing fix, on top of Codex gpt-6-astra's SHIP at 6df0a6e. Fleet at this head: link_sync_store 6, sync_cli 33, clippy clean; CI clippy (changed cmux-tui packages) passes. Remaining reds are nightly-guards (release-notary, Guard status), base reds. Follow-ups: direct_files counts a symlink-skipped file; under-1 MiB store-path writes go one at a time instead of in parallel; progress bytes can step back after a fetch. |
|
Review (codex gpt-6-astra high, router-review): BLOCK at 2c52d77 BLOCK at 2c52d77 Visual verdict: no images; Leo tries the build
Follow-ups: none |
…h redirects a write Review fix (Codex at 2c52d77): the fetch checked only the final path once, then PendingWrite::begin followed a symlinked parent folder or one put there before the commit. begin_no_follow refuses either (fs.symlink_in_path), as the link copy's no-follow writes do; the Mac sends a refused file over the link, which skips it the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
|
Review (codex, router-review): SHIP at 6f50c80 Reviewer: codex gpt-6-astra high, fast tier. Follow-ups: 2. They land as checklist items in this PR's one follow-up tracking issue in this repo ( SHIP at 6f50c80 Visual verdict: no images; Leo tries the build
Follow-ups:
|
Keeps the panic ratchet at its baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-fb75b7af9902231028aa
|
Review (codex, router-review): SHIP at b043228 Reviewer: codex gpt-6-astra high, fast tier. Follow-ups: 2. They land as checklist items in this PR's one follow-up tracking issue in this repo ( SHIP at b043228 Visual verdict: no images; Leo tries the build
Follow-ups:
|
|
Hi Leo. This PR (merged directly) made two tip reds on
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo (2026-11-03), on the sync speed-ups: "yeah let's do alllll of those things! :DDD". This is the content-addressed blob store from that list.
What it does
cmux link sync push --store PLATFORM|default(andstart-chat copy --store, which the app passes for a personal account on non-production auth) sends a folder through the user's sync store on the cmux platform instead of byte by byte over the link:POST /v1/sync/blobs/missing), uploads only those (PUT /v1/sync/blobs/{sha}, 16 at once, each piece re-read and re-hashed before it goes), then uploads the plan (itself a blob: every file's path, mode, size, sha256 and chunks).POST /v1/sync/grants,csg_…, one hour) and sends the machineblob.fetch.start {store:{origin, grant}, plan}. The machine never sees the account credential.blob-fetch-v2, opsblob.fetch.start/.status/.cancel/.forget) fetches the plan and blobs from the store, 16 files at once, verifies each chunk's and each file's sha256, and writes through the sameRootssandbox andPendingWritestaging asfs.write(atomic rename, permissions set before commit). A path outside the roots is refused and named.The direct link copy stays the fallback and is unchanged:
blob-fetch-v2, no--store, or a store that fails before the machine writes: everything goes over the link.store.reason: "small").Blobs are keyed per user (
v1/u/<user>/<sha[0..2]>/<sha>), so identical files are stored once per user, a second machine costs the Mac only the plan, and a push cut off partway is resumed by the next push re-planning: the store already has the blobs, so the Mac uploads nothing again.Server side: manaflow-ai/cmux-platform#746 (merged; the routes are live in development only, so staging and production answer 404 and the push falls back to the link).
What I took from Mutagen
transition). Here:PendingWriteplus a per-chunk and whole-file sha256 check.stagefrom the receiving end).Where it differs: Mutagen has two ends; this puts a store in the middle, so a third end (another machine, a resumed machine) costs the Mac nothing. Rsync deltas inside a changed chunk are a possible follow-up.
Performance
Same harness for both columns:
cargo test -p cmux-tui --test link_sync_store -- --nocapture(a fake platform and a fake machine daemon running the realFsServiceandBlobFetchService, loopback). Repo: 600 files of 2-24 KiB, a vendored 200 KiB file twice, a 24 MiB asset, committed. "Bytes from the Mac" is bytes over the link plus bytes uploaded to the store. Run on the Linux pool (fast checks).Wall time on loopback, where bandwidth is free, is about the same: first sync 1.08 s before and 1.17 s after, second machine 1.07 s and 1.09 s. On a real link, the bytes are what it pays for. The link bytes in each store push are the folder listings and the manifest. Before the 1 MiB floor, a one-file edit took 401 ms through the store and 103 ms over the link, so small writes stay on the link. Numbers are measured on this branch merged with feat-cmux-next, which sends git loose objects as one pack (#34537).
Tests
blob_fetchunit tests: a chunked plan written with its permissions, each file's revision from its commit, and a rerun after a machine-side edit; a blob that does not match its hash fails and writes nothing; a path outside the roots is named and not written; a plan file outside the job's folder fails the plan; bad origins, grants and targets refused before any fetch; starts past the running cap refused; a blob body past its size cut off; a symlink at the path refused; a job held mid-fetch writes nothing after a newer start or a cancel under its folder; a cancel names every job kept there until it is forgotten; only the four ops are frames.sync_cli::storeunit tests: chunks cover the file within bounds; an edit in the middle changes at most two pieces; the same bytes always cut the same way.link_sync_store: the end-to-end test above, plus a store that refuses the credential still lands every file over the link, and a lostblob.fetch.startanswer (with the link) fails the push as unsettled, then the next push fences the job and lands the whole tree. A machine that advertises the pre-releaseblob-fetch-v1(no cancel) gets a plain link copy, with or without--store. A push whose fetch a newer push cancelled stops without a link copy. A file only a failed push's fetch wrote is deleted by the next push whose tree drops it.blob.fetch.*one.Review findings fixed in this PR: #34788 (each written file's revision comes from its commit), #34789 (no direct link fallback while the machine's fetch may still be running), #34790 (blob bodies are cut off past their size). Also fixed from review: at most 4 live fetch workers (
blob.busy), a cancelled one counting until its thread ends; a symlink at a target path is refused; and a fence: every job names its folder (target), a start and the newblob.fetch.cancelcancel each running job under an overlapping folder and answer only once none of them can commit (a commit and its cancelled check share one lock), and every push to a machine serving blob-fetch-v2 fences first. The capability isblob-fetch-v2: pre-release builds of this branch advertisedblob-fetch-v1without cancel, and a Mac treats those machines as serving no store. So a retry with a changed plan, a push under the 1 MiB floor, or a link fallback after a lost start answer is never overwritten by an older job. A start answer that is lost stops the job, or fails the push when the stop cannot be confirmed. A push whose fetch was cancelled by a newer push stops (blob.superseded) instead of copying over the link. What a cancelled or failed fetch committed is in no manifest, so the cancel answers every job kept under the folder, the next push reads what each wrote and counts those files as its own at that revision (deleting any its tree dropped), and it forgets the jobs once its manifest is saved (blob.fetch.forget). Still open as follow-ups: #34791, #34861.Risk
Medium. New daemon ops behind a capability and the existing fs gate; a new Mac-side path behind
--store. Every failure falls back to the existing link copy. Only NEXT uses this code.Why this is one PR (about 1,900 lines with tests): the machine op and the Mac client are two ends of one wire format (the plan), and the end-to-end test that measures it needs both.
Follow-ups: pack small files into one blob; a machine-side chunk cache; per-user quota and GC (cmux-platform#751); presigned direct-to-R2 uploads.
Changelog
Added: Folder sync to a Cloud machine can go through your sync store: only content the store lacks is uploaded, and a second machine or a resumed sync moves almost nothing from your Mac.
Agent session: agent session
🤖 Generated with Claude Code