acpmux: a failed turn says auth.signed_out or auth.expired, and health keeps an expired sign-in
Leo: “you gotta re-authenticate. You gotta log in again on the modal picker before the user even sends a prompt.”
The composer needs typed sign-in evidence from the daemon, without parsing an agent's error text. This is the first of three PRs for the daemon's _acpmux/session/auth. It types a sign-in failure where errors already carry a kind, and it keeps an explicit expired-token answer from collapsing into "signed out".
Turn evidence. A failed turn whose agent says it is not signed in gets errorKind: "auth.signed_out". One whose credential expired gets auth.expired. The kind is on the request's error data, on the turn_error and turn_result records, and so on lastTurn. recovery says to sign in with the harness's login command. A sign-in failure now wins over agent.exited, so an agent that died saying "Not logged in" is not shown as a crash. An agent that died saying its credential expired is not auto-resumed, as one that died signed out already was (turn_errors::resume_cause); an error that says the provider was unreachable is still retried, whatever it says of a credential: transient_cause and resume_cause read a sign-in failure as recovery::auth_kind does, and claude_auth::explain leaves such an error as the agent said it (so "HTTP 503: OAuth token has expired" stays a capacity error, and a dead agent's stays agent.exited). A connection or capacity error (turn_errors::says_unreachable: is_capacity_error, an HTTP 502 or 504, or says_connection_dropped) is never an auth kind, whatever it says about a token, so "HTTP 503: authentication_failed" stays a capacity error. The kinds use claude_auth::is_sign_in_failure and a new claude_auth::is_expired_credential.
Harness health (from #26631, which this PR replaces). A failed auth.check whose output says a credential expired now reads auth: "expired". Before, it read signedOut. A failed check whose output says the provider had no capacity or the connection dropped reads unknown, not signedOut. It still reads unknown when there is no evidence, and nothing reads the keychain. The expired matcher is claude_auth::is_expired_credential, shared with the turn kinds so the two cannot drift. It needs an explicit token, credential, API key or login that expired: a bare "session expired", "the MCP session expired", "invalid token expiry value" or "expires in 30 days" doesn't count. #26631's clippy red (E0004 in cmux setup's harness report) is fixed here: Expired reads as signed_in: false, with the same login action as signed out.
Tests (before: base 18af98cc with only the test added; after: this head):
typed_errors::typed_error_a_turn_that_is_not_signed_in_says_so: before FAILED (errorKindnull), after ok.recovery::tests::a_sign_in_failure_is_typed_and_a_transport_error_is_not(a transport error is never an auth kind; a dead agent's sign-in failure beatsagent.exited): before it doesn't compile (nokind::AUTH_*), after ok.harness_health::tests::expired_login_probe_is_not_just_signed_out(expired phrasings, and the near misses above stay signed out): after ok.
Checks at this head (cmux-ci run 40648972324): cargo fmt --check and clippy -D warnings on acpmux and cmux-tui are clean. acpmux lib passes 1574, including turn_errors::tests::a_dead_agent_is_continued_unless_it_died_of_a_limit_or_sign_in with its expired and unreachable cases and claude_auth::tests::an_outage_that_names_a_token_is_not_explained_as_a_sign_in. typed_errors, harness_health, claude_auth_env, hub_integration (511) and the route suites all pass. The crash ratchet (scripts/ci/crash_ratchet_compare.py) is no worse than base, because the expired matcher carries no expect.
Risk: low. A client that doesn't know the two new kinds treats them as plain errors, as recovery.rs documents. harness/health rows may now say auth: "expired". The web settings card and the Swift settings row read auth as a string, and neither treats an unknown value as signed in.
No capture: the only webview change is a doc comment in the generated protocol types; nothing renders differently.
Next: _acpmux/session/auth, which reads these kinds (PR 2), then the route probe, the target form and peers (PR 3).
Agent session: agent session
🤖 Generated with Claude Code