carry of #29046: cloud.startChat's pick, pin, keys and copy in cmux-tui, with #28111's account rules
Takes back #28407: its start-chat change is kept, not undone. The Swift pick it changed now lives in cmux-tui's pick.rs, which skips a machine with a pause, resume or delete on its way (pausing, starting, deleting), and the App sends the shown status (shownStatus). Its Swift test aChangingMachineIsSkippedEvenWhenMarkedForChats is now the Rust test a_changing_machine_is_skipped_even_when_marked_for_chats.
Takes back #28111: its account rules are kept, not undone. They move from Swift into cmux-tui with #29046's pick and ledger: account-scoped pins, the creatorless-machine rule, the listed-machines filter, team on create, the per-send copy folder, and the git-folder and home-folder checks. checkAccount, the refresh and the account rechecks stay in CloudStartChat+Machine.swift. CloudMove.sync loses only its place parameter, since the copy folder is now cmux-tui's.
Carry of #29046 onto feat-cmux-next. #29046's lane has not committed since 06:45 UTC (over 11 hours), and the Cloud area owner handed its work to this lane. #29046 stays open for its owner to close.
Leo, 2026-10-30: "AS MUCH RUST AS POSSIBLE INSTEAD OF SWIFT" and "SPEED UP THESE F***ING ITERATIONS". Rule: leo-workspace machines/claude-rulebook.md, "New app logic goes in Rust".
What it does
cloud.startChat (a new chat's first send that runs in Cloud) keeps its behaviour. The bookkeeping moves from Swift to cmux-tui (cmux link start-chat plan|pin|copy, cmux-tui/crates/cmux-tui/src/link/start_chat_cli/), as #29046 built it:
- the machine pick (this person's free machine or one named for chats, never a teammate's or a lane-pool VM, else a new one),
- the per-send pin, so a retry stays on its first try's machine (#28845),
- the send's create, tab and copy keys,
- the copy, which is never repeated once it finished.
#29046 branched from #28111 at 8c9971bb5cac, and #28111 landed with 15 more commits. Their rules lived in the Swift this PR deletes, so this carry moves them into the Rust and keeps the rest in the App (the checklist in https://github.com/manaflow-ai/cmux/pull/29046#issuecomment-6541220135):
| #28111 rule | Here |
|---|---|
| A send's pin and copy record are kept under its account (user and team) | Rust: each ledger entry carries account; plan, pin and copy take it |
| A machine with no creator is the person's only through this account's sends | Rust: Ledger::machines_of(account) |
| Take machines only from the list the server answered for this account; keep a pin to a machine made since | App sends only listed machines, plus stored ids that keep a pin but are never picked |
| Create in the send's team; recheck the account after the create and around the copy | App, unchanged |
| A retry whose record was evicted copies into a new folder | Rust: every copy gets a new folder (below) |
| Folder and git-folder scope checks, the app-server link refusal | App, unchanged |
The two replay hazards from the SW-00 review (https://github.com/manaflow-ai/cmux/pull/29046#issuecomment-6541602038):
- Unreadable ledger. A ledger that can't be read no longer reads as empty, which made a retry pick and copy afresh. A read error refuses the send with
ledger_failedand leaves the file alone. A file that isn't a ledger (cut short, damaged, another version's) also refuses, and is moved aside with its bytes as they were (destinations.unreadable-<time>.json), so later sends can start. - Copy whose answer was lost. Each copy now goes to a new folder of the send's own (
chats/<key>/<8 hex>), recorded before the push starts, and the finished copy is recorded with that folder. Three cases now copy into a new folder and never write over an earlier one: a retry whose push finished on the machine but lost its answer, a retry after a relaunch, and two copies of one send at once.
Not done here: the SW-05 picker finding (a machine with a terminal status but a stale connection can still be picked). It is the same behaviour as base's Swift pick, so it is not a regression; it stays a follow-up with hq#5685.
Prior art: #29046 (carried here). #29109 changes CloudStartChat.swift and AgentPaneCloudStart.swift, and #28675 changes sync_cli.rs (this PR only makes push pub(super)). Both may need a base merge after this lands. #29532 (one machine per Mac workspace) is stacked on this work and merges it next.
Upgrade note: the sends the App kept in its user defaults (cmux.cloud.startChatDestinations) carry over once: before its first plan the App hands them to cmux link start-chat import --entries JSON and then forgets them (router-review BLOCK at 83b8b35). A send in flight during the update keeps its machine, its copy's folder and its finished copy. cmux-tui keeps a send it already has, and leaves out one kept under no account, since it can't say whose it is. A failed hand-over refuses the send and keeps the old entries.
Tests
Rust (start_chat_cli/tests.rs): #29046's tests now run under an account. New tests:
a_send_is_kept_under_the_account_it_began_ina_pin_to_a_machine_made_since_the_list_was_read_is_keptplan_and_pin_need_the_account_the_send_began_inan_unreadable_ledger_refuses_the_send_and_keeps_its_bytes(SW-00 #1)a_copy_whose_answer_was_lost_is_never_written_over(SW-00 #2)two_copies_of_one_send_at_once_never_share_a_foldera_changing_machine_is_skipped_even_when_marked_for_chats(ported from #28407)the_sends_the_app_kept_before_carry_over_once(router-review at 83b8b35)
Swift (CloudStartChatMachineTests):
- kept: the upload scope, refusal and team create tests
- new:
aSendIsKeptUnderTheAccountItBeganInAndChecksItAtEveryStep,thePlanRequestCarriesTheSendItsAccountAndTheListedMachines - moved to Rust: the pick, ledger and key tests
Performance
No perf numbers: this moves the start-chat bookkeeping into Rust and changes no hot path; "SPEED" is Leo's iteration-speed reason for writing it in Rust, not a runtime speed change.
Observed / Not run
Results are posted as comments once they run.
- Observed: the pre-push gate passed (no new violations; CmuxNextApp is 142 lines shorter).
- Not run: an end-to-end send on a real Cloud machine.
No capture: no UI changes.
Changelog
- Fixed: a Cloud chat's first send never writes its copy over an earlier copy whose answer was lost, and refuses instead of starting over when its record on this Mac can't be read.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Carries #29046's cloud.startChat bookkeeping from Swift into Rust (cmux link start-chat plan|pin|copy|import, new link/start_chat_cli/): the machine pick, the per-send pin, the send keys, and the one-shot copy now live in cmux-tui. It also folds in the #28111 account rules that landed after #29046 branched, instead of dropping them. The only new commits since the last summary are base merges; no behavior changed.
Behavior
- Every send is kept under the account (user and team) it began in: a retry never picks another team's machine, and a creatorless machine counts as this person's only through that account's sends.
- The App sends only machines from the server's list for that account; a pin to a machine made since the list was read is stored but never picked.
- Each copy goes to a new folder of the send's own, recorded before the push: a retry whose answer was lost, a retry after a relaunch, or a concurrent second copy never write over an earlier copy.
- A ledger that can't be read refuses the send with
ledger_failedand moves the file aside with its bytes intact, instead of reading as empty. - The App hands its existing user-defaults pins to
cmux link start-chat importbefore the first plan; cmux-tui keeps a send it already has and drops sends kept under no account. - The stale-connection picker finding from the SW-05 review matches base's Swift pick, so it stays a follow-up.
Migration
- Existing pins in user defaults (
cmux.cloud.startChatDestinations) carry over to cmux-tui once viaimport, so a send in flight during the update keeps its pin.