Skip to content

fix(core): stop dropping broadcasts sent while peer connections are still dialling - #25

Merged
Mearman merged 2 commits into
mainfrom
fix/lost-patches-before-connection
Sep 9, 2026
Merged

fix(core): stop dropping broadcasts sent while peer connections are still dialling#25
Mearman merged 2 commits into
mainfrom
fix/lost-patches-before-connection

Conversation

@Mearman

@Mearman Mearman commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #23.

Two windows caused the loss described in #23, and both needed closing:

The join handshake resolved too early. connectToCoordinator() resolved once the introduction was sent, before the coordinator answered with the peer list — so init() returned before the post-join dials had even started, and the first broadcastPatch() fired with no connections and no dials under way. The handshake now resolves on the peer list (with the existing connect timeout as a degraded fallback for an unresponsive coordinator), so the dial loops — and their broadcast queues — exist before the first registration.

Broadcasts to dialling peers were discarded. The peer dials are fire-and-forget, so messages sent between connectToPeer() and the connection registering went nowhere. Each transport (TCP, TLS, WS) now queues broadcasts for dialling peers and flushes them in order when the connection registers, from either side of the dial (the dialler on connect, the acceptor on pong), bounded per peer so a dial that never completes cannot grow the queue unbounded.

Evidence: instrumented traces showed the coordinator never receiving the joiner's agent_upsert at all; after both fixes, the same trace shows the upsert arriving within milliseconds, and the restart case converges to active under the same agent ID.

Tests:

Also fixed here (separate commit): chasing the test flakes this work exposed found that generateIdentity() intermittently produced certificates OpenSSL rejects (illegal padding) — clearing the serial's sign bit can leave a leading zero byte, a non-minimal DER INTEGER, hitting ~1 in 128 generated identities and making tls.createServer fail despite its retries. The serial is now pinned to a minimal encoding, with a batch certificate-loading regression test wired into pnpm test (adds <1s).

Known boundary, deliberately out of scope: state_sync's receive path merges add-only, so it cannot repair an entity that changed while a peer was fully disconnected — converging that needs entity versioning and is a separate piece of work. The dial-window loss this PR fixes was the cause of every symptom traced so far.

Clearing the sign bit on the random serial number can leave a leading
zero byte, which is not a minimal DER INTEGER encoding; OpenSSL rejects
such certificates as illegal padding when they are loaded, so
tls.createServer failed for roughly one generated identity in 128 and
the bridge intermittently could not start. Pin a masked-to-zero first
byte to one, and load a batch of generated certificates in the test
suite to catch a reintroduction.
…till dialling

State patches broadcast in the window between store.init() returning and
the TLS data connections registering had nowhere to go and were silently
dropped, so a bridge that registers immediately after init() (as every
production bridge does through ensureRegistered) could stay invisible in
established peers' list_agents, and a restarted bridge with a persisted
identity could stay offline on peers that missed its re-activation
upsert even though delivery routing to it worked.

Two windows caused the loss. connectToCoordinator() resolved once the
introduction was sent, before the coordinator answered with the peer
list, so the post-join dials had not even started when the first
broadcast fired; and broadcasts to a dialling peer were discarded
because the dial is fire-and-forget. The handshake now resolves on the
peer list (with the existing connect timeout as a degraded fallback)
and each transport queues broadcasts for dialling peers, flushing them
in order when the connection registers from either side, bounded so a
dial that never completes cannot grow the queue unbounded.

The restart-continuity test no longer needs its settle-delay
workaround, and a regression test pins immediate-registration
visibility. Found while testing the persistent-identity work; also
exposed an intermittent invalid-certificate bug fixed separately.
@Mearman
Mearman marked this pull request as ready for review September 9, 2026 03:55
@Mearman
Mearman merged commit e945458 into main Sep 9, 2026
5 checks passed
@Mearman
Mearman deleted the fix/lost-patches-before-connection branch September 9, 2026 03:55
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-09T04:04:39.269288Z b75023e Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.25.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

State patches broadcast before TLS data connections are established are silently lost

1 participant