Skip to content

v3.1.0

Choose a tag to compare

@steffen-heil-secforge steffen-heil-secforge released this 16 Sep 20:36
· 184 commits to main since this release

The membership of a conversation is now one message the server states,
not a list each client reassembles.

BREAKING, protocol 4. peerJoined, peerLeft, rosterComplete and
joined.peerCount are gone. A 3.x client against a 4 server waits for a
rosterComplete that never comes; the version says so at connect rather
than leaving it to be noticed. Both mcp-hub-server and chat-relay serve 4.

Why state and not deltas. The roster used to arrive as one
peerJoined per peer, terminated by a rosterComplete, with deltas
after. That asks a client to answer a question it cannot: where a
server's roster burst is not serialised ahead of live traffic — and it
was not — a newcomer joining mid-enumeration is indistinguishable from a
roster entry. Counting the peers cannot fix it, and neither can waiting:
"that is everyone" was not provable from this side at all. Four separate
defects were being bounded one at a time, each a symptom of that.

roster { members, readAt? } carries the whole membership, including
the peer receiving it, and is re-sent in full on every change. A client
that misses one repairs on the next instead of drifting. It also
dissolves a displacement bug rather than patching it: a departure
publishes who is here NOW, recomputed at send time, so a peer whose
identity was reclaimed while its old connection was still tearing down
cannot be announced as gone.

There is no count on the wire, because the list is the count — and
because an absent peerCount decoded as zero. A server that simply
stopped sending it would have had this client tell the model "No other
peers are in this session yet" about a session with two people in it: an
absent field turned into a confident sentence, in the one place a reader
acts on it.

Attachments. A reference-form attachment is fetched with its own
deadline instead of the 5s control-plane ack timeout, which was a
size-independent bound on a transfer of the bytes themselves. When it
expired the claim was already gone, so the data arriving a moment later
was discarded as unsolicited — a whole report was lost that way, with
the server reporting nothing wrong because nothing was. The claim is now
narrowed to the requested token, so a late answer to an abandoned
request can never be handed back as the wrong file.

hub_read never fetched attachments at all. A message read out of
history rendered as though it had none: no error, nothing to retry, no
reason to suspect anything was missing.

Delivery. Registering for activity now reports what is already
waiting, not just what arrives next. A connect fills the buffer before
the caller has finished wiring up, so the first events waited for some
later event to carry them out — on a quiet session, indefinitely. With
the roster as N events this was hidden, since each one was another
chance to land after the wiring; as one event it became a lost message.