cloud.startChat: a new machine whose older cmux-tui refuses the copy is updated and copied again (re-land in CmuxNextCloud)

Merged
TE
4d ago
+95-30

Re-lands #30258 (reverted by #30438: "CmuxNextApp +21 lines turned the feat tip arch ratchet red; re-land with the code moved to a package").

Leo's ask this serves: "whenever i start a chat it should be able to send me to cloud automatically keep launching/downloading new dev builds and such"

What it does

A new Cloud machine's image may carry an older cmux-tui than the app, and the automatic update its first handshake queued may not have run yet. cloud.startChat's folder copy (cmux link start-chat copy) refused with machine_outdated about the machine (not side: "source") now updates the machine to the app's build (CloudMachineUpdates.updateNow, --to-bundled --if-older) and copies once more, instead of failing the first send with "Update the machine". Any other answer, or no update turn (toggles off, no client, machine gone), keeps the first answer.

Where the code lives (net CmuxNextApp lines: -16)

  • CmuxNextCloud/Move/CloudOutdatedMachine.swift (new): code (machine_outdated), the predicate updatesBeforeCopyingAgain(ok:errorCode:side:), and the generic copy, update, copy-again sequence over closures.
  • CmuxNextCloud/Move/CloudWait.swift (new): CloudMove.holds(within:until:) moved verbatim as CloudWait.holds, to offset the call site (a move with no behavior change; its 6 callers renamed).
  • CloudStartChat.swift: the copy goes through CloudOutdatedMachine.copy. CloudMoveStrings.outdatedCode now reads CloudOutdatedMachine.code.
  • Test: CloudStartChatMachineTests.onlyTheMachinesOutdatedRefusalUpdatesItBeforeCopyingAgain covers every answer kind and whether the update ran or not.

Arch ratchet (scripts/cmux-next/arch-ratchet.py --audit): app_lines 159583 at base 00085a36e16, 159567 at head. arch-ratchet.py --base ... --head HEAD and guardrails.py say "no architecture count rises"; check-concurrency.sh passes.

Review follow-up from #30258: wait for the daemon to reconnect

#30258 read the socket again from the app's own connection after the update, which may be nil or stale right then. This version copies again on the same socket and does not need the app's own connection to be back first:

  • cmux link machine update answers updated only after the new daemon answered over that socket: wait_for_outcome reads the outcome and identify through it (cmux-tui link/machine_update.rs:334-366), so when updateNow returns (it awaits the policy turn, CloudMachineUpdates.swift:94-98), the socket reaches the new daemon. The socket is the local cmux-tui remote connect link's, which redials, and each CLI dials its own connection over it (CloudMachineLink.swift:4-13).
  • Update Machine already reuses the pre-update socket the same way (CloudMove.swift:151-156).
  • updateNow does not wait for the app's own reconnect: the target calls source.reconnect (CloudMachineUpdateTarget.swift:111), and reconnectAfterUpdate (CloudMachineSession.swift:451) starts the connect without awaiting it. The open after the copy waits for that connection's tree for up to 10 s (CloudHandlers.existingWorkspace, CloudHandlers.swift:246-252).

Risk: low. The change is limited to the start-chat copy's machine_outdated path, and the moved wait helper is unchanged.

Agent session: agent session

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Re-lands cloud.startChat's handling of a new Cloud machine whose older cmux-tui refuses the folder copy with machine_outdated: the machine is now updated to the app's own build and the copy runs again, instead of failing the first send with "Update the machine". Any other answer, or an update that didn't run, keeps the first answer.

The logic moved into the CmuxNextCloud package — CloudOutdatedMachine for the refusal handling, and CloudMove.holds moved verbatim to CloudWait.holds — so CmuxNextApp's line count drops.