cmux NEXT: the app opens a Cloud chat's hand-off turns (cloud.handoffTurn)

TE
5d ago
+557-5

cmux NEXT: the app opens a Cloud chat's hand-off turns. When the agent pane asks cloud.handoffTurn {sessionId}, the app makes the session's hand-off on its first turn, or begins the next turn once the last one ended. It answers the turn's number, and the page sends the prompt as handoff-<turn>-<id> (#26918). acpmux on the machine reports that turn, and the hand-off watcher from #26716 tells the person when it runs, needs them, finishes or fails.

Leo, 2026-10-28: "whatever's convenient for cloud, like that can be productionized, operationalized, we ought to just put out there"

Leo, 2026-10-28: "you can just say ... where should this chat run and stuff, and that's just about it ... it's somewhere in the cloud, and then it gets managed for you."

What changes

  • CloudHandoffTurns.swift (CmuxNextCloud):
    • CloudOwnerAPIClient.handoffOpen(session:machine:) makes the hand-off (POST /v1/handoffs) and handoffBeginTurn(_:after:) begins the next turn (POST /v1/handoffs/:id/turns). Both use the owner's session, the same three headers as the other owner calls, and a 20 s deadline.
    • The create answer's machine token is never read, kept or logged.
    • A refusal that carries the hand-off (conflict, turn_conflict, turn_running, closed) is read as that hand-off.
  • CloudHandoffTurnOpener.swift (CmuxNextApp):
    • It finds the asking pane's tab, the tab's daemon and that daemon's Cloud machine id. A chat on this Mac or an SSH machine asks nothing.
    • It names a turn that was made or begun but has not run yet, and begins the next turn only after the last one ended. A hand-off on another machine, a running turn or a closed hand-off answers a refusal code.
    • It starts HandoffNotices.watch for the hand-off. Only Cloud allowed, Labs on and signed in let it run.
    • It also needs the machine list loaded for the account signed in now (hasLoadedCurrentMachines), like the other Cloud entry points: after a team switch the previous team's machines stay in the store until the new list lands, so the handshake does not advertise cloudHandoffs and the runner refuses (machines_unread) before any store call. A chat opened before the list loaded gets no hand-off reports until it's reopened.
  • Agent pane:
    • The request cloud.handoffTurn is answered only for a remote chat's own session.
    • The handshake says cloudHandoffs: true only for a chat on a Cloud machine whose app may open turns.
    • AgentPaneHandoffPrompt.ownID makes the gesture pick read handoff-<turn>-<id> as <id>. A seeded prompt's ticket or a send ticket issued for <id> still redeems once, and only for that prompt.
    • A session/prompt gesture reservation strips the same one label, so a failed row's Trust resend, whose hold reserves under the labelled id it kept, still redeems its ticket.

Known limits

  • No size goes with the hand-off create: CloudSizePick.handoffSize is not on base (#26740 is held).
  • Prompts queued or steered into a running turn start no turn of their own and are not reported separately (page side, #26918).

Tests

  • AgentPaneHandoffPromptTests: ownID; the pick reads a labelled id as its own; a seeded ticket redeems once under a label; a send ticket works for its own prompt only; request decoding; a ticket reserved under the label allows its resend once.
  • CloudHandoffTurnOpenerTests: make, name, begin, and a refused or raced begin; wrong session or machine; JSON parsing; refusal codes the page may see; a machine list not loaded for the current account refuses.
  • On the fleet: AgentPaneHandoffPromptTests, CloudHandoffTurnOpenerTests, HandoffNoticeTests, AgentPaneUploadSendTicketTests and AgentPaneSandboxRunTests all pass.

Risk: medium. It touches the gesture pick, which is narrowed to a strict handoff-<1–15 digits>- prefix and covered by ticket tests. Everything else runs only for Cloud chats with Labs on.

No capture: no visible change. This adds the cloud.handoffTurn request, the hand-off API calls, a handshake flag and ticket-id parsing; nothing new is drawn. The notices a person sees come from the hand-off watcher already on base (#26716), and they need a live Cloud machine, which no tour or scene can stage.

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

Lets Cloud chats open hand-off turns for prompts: the app creates the session's hand-off at its first turn or begins the next one once the last ended, and the page labels the prompt handoff-<turn>-<id>.

  • Requires Labs, Cloud, sign-in, and the current account's loaded machine list; rechecks the pane's chat and machine after store calls. A pane with no chat yet is held to the first session it asks for.
  • Names turns only for the session recorded on the pane's tab, and refuses running or closed hand-offs and hand-offs on another machine; a closed-between-create-and-begin hand-off propagates its refusal.
  • Strips one turn label from gesture picks, prompt reservations, and the upload send-ticket check so tickets stay bound to the prompt's own ID: a Retry's hold under a label matches the resent frame's pick, and an id under two labels never redeems another's ticket. The create response's machine token is never read or logged.

No visible change. No turn names a hand-off on another machine or while a turn runs or waits. Covered by AgentPaneHandoffPromptTests and CloudHandoffTurnOpenerTests, all passing on the fleet.