cloud.startChat: machine pick, per-send ledger, keys and the copy move to cmux-tui (Rust)
HOLD: stacked on #28111; do not merge into its branch. Retarget to feat-cmux-next and merge base after #28111 lands, then rerun CI (the Swift compile has not run at this base).
Leo, 2026-10-30: "AS MUCH RUST AS POSSIBLE INSTEAD OF SWIFT" and "SPEED UP THESE FUCKING ITERATIONS". Rule: leo-workspace machines/claude-rulebook.md, "New app logic goes in Rust".
Stacked on #28111 (branched from its head 8c9971bb5cac; base is its branch, so this diff is only the move). Once #28111 lands, this retargets to feat-cmux-next. Don't land before #28111.
What moves
A new cmux link start-chat CLI in cmux-tui (cmux-tui/crates/cmux-tui/src/link/start_chat_cli*). Swift reaches it the same way it reaches link sync push: through CloudMoveCLI, one JSON line on stdout.
plan --request JSON: picks the machine for a first send. That is this person's free machine or one namedcloud-chats, never a teammate's machine or a lane-pool VM; otherwise a new machine namedcloud-chats[-N]. A retry stays on the machine its first try pinned. Machines are pinned before anything runs on them. The answer carries the send's create, tab and place keys.pin --key --machine: pins a machine the App just created.copy --key --machine --socket --local --subpath --work-root...: pushes the folder to the send's ownchats/<key>/<name-hash>place. It falls through work roots onfs.permission_denied. If this send's copy already finished on that machine, it never pushes again.- The per-send ledger moved from UserDefaults to
<state>/cloud-start-chat/destinations.json(0600). Reads and writes happen under a file lock, so two sends at once don't drop each other's pins.
Swift keeps four jobs: it lists its machines for the plan, creates the machine the plan asks for, connects and opens the seeded chat with the plan's tab key, and closes the local tab. The place: parameter #28111 added to CloudMove.sync is gone, and CloudMove.failure(syncing:) is shared between Move to Cloud and Start Chat.
Line counts (vs #28111's head)
| added | removed | net | |
|---|---|---|---|
| Swift sources | 122 | 235 | -113 |
| Swift tests | 18 | 183 | -165 (ported to Rust) |
| Rust (cmux-tui) | 1,038 | 1 | +1,037 (652 logic, 386 tests) |
The Rust logic is longer than the Swift it replaces for three reasons: the CLI layer, the arg parsing, and the locked file ledger.
Not moved, on purpose
The pane's folder scope check (AgentPaneHandOff.folderInScope) stays in Swift for now. It is built on AcpmuxPathPolicy, which uses macOS getattrlist canonical spelling and is shared with every pane path check. Moving it means moving that whole policy, which is its own PR.
Tests
- Rust, on the Linux pool (
cmux-ci test --setup rust):cargo test -p cmux-tui --lib link::start_chat_clipassed 17/17, plus fmt and clippy clean on the new files. They cover every case from the removedCloudStartChatMachineTestsand the key tests inAgentPaneCloudStartTests, plus new ones: the work-root fallthrough, no second copy on retry, the ledger's 0600 mode and 64-entry cap across a relaunch, and refusal of escaping paths. - Swift: the fleet compile-check gave no verdict (controller TimeoutError), so this PR's CI compile is the Swift check.
Risk
A behavior-preserving move of #28111's logic. The one behavior change is that pins live in the state folder, not UserDefaults. #28111 hasn't shipped, so there is no data to migrate.
Other Cloud Swift logic to move (ranked)
- The Move/Bring Back retry state machine and ledgers (
CloudMoveRetries,CloudMovedChats, control flow inCloudMove/CloudBringBack, ~450 lines) intolink/agent_cli+sync_cli. - Machine update outcome mapping and the legacy path (
CloudMachineUpdateTarget/Updates/LegacyMachineUpdate, ~350 lines) intolink/machine_update. - The work pick and repo-key parsing (
CloudWorkPick, ~160 lines) intocmux-host/cloud. - GitHub setup command building and parsing for the setup wizard (
CloudGitHubSetup, ~220 lines) intocmux-host/cloud/github. - The machine secrets plan and ledger (
CloudMachineSecrets, ~300 lines) intolink/access_cli; the Keychain read stays in Swift. - Access asks polling and upload staging (
CloudAccessAsks,CloudUploads) via a streaminglink access watch. - The Codex broker refresh scheduler, into acpmux.
CloudServiceitself is mostly wiring and observable state, so it stays thin Swift.
š¤ Generated with Claude Code
Agent session: agent session
![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
Moves cloud.startChat's machine pick, per-send pin ledger, send keys, and folder copy out of Swift into a new cmux link start-chat CLI in cmux-tui. Swift now only lists machines, creates the machine the plan asks for, opens the seeded chat, and closes the local tab.
- The ledger moved from UserDefaults to
<state>/cloud-start-chat/destinations.json(0600), read and written under a file lock so concurrent sends don't drop each other's pins. - A retry of a send stays on its first try's machine, and a send whose copy already finished never copies again.
- Removed
CloudMove.sync'splace:parameter;CloudMove.failure(syncing:)is shared between Move to Cloud and Start Chat. - Swift pick and key tests are ported to Rust (
link/start_chat_cli/tests.rs), which also covers the work-root fallthrough, the ledger's 0600 mode and 64-entry cap, and escaping-path refusal.
This is a behavior-preserving move except for the ledger's location, and since #28111 hasn't shipped there is no data to migrate. The pane's folder-scope check stays in Swift for now because it depends on AcpmuxPathPolicy.