Skip to content

fix: reject same-direction duplicate sessions in addPeer (followup to v0.3.79 PR #4)#5

Merged
sym-bot merged 1 commit into
mainfrom
fix/same-direction-duplicate-rejection
Apr 29, 2026
Merged

fix: reject same-direction duplicate sessions in addPeer (followup to v0.3.79 PR #4)#5
sym-bot merged 1 commit into
mainfrom
fix/same-direction-duplicate-rejection

Conversation

@sym-bot
Copy link
Copy Markdown
Owner

@sym-bot sym-bot commented Apr 29, 2026

Summary

Followup to #4 (v0.3.79). The dual-dial dedup introduced there applied the nodeId tie-break unconditionally whenever a prior bonjour transport existed. The tie-break assumes the prior is in the opposite direction of the new session — true for genuine simultaneous-dial collisions, but NOT for same-direction duplicates.

When two inbound sessions arrive in rapid succession (TCP retry, multipath race, repeated newConnectionHandler firing for the same advertised service), the tie-break was applied as if the prior were an outbound. For the higher-nodeId peer this returns preferNew=true, replacing the established healthy inbound with the duplicate. The replaced session was force-disconnected; its remote wire pair saw EOF; removeTransport fired on the remote side; peer-left storm continued.

Field evidence (iPhone↔Mac Catalyst on v0.3.79)

iPhone (higher nodeId) log shows the bug — two consecutive "replacing prior with new inbound" lines:

[SYM] session: handshake complete with Michael (019dd87c)
[SYM] peer: simultaneous-dial dedup — replacing prior with new inbound to Michael
[SYM] session: handshake complete with Michael (019dd87c)
[SYM] peer: simultaneous-dial dedup — replacing prior with new inbound to Michael    ← second one

The second "replacing prior" is tearing down the survivor of the first — exactly the symptom described above.

Mac (lower nodeId) consequence — peer flap continued:

[SYM] session: handshake complete with Hongwei (019dd87d)
[SYM] peer: connected: Hongwei (outbound, bonjour)
[SYM] wake: from Hongwei: apns
SYM peer joined: Hongwei (total: 1)
SYM metric: peer-joined
[SYM] session: disconnected: Connection closed         ← survivor torn down
[SYM] peer: disconnected: Hongwei
SYM peer-left pending for Hongwei — waiting 6s for reconnect

Fix

In addPeer, only apply the dual-dial tie-break when prior and new differ in direction. Same-direction prior → keep established session, reject duplicate. The duplicate's delegate is detached before disconnect() so its teardown can't ripple back through removeTransport.

let isDualDial = prev.isOutbound != session.isOutbound
let preferNew: Bool
if isDualDial {
    preferNew = SymNode.preferNewSessionInDualDial(...)
} else {
    preferNew = false  // same-direction → keep established prior
}

Tests

New test testSameDirectionDuplicatesAreRejected — truth-table coverage of both same-direction cases (both inbound / both outbound) for both nodeId orderings, plus a regression guard that the dual-dial tie-break still applies correctly through the combined decision.

71/71 tests pass.

Test plan

  • All existing unit tests pass (swift test)
  • New same-direction test passes
  • Verified on iPhone↔Mac Catalyst pair — single "peer connected" per cycle, no consecutive "replacing prior with new inbound" lines, no peer-left storm

🤖 Generated with Claude Code

v0.3.79 introduced dual-dial dedup but applied the tie-break unconditionally
whenever a prior bonjour transport existed. The tie-break assumes the prior
is in the OPPOSITE direction of the new session — which is true for
genuine simultaneous-dial collisions but NOT for same-direction duplicates.

When two inbound sessions arrive in rapid succession (TCP retry, multipath,
or repeated newConnectionHandler firing), the tie-break was applied as if
the prior were an outbound. For higher-nodeId peers this returns
preferNew=true, replacing the established healthy inbound with the
duplicate. The replaced session was force-disconnected; its remote wire
pair saw EOF; removeTransport fired on the remote side; peer-left storm.

Observed in the field on iPhone↔Mac Catalyst v0.3.79: iPhone log shows
two consecutive "replacing prior with new inbound" lines back-to-back —
the second replace tearing down the survivor of the first.

Fix: in addPeer, only apply the dual-dial tie-break when prior and new
differ in direction. Same-direction prior → keep established session,
reject duplicate (delegate detached + disconnect).

Test: testSameDirectionDuplicatesAreRejected — truth-table coverage of
both same-direction cases (both inbound / both outbound) for both
nodeId orderings, plus regression guard that dual-dial tie-break still
applies correctly through the combined decision.

71/71 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sym-bot sym-bot merged commit fd272c7 into main Apr 29, 2026
1 check passed
@sym-bot sym-bot deleted the fix/same-direction-duplicate-rejection branch April 29, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant