acpmux: a peer's watch list reaches the hub before the notices read after it
What was wrong
Peer::serve (cmux-tui/crates/acpmux/src/peer.rs) runs the link handshake in a spawned task. That task awaited request(_acpmux/watch) and then sent PeerNotice::Sessions(list) on notices. Meanwhile the read loop completed the request's oneshot, kept reading frames, and forwarded the peer's session_changed notices onto the same channel. A purge or recreate the reader read right after the watch reply could therefore reach Hub::apply_peer_notice (hub/peers.rs) before the older watch list, and the list then put the purged row back into remote_sessions.
Reported by pm-snapshot (p0-machines lane); confirmed on feat-cmux-next at a8d57c9a.
The fix
link_request_onrecords the pending key of an_acpmux/watchrequest (Peer::watch_key).- The reader, when it resolves the reply with that key, publishes
PeerNotice::Sessionsonnoticesitself, before it completes the oneshot and before it reads the next frame. So the list is on the channel before any notice read after the reply. - The handshake no longer sends Sessions. It sends Connected after its request returns, so Connected still follows Sessions.
- The reader no longer holds the
pendinglock while it awaits room onnotices.
One file, no protocol change. pm-snapshot's stack (#25446 and siblings, peer_snapshot.rs) does not touch peer.rs. pr-overlap lists only #22030 (a build-stamp change to other lines of peer.rs), which is unrelated.
Test
peer::watch_order_tests::a_purge_sent_right_after_the_watch_reply_lands_after_its_list drives serve over an in-memory WebSocket. The fake peer writes the watch reply (listing m1) and a session_changed purge of m1 in one write. The test checks the notice order, then replays the notices into a Hub and asserts that no purged row is left.
Before (base a8d57c9a, test only, cmux-ci run 40351379633):
test peer::watch_order_tests::a_purge_sent_right_after_the_watch_reply_lands_after_its_list ... FAILED
panicked at crates/acpmux/src/peer.rs:1278:9:
the list came after a newer purge: [SessionChanged { session: Object {"sessionId": String("m1")}, kind: "purged", seq: 2 }, Sessions([Object {"name": String("lane"), "sessionId": String("m1"), "status": String("idle"), "updatedAt": Number(5)}]), Connected]
test result: FAILED. 0 passed; 1 failed
After (this head, cargo test -p acpmux --lib peer::, cmux-ci run 40351922959):
test peer::watch_order_tests::a_purge_sent_right_after_the_watch_reply_lands_after_its_list ... ok
test result: ok. 7 passed; 0 failed
Risk: low. The change is limited to peer link handshake ordering. The list is now published only from the reader, and only for a watch reply that still had a waiting caller. The only callers of _acpmux/watch are in the handshake: the hub never forwards it to a peer (SESSION_SCOPED_EXCLUDED).
Agent session: big-red
š¤ 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.