Cloud: keep acpmux's typed error up the relay and in cloud start-chat

Merged
TE
1d ago
+562-27

Leo, 2026-11-05: "I thought we had better typed errors than this. I thought we had a better solution than whatever was going on here."

A Cloud chat that failed on its machine lost its type on the way back. Two places collapsed it:

  • The machine relay (cmux-host/src/cloud/relay.rs) sent every acpmux failure up as chat.failed plus acpmux's last stderr line. That line is acpmux's whole --json envelope, cut at 300 characters. A failed session create always reported exit 1, so a no_session create read as a generic failure. The production fresh-machine proof (cmuxterm-hq run 42112686310) shows the result: chat.failed: {"error":{"code":"runtime","detail":"daemon_closed",... cut mid-object.
    Now the relay reads the envelope and sends up detail (daemon_closed, trust_required, ...), acpmux_code, retryable and error_kind, with acpmux's own sentence as message. A session create keeps its real exit. Only short code-shaped words go up (64 chars max: lowercase letters, digits and _.-). A sentence, a path or a value is dropped, and stderr with no envelope stays the plain failure it was. code keeps its values, so the platform and app consumers are unchanged.

  • cmux cloud start-chat (cli/cloud_start_chat.rs signed_out) treated any message containing "not signed in" or "sign in" as the cmux account being signed out. So acpmux's "Claude is not signed in here ..." (and any auth.* errorKind) became cloud.signed_out with "run cmux cloud sign-in": the wrong account and the wrong fix. Now a harness auth kind is never the account. The account is matched by its codes and by the app's own sign-in refusal text.

  • acpmux's daemon_closed (client.rs closed_error, cli/errors.rs) said only in prose why the connection closed. A panicked request task and a restart read the same, since both leave the daemon running. The error now carries data.cause (panicked, dropped, crashed, socket_left, shut_down) and data.panic, the newest panics.jsonl line from the last 10 minutes with only its thread, location and time (never its message or backtrace). The relay sends cause up. The prod proof's case is socket_left.

It also adds fixtures for the start's login decision (cmux-cloud-start-core harness_login): the Mac's cached Claude login present and absent, against each machine-login probe answer (none, rejected, failed exec, session, cached, working, unknown), for the composer and for a headless run. Further fixtures check that a cached login never moves a start past an earlier check and that the FFI wire holds no credential field.

Changelog

Fixed: Cloud chat failures keep their cause (a closed daemon, a refused permission, a Claude sign-in) instead of a generic failure or a wrong "sign in to cmux".

Tests

The regressions are in commits of their own, ahead of the fixes:

  • cargo test -p cmux-host --lib cloud::relay::tests: daemon_closed and retryable, permission, timeout, errorKind, a signal's exit taken from the envelope, the malformed and no-envelope fallbacks, field bounds, and a failed session create run through run_chat with a fake acpmux (exit 4 keeps chat.no_session).
  • cargo test -p cmux-tui --lib cloud_start_chat: a harness sign-in failure is not cloud.signed_out, and the account's refusals still are.
  • cargo test -p acpmux --lib -- client::closed_tests cli::errors: each cause, the panic line's fields only, old, test and broken lines ignored, and the CLI envelope's data.cause. These are new functions, so there is no behavioral red.
  • cargo test -p cmux-cloud-start-core --lib: the cache matrix and the ordering above.

Observed / Not run

Red and green results from the PR runner are recorded in the comments below.

šŸ¤– Generated with Claude Code

Agent session: agent session