cmux NEXT Cloud: the agent ask and runs registry behind start-session (1/2)

Merged
TE
12d ago
+1085-0

Audit group §2d (p0-machines): start a VM from an ACP chat. This is part 1 of 2, split out of #25319 so each half gets its own review. This half is the agent ask and the runs registry. Part 2 is the cmux cloud start-session verb that uses them; it moves to its own PR on top of this one.

Lawrence: "start new VMs from acp chat", with "decouple persistence of acp trajectories from the VM itself, in case the VM is resuming".
Leo: "so chieff related stuff we dont need to worry too much about but ergo of commands and docs and error/hint messages we do need ... anyways those are just quite a few things that we already kinda own/should prob own"

What changes

Two pieces with no caller yet. Nothing on screen changes until part 2 lands.

CloudStartSessionAsks: an agent's ask, as a card in its own chat. The card reads "The agent asks to start …" and has Start and Not now. It works like cloud move's agent ask (CloudMoveAsks), and only a click on Start lets the run through.

  • From the ask until its run ends, the ask owns the chat's card. Another ask in that chat meanwhile is refused (askOpen) and never replaces it.
  • The card ends in any of these ways, and each removes it:
    • Not now (declined);
    • no answer within 2 minutes (expired);
    • a chat with no page (noPage);
    • a change of the signed-in user or team (accountChanged);
    • cancellation of the run.
  • After Start, the card shows the run's progress until finish. A failure stays on the card with Dismiss.
  • endAll() ends every card: a card still asking ends its ask, and a card whose run is going on says the start stopped (accountChangedWhileStarting). A Start clicked just before the account ended, with the ask not yet resumed, is void: the ask refuses as an account change.

CloudStartSessionRuns: what lets the same command again pick up a run. A start on a new machine can take minutes, longer than action.run waits.

  • Each caller waits for its run until its own deadline, then answers pending, and the run goes on.
  • The same ask again, while the run is going on, joins it.
  • A run that ends after every caller has left is kept, and the next same ask takes its end once. Kept runs last at most 10 minutes, at most 32 are kept, and nothing is kept once a caller has seen the end.
  • Runs are keyed by the machine, the agent, the folder, the asking chat, and the user and team signed in. Another account's ask never joins or takes one.
  • creating(name, account:): two asks for the same new name in the same account create one machine. A creation that finishes after forgetAll() hands its machine to nobody (its asker and joiners get CancellationError).
  • forgetAll() answers every waiting caller at once with its own refusal, cancels every run and creation, and keeps none of them. A caller whose deadline fired just before gets the refusal too, never pending.

CloudService holds both. It adds one observer of the signed-in user and team (account, read from the observable CloudAuth). A sign-out or a team switch calls forgetAll() and endAll(), so no ask or run outlives its account. signOut() also calls both first, before its link and install cleanup.

Strings are in a new table, CloudSessions.xcstrings, with 7 keys in all 21 languages the l10n gate requires. They don't touch Cloud.xcstrings.

Timers and loops

  • Card deadline: a one-shot Task.sleep(120 s) per open card, cancelled in the ask's defer once the card ends.
  • Card account watch: one ObservationStream of the signed-in account per open card, cancelled in the same defer.
  • Caller deadline: a one-shot sleep per waiting caller, cancelled once the run ends first. The caller's continuation is registered on the run and removed in the caller's defer. No task waits on the run for the caller.
  • The account observer: one ObservationStream in CloudService.start(), cancelled in stop() with the other observers.
  • Kept runs have no timer. They are trimmed when a run starts and when one is kept.

Size

1043 added lines, over the ~800 cap:

  • 458 are tests;
  • 195 are the translation table (7 keys, 21 languages);
  • 390 are code.

Tests

CloudStartSessionAsksTests:

  • The card:
    • a card that can't show leaves no ask;
    • the card's actions;
    • a second ask never takes a running ask's card, and another ask's token changes nothing;
    • Not now frees the chat;
    • an account change ends an open card;
    • a cancelled ask removes its card;
    • endAll ends an asking card and a started one (which says the start stopped);
    • a Start clicked just before the account ended starts nothing.
  • The runs:
    • a caller that answered pending holds nothing on a run that never ends;
    • forgetAll answers a waiting caller at once, and the next ask starts afresh;
    • a forget after a caller's deadline fired still refuses that caller;
    • a run whose caller saw it end is not kept;
    • the same ask during a run joins it;
    • a run every caller left is handed out once, both success and failure;
    • kept runs expire;
    • runs that end together keep at most the limit;
    • runs are per account.
  • Creations: two asks for one new name create once, while another team's ask creates its own. A creation that ends after forgetAll() hands out nothing.

No capture: nothing calls these yet, so nothing shows. Part 2 adds the DEBUG scene cloud-start-session-ask for the card.

Agent session: Air-Blue

🤖 Generated with Claude Code