Correctness:
- A child that stopped reading could freeze the whole client: the relay
writes under the context lock and the socket was blocking, so one stalled
peer took every download, upload and API call with it. Child writes now
have a deadline, and a child that hits it loses its slot.
- Relaying searches to children was gated by the leaf's answer budget, so a
parent forwarded a fraction of the stream while advertising itself as
healthy. The budget bounds our own share scan, which is what it was for;
the tree's stream goes down whole.
- A dropped client kept its listener port for the life of the process, so a
retried login advertised an ephemeral port nobody was told about. The
listener is now stopped and the operations thread let go.
- Excluded phrases were compared against a lowercased path without being
lowercased themselves, so a capitalised phrase never matched. They are
normalised where they arrive.
- Losing our own operatorship wiped the room's whole operator roster,
though we are still a member who can see it.
- A brokered ConnectToPeer of type D was logged and dropped; the peer is
now told, since we never dial out to adopt a child.
- A peer request made with no server connection returned Ok and parked
forever; it says NotConnected, as its docs always claimed.
- An empty interest was refused locally but still sent, leaving the
server's list and ours permanently apart.
Simplification:
- Deleted the excluded-phrase helper nothing called, and the field doc that
promised behaviour it never had.
- The four roster handlers and four standing handlers were eight
constructors carrying constants their own message code already implies:
one newtype each, 100 lines less.
- One place registers a search; the phrase list is borrowed, not cloned per
relayed search.