fix(webviews): relay local daemon peer state events
Carries #27971 (its lane is gone) with its commits and authorship, merged with current feat-cmux-next. The merge resolution keeps both the new _acpmux/peer_state_changed and base's _cmux/question_key_request notification branches, and when an open draws its chat from the attach before the paced list (attachedBeforeList, #29126), the peer snapshot is read once that list settles, so it still comes after the list's _acpmux/watch.
At the merge head: bun test of peerStateEvents, peerStateReducer, chat-unavailable, question-key-bridge, chatOpenPacedList, session-pages, cached-chat-open, switch-frames and peer-copy: 103 pass, 0 fail.
Takes over #26885 (its lane is gone) with its commits and authorship, rebased onto current feat-cmux-next, and fixes the review finding that blocked it.
Local daemon peer transitions do not reach the web session's peer state today: the picker only sees the last _acpmux/status read. This relays them.
- A zero-parameter
_acpmux/peersrelay in both host policies: the macOS host (AcpmuxPaneMethods) and the shared Rust policy (cmux-agent-pane-policy/policy.json), so a Rust host doesn't refuse the request. The reply is filtered toname,state,lastSeenanderror, never URLs or credentials. The RustReplyShapegainsNumber(a finite number, never a boolean, matching Swift's.number) solastSeensurvives the filter. - The web client reads one snapshot once the chat list's
_acpmux/watchanswers, on each reconnect and after an_acpmux/laggednotice, then applies_acpmux/peer_state_changednotes from the same connection. It never polls. The snapshot isn't on the first frame's path: it lands and draws on its own, so the first frame keeps #27755's one round trip. - Notes, snapshot reads and
_acpmux/statusreads are ordered on one clock (the tick each read started at). An older read of either kind can't overwrite a newer note or a newer read, and a status read replaces any entry written before it started. - It only turns on when initialize advertises
peerStateEventsfrom alocalorigin. Peer, remote and unknown origins and older daemons keep the existing snapshot behavior.
The review finding, fixed here
The earlier review blocked #26885 at fbb1240e because peerStateEvents.ts accepted only the five PeerLinkState values. _acpmux/peers summaries use PeerState::as_str (cmux-tui/crates/acpmux/src/peer_link.rs), which also sends restarting while a peer's daemon restarts (peer_state_changed maps that to reconnecting). The adapter dropped that row, so the authoritative snapshot cleared the peer's state and connected, and environmentReady (state undefined, connected !== false) treated the peer as ready. The adapter now accepts every state the daemon sends. A restarting peer keeps state: "restarting" and connected: false, so it is listed but not offered as ready, the same as the _acpmux/status path already does on base.
Later reviews of this branch found more:
- A second path to the same result: a peer that
_acpmux/statuslists but the authoritative snapshot omits (removed between the two reads, or a row the adapter could not read) also lost its state andconnected.applyPeerStatesnow marks such a peer not connected, keeping thelastSeenandattemptsthe offline banner shows. - A snapshot that said
restartingnever cleared if the restart then failed. Restarting and reconnecting are bothreconnectingon the wire, so the daemon sends no note, and the snapshot kept overriding newer status reads. A status read now replaces it (the one-clock fence above), andchatUnavailablecountsrestartingas still connecting. - The Rust relay policy didn't list
_acpmux/peers(above).frames.jsonhad sampled it as a method the pane never sends; that refusal sample moved to_acpmux/peer_remove, which stays unlisted.
Checks on the original
-
defaults guard: the "default" is the
replyShapestable hash (AcpmuxPaneMethods+ReplyShapes.swift, the new_acpmux/peersreply filter). It is a relay reply allowlist, not a setting default, and it changes no user-facing default. The guard clears with an independent review receipt at the head. -
check-fixed-waits (web lint): red on base. It flags
chatOpenTrips.test.ts, added by #27755, which this PR doesn't touch, so I pushed with aCMUX_WEB_LINT_SKIP_REASONnaming the base sha. -
tests/swift.rs
the_known_params_are_the_macos_hosts: red on base (acp.session.fork'sliveparam is missing frompolicy.json); this PR's_acpmux/peersentries match the Swift tables.
No capture: no new or changed UI. This keeps the existing picker rows' existing state labels current from daemon events, and a real peer transition can't be staged in a capture scene.
Relay analysis
_acpmux/peers takes no parameters (any parameter is refused, AcpmuxPanePeerSnapshotTests) and runs no command. It returns only the local daemon's peer names and link state, which _acpmux/status already relays. Only the local origin may consume it, so a peer daemon's connection never sees this host's peer inventory (#26860).
Testing
Each fix has its red test committed first:
-
test(webviews): reproduce a restarting peer dropped from the peer snapshot:cd webviews && bun test src/agent-session/acpmux/peerStateReducer.test.ts src/agent-session/acpmux/peerStateEvents.test.tsgives 16 pass, 2 fail (the reducer drops the restarting row; the client never reportsrestarting, so the picker reads the peer as ready). After the fix: 18 pass. -
test(webviews): reproduce a listed peer the peer snapshot omits reading as ready:bun test src/agent-session/acpmux/peerStateEvents.test.tsgives 10 pass, 1 fail (peerConnectedisundefined). After the fix: 11 pass. -
The restarting-state, first-frame and chatUnavailable reds:
bun test src/agent-session/acpmux/peerStateEvents.test.ts11 pass, 2 fail, andbun test test/chat-unavailable.test.ts1 fail; then green. -
The out-of-order reads:
bun test src/agent-session/acpmux/peerStateEvents.test.ts13 pass, 2 fail; then green. -
The Rust policy:
cargo test -p cmux-agent-pane-policyon CI (cmux-ci). Parityrepliesfailed at the red commit; at the head, parity passes 16 of 16.
At the head, the peer-state, reducer, environments and chatUnavailable suites give 30 pass, 0 fail.
Observed / Not run
- Observed: the focused Bun suites above, red then green;
scripts/web-lint.sh(the type-aware lint and type check are left to CI on this host);scripts/cmux-next/prepush-gate.py, no new violations. - Not run: the native
AcpmuxPanePeerSnapshotTests(left to CI's Swift lanes); a real remote-machine transition. A run of the wholesrc/agent-session/acpmuxfolder in one Bun process timed out on CI, so the PR's own web test jobs cover the rest.
Changelog
- Fixed: the machine picker follows local peers connecting, dropping and restarting live, and a restarting or just-removed machine is no longer offered as ready.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Relays local daemon peer state transitions to the web session so the machine picker shows peers connecting, dropping, and restarting live. The picker previously only saw the last _acpmux/status read.
- Adds a zero-parameter
_acpmux/peersrelay to both the macOS and Rust host policies, filtered toname,state,lastSeen, anderror. - The web client reads one peer snapshot after the chat list's
_acpmux/watchanswers and then applies_acpmux/peer_state_changednotes; it never polls, and the snapshot stays off the first frame's path. - Notes, snapshot reads, and status reads are ordered on a single clock, so an older read can't overwrite a newer note or read.
- A restarting peer keeps
state: "restarting"andconnected: false, and a peer the snapshot omits is marked not connected, so neither is offered as ready. - Activates only when initialize advertises
peerStateEventsfrom alocalorigin; peer, remote, and unknown origins and older daemons keep the existing snapshot behavior.