carry of #26791: cloud.startChat first send
Carry of #26791: cloud.startChat makes a new chat's first send run in Cloud.
Original purpose: add cloud.startChat {task, cwd?, harness?, model?, key}. With it, a new chat's first send picks or makes a Cloud machine, copies the folder there, starts the chat on the machine and closes the local tab. The Sandboxed guards and the AgentPaneHandOff path stay as they are.
This carry merges feat-cmux-next, with hand-resolved conflicts in the Cloud move, account handlers, machine model, work-pick, setup wizard and related tests. It refreshes the crash-class selector comparison. #26791 remains the source PR; this PR supersedes it for landing.
Leo, 2026-10-30: "why is it >10 hours and it's not been progressed on? how it blocked https://github.com/manaflow-ai/cmux/pull/28111 for no reason?"
Fixes for the first send (Leo, 2026-10-30: "can you guys just do it", "like the initial 0 to 1 chat auto it sends to cloud")
- Machine pick (hand-walk gap 4, https://github.com/manaflow-ai/cmux/pull/28111#issuecomment-6538116185).
cloud.startChatno longer takes the team's first connected machine, which on Leo's account is often someone's busy lane VM. It now takes a machine this person created (CloudMachine.createdByagainst the signed-in user) that is free: connected with no chat open, or paused by the idle timer. A machine named for chats (cloud-chats,cloud-chats-N) comes first. If none fits, it creates one namedcloud-chats. Lane-pool VMs (fs-lane-N,lane-vm-N) are never picked. Move to Cloud's own pick is unchanged. - Retry pinned to the machine (#28845). See "A retry goes to the same machine" below.
- First Claude call on a fresh VM (gap 5). acpmux resends a chat's first send once, 2 s later, if it failed with "OAuth token revoked" (
hub/auto_resume/first_send.rs). The resend goes through the Retry button's path (retry_turn.rsresend_turn) under the chat's continuation mark. Stop, close, a move, or a person's prompt during the wait cancels it. It applies only to the chat's first turn and the prompt's own id, never to a~resume-attempt. A resend that fails the same way shows the failure as before.daemon/cloud_handoff.rsholds that turn's failed report until the retry is decided. Expired or missing sign-ins and outages are unchanged. - Review BLOCK at 8b117beb, all three findings fixed (router-review, gpt-6-astra):
- [P1] The copy stays in scope. The copy uploads the Git working copy that holds the page's folder. The pane now checks that upload root against its roots too (
AgentPaneHandOff) and hands it on asAgentPaneCloudStart.uploadRoot. The App copies only that folder (CloudStartChat.uploads), so a grant of/repo/paintnever uploads/repo. - [P1] A retry never copies over the chat's edits.
Destinationsrecords, per send key and machine, that the send's copy finished. A retry whose copy finished opens its chat by its tab key without pushing again. - [P2] No second VM. A new machine for a send is created under
AgentPaneCloudStart.createKey, derived from the send key, so a retry after a lost create answer gets the same machine back.
- [P1] The copy stays in scope. The copy uploads the Git working copy that holds the page's folder. The pane now checks that upload root against its roots too (
- Not behind Labs (Leo, 2026-10-30, on cloud.startChat refusing with Labs off: "that seems dumb. i dont like putting so much stuff in labs"; and "WHAT GOOD REASON DO WE HAVE IN THE FIRST PLACE TO KEEP THIS STUFF IN FING LABS IF I FING ASKED FOR THESE F***ING FEATURES").
cloud.startChatno longer refuses withlabs_off. It refuses only when Cloud is off by policy (cloud_disabled) or nobody is signed in (not_signed_in), and the person still picks Cloud for the send. The chat's machine size pick (CloudChatPick.read) no longer checks Labs for any caller. The other shipped Labs gates (Move to Cloud's menus, the setup wizard, Sandboxed runs) come off in their own PR. - Review: router-review (gpt-6-astra) SHIP at 8c9971bb5cac (earlier SHIP at 4337dc56b4dc, before the Labs change). Follow-up from 8c9971b: #28979 (refuse an app-server link before creating a machine). Earlier follow-ups: #28930 (a lost success answer keeps the pane's hold), #28931 (pin a new machine's send before its create answers), #28932 (a daemon restart during the 2 s first-send wait).
- Not in this PR: fs-v1 on fresh VMs (#28823) and the Codex 503 hang (separate lane).
Fixes since the carry (taken over from its unreachable owner)
- The model reaches the machine (next-review P2). The remote seed carried only the prompt and the harness, so the model the page picked was dropped.
AgentPaneCloudStart.seed(cwd:)now builds the seed with the model too. The request has no effort field, so there is no effort to forward. - Xcode 26.6 types the page's Cloud runner.
AgentPageChats.seedwrites the closure out with its type (as #26921 found it must), in place of themapform. - A retry opens the same tab (Codex review, P1). The machine's chat tab is created with the send's own key (
AgentPaneCloudStart.tabKey). A retry after a lost answer gets that tab back, so the prompt is never sent to a second chat. - A retry goes to the same machine (Codex review, P2; #28845).
CloudStartChat.Destinations(CloudStartChat+Machine.swift) pins each send key to the machine its first try took, keeping the newest 64 in the app's defaults. A retry names that machine, so its tab key gets the first try's tab back, never a second chat with the prompt on another machine. A pin to a machine that no longer exists is dropped. - The folder check runs off the main thread (
@concurrent,FileManager.checked). - A second Cloud chat from the same folder keeps the first one's edits (Codex review, P1). Each send now copies the folder into its own place under the machine's work root (
chats/<send key>,AgentPaneCloudStart.remotePlace), and a retry of the same send reuses that copy, so a second chat never writes over the first one's changes.
No capture: the method has no UI caller until the composer's Cloud row (#28686).
Risk
risk:high (a new host method that starts a remote machine and copies a folder to it). Not behind Labs. A signed-out or Cloud-disabled account refuses with its own code. The method has no UI caller until the composer's Cloud row (#28686), and only the person's Send gesture reaches it. A refusal releases the hold so the person can send again; a started chat keeps it.
The guardrails' app-size rule was skipped for the push, with this reason: CloudStartChat composes App-only services (AppServices, CloudMove, CloudHandlers, AgentTabStore), beside its siblings CloudMove and CloudStartSession. CmuxNextCloud can't see those services until the B4 host protocols land, and CloudMove.swift is at 366 of 400 lines.
Tests
AgentPaneCloudStartTests: the hand-off guards; the remote seed carries the prompt, agent and model; a retry's tab key matches its first try's, and another send's doesn't.AgentPaneCloudStartTests:aFolderInsideAGitWorkingCopyIsCopiedOnlyWhenTheWholeCopyIsInScope,aRetryOfTheSameSendCreatesUnderTheSameKey.CloudStartChatMachineTests(new):aFinishedCopyIsKeptPerSendAndMachineAcrossARelaunch,aPageNamedFolderIsCopiedOnlyAsItsCheckedUploadRoot,aNewMachineIsCreatedUnderTheSendsKey; and the pick (a teammate's machine is never taken, a busy machine is passed over for a free one, a machine named for chats comes first, lane-pool VMs are never picked, idle-paused is free and a person's pause is not, a machine with no creator counts only when an earlier send used it, new-machine naming);aRetryOfTheSameSendStaysOnItsFirstTrysMachine(#28845); a gone machine's pin is dropped; Destinations survive a relaunch and keep the newest 64.- acpmux:
claude_auth::tests::a_revoked_token_is_told_from_other_sign_in_failures;hub::auto_resume::first_send::tests(a first send a revoked token failed goes again once and completes; a retried one that fails again is not retried; a later turn is not retried; a Stop or a prompt during the wait cancels the retry);cloud_handoff_tests::a_revoked_token_holds_the_turn_for_its_first_send_retry.
Observed / Not run
- At caac4e4 the base (feat-cmux-next fe08891) is brought in again: the only conflict, the page's op list in AgentPageProvider.swift, keeps the base's
acpmux.restartStuckand this PR'scloud.startChat. - Observed: the cmux-next prepush gate passes, apart from the app-size skip named above. The base merge's only conflict, the page's method list, keeps both sides.
- Fleet runs at this head: app compile-check (cmux-ci) and the acpmux suites (153 lib tests, 59 hub_integration auto_resume/retry tests, clippy -D warnings clean; cmux-ci jobs 1db5ce0413f9b6d113726b0f, ba5f1a79ad4768e0ceaad195).
cmux-next checks: on the carry's head, its two failing steps were in files this PR doesn't touch. "No literal state roots" was acpmuxhelp_examples/session.rs:179, and it passes on feat-cmux-next now. "String tables (21 languages)" is missing translations in the ActionCatalog, PaneCreation, Updater and CmuxHome tables.
Agent session: agent session
🤖 Generated with Claude Code