Agent pane: a Cloud chat's prompt names the hand-off turn the app opened for it

Merged
TE
8d ago
+379-0

Agent pane: a prompt that starts a turn in a chat on a Cloud machine goes under the hand-off turn the app opened for it (handoff-<turn>-<id>), so acpmux on the machine reports that turn and the owner hears 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

  • cloudHandoffTurn.ts (new): the page asks the app cloud.handoffTurn {sessionId} and reads {turn}. It waits at most 2.5 s and never rejects. namesHandoffTurn matches a whole handoff-<1–15 digits, no leading 0>- prefix, the way acpmux's named_turn reads one.
  • direct.ts sendNow: once the chat is known, a prompt that starts a turn asks for its turn and goes as handoff-<turn>-<id>. The row, delivery and gesture ticket all key on that id from then on. Nothing is asked for:
    • a steer;
    • a prompt behind a running turn or the queue;
    • a Retry whose id already names a turn (it keeps its id, so acpmux dedupes it).
  • One ask per chat at a time. A second send while an ask is out waits for it and goes behind it under its own id. If the selected chat changes during the ask, the prompt goes back to its sender and nothing is drawn or sent.
  • App.tsx: the page asks only when the app's ready reply says cloudHandoffs: true and the chat runs on a Cloud machine (cloudMachineID). An app without the Swift side never says so, so on its own this PR changes nothing.

The app side (cloud.handoffTurn, which makes the session's hand-off at its first turn and begins each later one, plus the gesture pick that reads a labelled id as the same prompt) is #26919 (Swift, held), stacked on #26716 (the hand-off watcher).

Known limits

  • If the app answers after the 2.5 s wait, the prompt has already gone under its own id and that turn is not reported. The turn the app opened stays starting in the store, and the chat's next prompt that starts a turn names it, so nothing gets stuck.
  • Prompts queued or steered into a running turn start no turn of their own and are not reported separately.
  • The first bubble of a Cloud chat's turn draws after the app's answer, at most 2.5 s later.

Tests

  • cloudHandoffTurn.test.ts: turn parsing, the id, the timeout, the ask gate, the whole-prefix match.
  • test/agent-pane-cloud-handoff-turn.test.ts (a fake daemon records session/prompt):
    • the prompt goes as handoff-3-p-1;
    • with no turn it goes under its own id;
    • a steer, a Retry's named id, and a page with no ask all ask nothing;
    • a non-turn handoff-… id still asks;
    • two quick sends make one ask, in order;
    • a chat change during the ask sends and draws nothing.
  • cmux-ci (Linux pool): those tests, agent-pane-not-sent-retry and agent-pane-blank-prompt pass, and bun run check is green.

Risk: low for local chats, because the gate is false unless the app opts in. For Cloud chats, the first send of each turn waits up to 2.5 s.

Agent session: agent session

šŸ¤– Generated with Claude Code