PeerReader
PeerReader: ONE reader per peer connection, dispatching frames by tag.
Distributed actors 2/4, step 1 (the per-peer receive loop): specs/progress/2026-09-14-distributed-plane-flow-control-and-control-channel.md.
Until this module there was no receive loop at all. Every consumer read its own frames off the fd it was handed -- NodeCall.recv_reply, NodeSend.recv_failure, the discovery fixture's SwimDriver.decode_msg -- and each SKIPPED the frames it did not recognise. Two consumers on one connection therefore stole each other's frames: a MONITOR_FIRE read by a NodeSend loop waiting for a DELIVERY_FAILED was simply gone. A frame must be read once, by one reader, and handed to the consumer its tag names.
Tag space (the first element of the frame's msgpack array, an Int): 0..3 SWIM (SwimDriver) 5,6 REGISTRY_SYNC_REQ/RESP (GlobalRegistry) 7,8 MONITOR_REQ/FIRE (DistLink) 9,10 ACTOR_MSG/DELIVERY_FAILED (NodeSend) RPC frames (RemoteCall) carry NO tag: a request begins with the module name (a Str) and a reply with its correlation number (an Int that lands anywhere in the tag space). tag_of reports a request as untagged(); a reply is indistinguishable from a tagged frame by its first element, so RPC keeps its own connection (as NodeCall already requires) until the control/data split gives it a tagged envelope. That is the one wire change the split will make.
The dispatch is INJECTED, as in NodeRpc and NodeSend: a library cannot name an actor's constructors, so the caller's on_frame(tag, frame) decides what each tag means on this connection. Returning Err stops the loop (the reason is returned); returning Ok(false) stops it cleanly.