Repository navigation
fix(webviews): relay local daemon peer state events - #29494
Conversation
On base 1f723c6, bun test src/agent-session/acpmux/peerStateEvents.test.ts runs 7 tests: 3 fail and 4 pass. The client never reads peers and ignores the local state feed. Nonlocal and feature-absent fallback cases pass. Native validation is held by #26809. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c
Gate peer events on the advertised feature and local/app origin. Read once after watch at initialize/reconnect, retain exact daemon state, clear nullable metadata, deduplicate lag replay, and reject superseded sockets/reads. Add a zero-parameter pane snapshot relay filtered to name, state, lastSeen and error. Preserve picker presentation and policies. Linux web validation: 14 focused tests / 62 assertions pass; bun --bun run check passes including formatting, lint, typecheck (2933 files), and protocol checks. The focused regression was red before repair (3 failed / 4 passed). Native relay tests were added but native build/test/parse and CI dispatch remain held by #26809/HQ4779. No real-machine capture is claimed. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c
The replacement socket must initialize its own local/app origin and peerStateEvents feature before receiving peer notes; invalidate prior read tokens at open start. Focused Linux web tests: 15 pass, 66 assertions. Full bun --bun run check passes. Native verification and CI remain held by #26809/HQ4779. Agent-Session: https://cmux.com/deeplink/agent?id=aid-93c3a66aa0c6bbfa650c
The daemon drops peer_state_changed notices when a client lags and says so with _acpmux/lagged; resyncing only the selected chat left a machine that went down meanwhile shown as connected. Review P2 on #26885. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-710a4a518c009a6ad126
…pshot _acpmux/peers says `restarting` (peer_link.rs PeerState::as_str) while a peer's daemon restarts. The adapter drops that row, so the authoritative snapshot clears the peer's state and the picker reads it as ready. Red: bun test src/agent-session/acpmux/peerStateReducer.test.ts src/agent-session/acpmux/peerStateEvents.test.ts: 16 pass, 2 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
_acpmux/peers names every PeerState acpmux has, `restarting` included (peer_link.rs as_str), while peer_state_changed maps it to reconnecting. The adapter now accepts it, so a restarting peer keeps its state and reads not connected instead of falling back to ready in the picker. Green: bun test src/agent-session/acpmux/peerStateReducer.test.ts src/agent-session/acpmux/peerStateEvents.test.ts: 18 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…ng as ready A peer that _acpmux/status lists but the authoritative _acpmux/peers snapshot omits (removed between the two reads, or an invalid row) loses its state and `connected`, so environmentReady offers it as ready. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 10 pass, 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
An authoritative _acpmux/peers snapshot replaced the peer maps with its own rows, so a peer the status read still lists lost its state and `connected` and the picker offered it as ready. Such a peer now reads not connected until a note or the next read says otherwise. Green: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…r-events-restarting-1030
…r-events-restarting-1030
…r-events-restarting-1030
When the authoritative peer snapshot omits a peer the status read lists,
its peerStatus becomes only { connected: false }, so the offline banner
(chatUnavailable) loses lastSeen and attempts.
Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 10 pass, 1 fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
The authoritative-snapshot branch replaced an omitted peer's status with
{ connected: false } alone. It now keeps lastSeen, attempts and error
from the last read and drops only the stale state, so the offline banner
still says when the machine was last seen.
Green: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…r-events-restarting-1030 # Conflicts: # webviews/src/agent-session/acpmux/direct.ts
…t relay policy The web pane sends _acpmux/peers (zero parameters) and the macOS host admits it, but policy.json does not list it, so a Rust host refuses the request and its reply is not filtered. The cases also keep a numeric lastSeen, as the Swift .number shape does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…n the first frame - A status read after a snapshot said `restarting` keeps it: a failed restart goes restarting -> reconnecting, both `reconnecting` on the wire, so no note says so and readPeers re-applies the snapshot. - connect awaits _acpmux/peers before the first frame, a second round trip after the one #27755 keeps it to (a held snapshot holds connect). - chatUnavailable calls a restarting peer offline instead of connecting. The initial snapshot is settled in the fixture, and the in-flight status test now races a real in-flight read. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 11 pass, 2 fail; bun test test/chat-unavailable.test.ts: 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
The web pane sends _acpmux/peers and the macOS host admits it with a zero-parameter rule and a name/state/lastSeen/error reply filter. policy.json now does the same, so a Rust host no longer refuses the request. ReplyShape gains Number (a finite number, never a boolean), mirroring Swift's .number, so lastSeen survives the filter. Red at a4b6b6e (cmux-ci, cargo test -p cmux-agent-pane-policy): parity replies FAILED, 15 passed, 1 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…pshot leaves the first frame - A _acpmux/status read now drops every peer event entry no note has updated since the read started (PeerStateEvents.statusRead), so a failed restart that the daemon reports only as a state (no note) reads reconnecting instead of restarting forever. - connect starts the _acpmux/peers snapshot once the list's watch answered and draws it when it lands, instead of awaiting it before the first frame (#27755's one round trip). - chatUnavailable counts a restarting peer as still connecting. Green: bun test peerStateEvents, peerStateReducer, environments and test/chat-unavailable.test.ts: 28 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…al samples another unlisted method frames.json sampled _acpmux/peers as a daemon method the pane never sends. The pane now sends it (policy.json admits it), so those two refusal cases move to _acpmux/peer_remove, which stays unlisted, and a pass case covers _acpmux/peers like _acpmux/status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
The status-read fence and the snapshot fence are separate, so: - a snapshot that started before a status read, and landed after it, brings back the restarting state the status read cleared; - a status read that started before a snapshot, and landed after it, deletes the newer snapshot's state. Red: bun test src/agent-session/acpmux/peerStateEvents.test.ts: 13 pass, 2 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
PeerStateEvents now ticks one clock for notes and for the start of every read (statusStarted, readStarted). A snapshot that landed after a status read started later is dropped, and a status read drops only entries written before it started, so an older read of either kind can neither bring back a cleared restarting state nor erase a newer snapshot. A status read clears the snapshot's authority only when it is newer. The reducer tests take the read's tick in place of a separate revision. Green: bun test peerStateEvents, peerStateReducer, environments and test/chat-unavailable.test.ts: 30 pass, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…notes drop The stale-socket guard dropped every message from a socket the client had replaced, so a request asked on it never settled: the export that watches for a socket swap waited forever instead of rejecting (conversation/exportClient.test.ts timed out on this branch and passes on base). Only _acpmux/peer_state_changed from a replaced socket (or after close) is dropped now; everything else is received as on base. Red: bun test ./src/agent-session/acpmux/conversation/exportClient.test.ts times out at a01e4e5 (CI react-apps-check shard 1: TIMEOUT twice, passes replayed on base ad6de25). Green: 3 pass; peerStateEvents 15, reducer 8, chat-unavailable 7 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
readPeers writes each _acpmux/status reply into peers, peerConnected, peerStates and peerStatus before the clock fence runs, so of two refreshPeers calls the one asked first and answered last wins. The fixture's status read now answers with what the daemon knew when the read reached it. Red: bun test ./src/agent-session/acpmux/peerStateEvents.test.ts: 15 pass, 1 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
statusRead now says whether the read is newer than every status read that already landed (and than this connection); readPeers drops one that is not before it writes peers, peerConnected, peerStates or peerStatus, so two refreshPeers calls answered out of order keep the newer answer. Green: peerStateEvents 16, peerStateReducer 8, chat-unavailable 7, exportClient 3, chatOpenTrips 3, agent-pane-peer-copy 55, agent-pane-cached-chat-open 10 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c35ebd1681cf79649972
…r-events-restarting-1030
…r-events-restarting-1030 # Conflicts: # cmux-tui/crates/cmux-agent-pane-policy/tests/cases/params.json
Keeps both the peer_state_changed and _cmux/question_key_request notification branches, and reads the peer snapshot after the list settles when the open is drawn from the attach (attachedBeforeList). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-2dcca989fa7ce7778b47
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Stale: these images are of Gallery before/after for
States
Pick other states with a Default changesThis pull request changes 1 default at
Signed off on this commit by Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
|
Review (codex gpt-6-astra high fast): SHIP at 6cdcb49 Follow-ups:
|
|
Reds at 6cdcb49, by test name:
|
|
Review (codex gpt-6-astra high fast, carried by patch-id): SHIP at cedc3dd cedc3dd is a clean merge of feat-cmux-next (the CmuxNextAppTests compile fix #29486, #29478) into 6cdcb49. |
|
Review (the owner lane's independent review, at its land request): SHIP at cedc3dd Recorded by lane-gh-broker from this PR's owner lane, which asked to land this exact head after its independent review. next-autoland lands it once every red is the base's; a BLOCK at this head, a HOLD line or a hold label still stops it. |
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_changedand base's_cmux/question_key_requestnotification 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 testof 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/statusread. This relays them._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._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 cmux-next: a chat's first frame waits on one daemon round trip #27755's one round trip._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.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
fbb1240ebecausepeerStateEvents.tsaccepted only the fivePeerLinkStatevalues._acpmux/peerssummaries usePeerState::as_str(cmux-tui/crates/acpmux/src/peer_link.rs), which also sendsrestartingwhile a peer's daemon restarts (peer_state_changedmaps that toreconnecting). The adapter dropped that row, so the authoritative snapshot cleared the peer's state andconnected, andenvironmentReady(state undefined,connected !== false) treated the peer as ready. The adapter now accepts every state the daemon sends. A restarting peer keepsstate: "restarting"andconnected: false, so it is listed but not offered as ready, the same as the_acpmux/statuspath already does on base.Later reviews of this branch found more:
_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.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._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 cmux-next: a chat's first frame waits on one daemon round trip #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/peerstakes 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/statusalready relays. Only thelocalorigin 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
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.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
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith 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/statusread._acpmux/peersrelay to both the macOS and Rust host policies, filtered toname,state,lastSeen, anderror._acpmux/watchanswers and then applies_acpmux/peer_state_changednotes; it never polls, and the snapshot stays off the first frame's path.state: "restarting"andconnected: false, and a peer the snapshot omits is marked not connected, so neither is offered as ready.peerStateEventsfrom alocalorigin; peer, remote, and unknown origins and older daemons keep the existing snapshot behavior.Written for commit cedc3dd. Summary will update on new commits.