cloud.startChat: one workspace per small Cloud machine
Leo, 2026-10-31: "and we ofc wanted to have the thing be having snapshots of whatever vms to do the right-sizing... and also austin/lawrence said indeed we wanted 1 workspace per vm for now / i guess they're relatively smol vms / but anyways we ideally also can just hide the machines too and it's just a cloud button"
This PR is the "1 workspace per vm for now" part, in cloud.startChat's pick (Rust, cmux-tui). Spec: leo-workspace design/cloud.md, "One workspace per small machine, snapshots, and a Cloud button".
What changed
pick::destination: after the pinned-send check (a retry stays on its first try's machine) and the workspace's own machine check, a workspace with no machine, or a send from no workspace, getsDestination::New. An existing machine is never reused for a new workspace.pick::claimbinds a machine to a workspace only when that workspace has none yet and no other workspace of the account has it;planandpingo through it, so a machine bound to workspace A is never bound to workspace B (a retry of A's send from B still runs on A's machine but does not give it to B).pick()and its ranking (free, marked-for-chats, lane-pool, idle-paused) became dead and are removed, with theCandidatefields only it read (slug,pause_reason,chats; serde ignores them from the App).- The ledger still holds many workspaces per machine (
Ledger::held_by_anotheradded); the one-per-machine limit is the pick's policy, so sharing can come back there alone. Ledgers written under #29532 that already share a machine across workspaces keep those bindings (their chats are on it). - New machines keep the
cloud-chats-Nname (planhas only the workspace id, not its name).
Supersedes the reuse/share behaviour of #29532 and #29582 (merged; Leo's earlier "go arbitrarily many"). Austin's open #26078 does per-workspace VMs for Cmd-N in Swift; this is the chat path in Rust and touches none of his files. Not touched: the create request's team/owner targeting, the composer UI, Swift.
Tests
Updated the tests that asserted reuse; added each_workspace_gets_a_new_machine_and_keeps_it (two workspaces get two New, each keeps its machine across sends, paused or before the list loads), a_free_machine_of_mine_is_not_reused_for_a_new_workspace, and a_machine_bound_to_one_workspace_is_not_taken_by_another.
cmux-ci fleet job 8880d5bb9bad24b949423853 (linux-pool), at 57a00e09: cargo test -p cmux-tui --lib start_chat_cli passed 25 of 25, and cargo clippy -p cmux-tui --all-targets -- -D warnings -A clippy::redundant_closure passed. That one lint is allowed because the base fails it at crates/acpmux/src/hub/processes.rs:256, a file this PR does not touch.
Router review at 6b62dda flagged a P1: two first sends from one workspace can race (both plan New, then both pin). Fixed in 57a00e09. pick::claim binds only while the workspace has no machine, so the first pin wins and the later machine keeps only its own send's chat. A test covers this. The orphaned second machine stays the person's, and pausing or deleting it is a follow-up.
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
cloud.startChat now gives each Mac workspace its own small Cloud machine instead of reusing or sharing existing ones.
- A workspace's first send plans a new
cloud-chatsmachine and keeps it across later sends, even paused or before the list loads; sends from no workspace get a new machine too. - Existing machines are never reused, and
pick::claimnever binds a machine to a second workspace: a retry of workspace A's send runs on A's machine but does not give it to B. - A workspace still drops its own machine when it is a fleet VM or failed (#29959's check is kept), so the next send makes a new one.
- The reuse-by-ranking logic in
pick()is removed as dead; the ledger still allows sharing, so the one-per-machine rule can move back there alone. - Ledgers that already share a machine across workspaces keep those bindings.
- Two first sends of one workspace that race to pin: the first pin wins, and the later machine keeps only its own send's chat.