Cloud: relay-first chat in cloud start-chat

Merged
TE
17h ago
+1045-19

Summary

The app and CLI half of the relay-first Cloud chat (cmuxterm-hq#7086; Leo: "it should take <30 seconds from starting a thing to having a response on cloud"). With #35785, a Cloud machine holds a socket to the platform from boot. A prompt now goes to the machine over HTTPS and down that socket, so the first reply never waits for this Mac's link to the machine.

  • cmux cloud relay-chat --target machine:ID --agent A --prompt P [--cwd C] [--prompt-id ID] (new action cloudRelayChat) posts the chat to the platform and reads the turn's NDJSON stream (CmuxNextCloud/API/CloudRelayChat.swift).

    • It answers when the turn ends: session_id, reply, stop_reason, first_text_ms, total_ms.
    • If the turn is still running at 35 s, it answers what has come so far with ended: false, and the stream stays open until the turn ends (closing it would cancel the turn on the machine).
  • delivered: false means the prompt certainly never reached the machine. That covers three cases:

    • the platform refused it;
    • no machine connected within the 30 s the request asks for (start_within_ms);
    • the run was refused before sending (an agent's run, a bad target or argument).

    The platform alone decides whether a chat was sent, so a resend never races a send.

  • cmux cloud start-chat --wait tries the relay first and falls back to start-session and the link only when delivered is false or the app has no relay-chat. Any other failure, including a broken stream or a late answer, is reported with how to see the machine's sessions. It is never sent again.

    • A turn that is still running is waited on in its session with the existing poll loop, and the relay time counts against --timeout.
    • Each call carries its own --prompt-id.
  • An agent's run is refused by relay-chat and goes the start-session way, which asks the person.

  • The App lines this adds come with plans/cmux-next/arch-exceptions/cloud-relay-chat-7086.json. The binding needs App-only types; the logic is in CmuxNextCloud.

Depends on, at runtime: manaflow-ai/cmux#35785 (the machine side) and the platform relay route. Until both are deployed, start-chat falls back to today's path.

Changelog

none

Testing

Observed / Not run

  • Observed: Swift suite CloudRelayChatTests (stream parsing, delivery, the whole reply in done) and the cmux-tui cloud_start_chat unit tests are added. Both will run through the fleet after this push, and the results will be posted here.
  • Independent review subagent (correctness first): SHIP at 093c46fe123ce5d51c7670e226588bd0b458c99e. The delta since is the arch exception JSON and one message string.
  • Review (Codex, independent model family): SHIP at f82f5996 (the broken-stream fix).
  • Review (independent review subagent, delta c9ad820..279baa0: translations, then a "safe:" send-path cap allowlist entry for the 64 KiB refusal-body read): SHIP at 279baa03a52f6583a006f6c1b6b0a0b130d958ee
  • Guard group compared against base cf49e02: the one new red (send-path caps) is fixed in 279baa0; the other 3 are red on the base too.
  • Not run: an end-to-end chat against prod. That needs the platform route deployed and a machine image with #35785; it is tracked in cmuxterm-hq#7086.
    Agent session: agent session

๐Ÿค– Generated with Claude Code


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


Summary by cubic

Implements the relay-first Cloud chat: a prompt now goes over HTTPS to the platform and down the socket the machine holds from boot, so the first reply no longer waits for a link to the machine.

  • New cmux cloud relay-chat action posts the chat and streams the turn's NDJSON back, answering with session_id, reply, stop_reason, first_text_ms, and total_ms.
  • cmux cloud start-chat --wait tries the relay first and falls back to start-session and the link only when delivered is false or the app has no relay-chat; a prompt over 96 KiB goes the link path, which uploads it. --timeout counts from the start of the relay attempt, and with less of it left than the relay's answer window the link path runs to keep to it.
  • delivered: false means the prompt certainly never reached the machine (the platform refused it, no machine took it within 30s, or the run was refused). A stream that breaks once the platform accepted the chat counts as delivered โ€” it may be on the machine โ€” so it and any other failure are reported with how to check the machine's sessions, never resent.
  • A turn still running after 35s answers with ended: false and keeps its stream open โ€” closing it would cancel the turn on the machine.
  • An agent's run is refused by relay-chat and goes the start-session way, which asks the person.
  • A person's palette run waits for the whole turn and shows the reply in a sheet; a refusal reaches the person directly, while the CLI and MCP keep the result fields.
  • Each call carries its own --prompt-id so a resend runs only once.
  • The relay request carries the bearer and the prompt, so it uses the redirect-refusing owner transport, not the machines client's session.
  • Until the machine-side socket and the platform relay route are deployed, start-chat falls back to today's path.