Skip to content

fix: escalate an interactive connect to a bond refresh when a bonded device is in range but refuses to open - #103

Merged
MegaManSec merged 1 commit into
mainfrom
fix/stale-bond-escalation
Aug 10, 2026
Merged

fix: escalate an interactive connect to a bond refresh when a bonded device is in range but refuses to open#103
MegaManSec merged 1 commit into
mainfrom
fix/stale-bond-escalation

Conversation

@MegaManSec

Copy link
Copy Markdown
Owner

Problem

A Magic device can sit at paired=true locally while it actually answers to the other Mac — a handoff done outside the app, or desynced state. An interactive no-peer take (dropdown click with the peer unreachable, takePeripheralFromPeer's fallback, the take-all fallback) then hits the bonded branch in connectPeripheral, fails openConnection(), and reports "Couldn't Connect" — with no path that ever breaks the dead bond. The user is stuck until they unpair manually in System Settings.

This ports the useful half of xneg's 22ed2d3, which routes no-peer takes through the pairing-refresh path unconditionally. Unconditional refresh would regress the common case though: clicking a switched-off peripheral that's healthily bonded to this Mac would -remove a good bond, fail the re-pair against an absent device, and cost the automatic reconnect macOS performs when the device comes back.

Fix

Escalate instead of always refreshing. When the plain open fails and the RSSI probe can still see the device — alive and in range, yet refusing the bonded connect, which is the stale-bond signature — the attempt removes the stale record and falls through to a fresh pair, the same refresh the peer-takeover path (refreshPairingBeforeConnect) already performs. If the probe can't see the device (merely off / out of range), it fails exactly as before and the bond is left intact.

Only the interactive entry point (connectPeripheral(_:)) opts in. The background watcher and reclaim paths keep retrying the plain open, so a transient link failure inside a retry loop can't repeatedly tear bonds down.

The remove-and-settle block is extracted into removeStaleBond(of:id:name:), shared between the existing refresh path and the new escalation.

Testing

  • Full-module swiftc -typecheck passes (this machine has no Xcode.app, so no xcodebuild run; the only pre-existing failure under CLT is the #Preview macro plugin).
  • Needs a two-Mac sanity pass: take a peripheral that's bonded to the sleeping Mac via the dropdown with no peer reachable (should now re-pair instead of erroring), and click a switched-off peripheral bonded to this Mac (should still fail with "Couldn't Connect" and reconnect on its own when powered back on).

…device is in range but refuses to open

A Magic device can sit at paired=true locally while it actually answers
to the other Mac (a handoff done outside the app, or desynced state).
An interactive no-peer take then hits the bonded branch, fails
openConnection(), and reports 'Couldn't Connect' with no path that ever
breaks the dead bond - the user is stuck until they unpair manually.

Now, when the plain open fails and the RSSI probe can still see the
device - alive and in range, yet refusing the bonded connect - the
attempt removes the stale record and falls through to a fresh pair,
the same refresh the peer-takeover path already performs. The probe
gate keeps a healthy bond safe: a device that is merely off or out of
range doesn't answer the probe, and its bond (and macOS's automatic
reconnect) is left intact, failing exactly as before.

Only the interactive entry point opts in; the background watcher and
reclaim paths keep retrying the plain open so a transient link failure
in a retry loop can't repeatedly tear bonds down. The remove-and-settle
block is extracted into removeStaleBond, shared with the existing
refreshPairingBeforeConnect path.
@MegaManSec
MegaManSec merged commit 28c0242 into main Aug 10, 2026
2 checks passed
@MegaManSec
MegaManSec deleted the fix/stale-bond-escalation branch August 10, 2026 10:37
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.5 🎉

The release is available on GitHub release

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.

1 participant