acpmux: a peer's watch list reaches the hub before the notices read after it

Merged
TE
10d ago
+118-15

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_on records the pending key of an _acpmux/watch request (Peer::watch_key).
  • The reader, when it resolves the reply with that key, publishes PeerNotice::Sessions on notices itself, 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 pending lock while it awaits room on notices.

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 Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.