Skip to content

feat: adopt peripherals when the other Mac sleeps or vanishes#54

Merged
MegaManSec merged 2 commits into
mainfrom
feat/adopt-peripherals-from-absent-peer
Jun 11, 2026
Merged

feat: adopt peripherals when the other Mac sleeps or vanishes#54
MegaManSec merged 2 commits into
mainfrom
feat/adopt-peripherals-from-absent-peer

Conversation

@MegaManSec

Copy link
Copy Markdown
Owner

Why

"Peripherals follow the awake Mac" only had one half implemented. The holder releases on sleep (gated on the peer looking present), but nothing ever made the other Mac pick the peripherals up: on wake a Mac only chases what it held before its own sleep (connectedBeforeSleep), and the watcher only arms for drops of peripherals it was holding. The "it used to work" experience was the peripherals' own bond-level reconnection -- which stops working once a handoff has remove()d the receiving Mac's bond.

So: close the lid of the Mac holding the set, open the other one, and nothing happens. Order-dependence in the release gate (stale, persisted Bonjour isActive) made the release half flaky too.

What

1. Adoption -- a second, politer flavour of the auto-reconnect watcher (armAdoptionOfUnheldPeripherals / continueAdoption, bookkept in adoptionProgress). Registered peripherals this Mac isn't holding get armed from two triggers:

  • on wake, alongside the existing reclaim of the pre-sleep set (covers "both lids closed, then open the other one"), and
  • when the reachability poll sees a pinned peer miss two consecutive pings (~a minute) -- covers the holder sleeping/shutting down/leaving while this Mac is awake. Fires once per outage.

Adoption reuses the existing probe chain (RSSI-visible -> read-only HOLDS_ONE -> pair locally) but, having no prior claim, is deliberately more cautious than a reclaim:

  • takes only from a provably absent peer: two consecutive HOLDS_ONE probes failing at the connect layer (.connectionFailed / .connectTimeout); the second probe gives post-wake Wi-Fi a chance to come up before anything acts;
  • stands down the moment a live peer answers anything -- an explicit "not holding" (.bodyFailed) included -- so the prior holder's reclaim (or the user) outranks it and a simultaneous dual wake can't fight;
  • caps pair attempts at 3: a free Magic peripheral pairs on the first try, while one held by an unreachable-but-awake peer (router outage) just hangs the pairing -- the cap bounds the phantom "Pairing..." churn;
  • stands down (instead of reclaiming locally) when there's no trusted peer to consult (none registered / TOFU mismatch);
  • an explicit claim (genuine drop, failed handoff, wake reclaim) upgrades an adoption entry to a full reclaim; an adoption sweep never downgrades an existing reclaim.

Reclaim behaviour is unchanged, and everything sits behind the existing default-on "Reconnect peripherals if they drop" toggle (help text updated).

2. Release-on-sleep gate freshening. The gate keyed solely off Bonjour's isActive, which is event-driven and persisted, so it goes stale in both directions (sleep proxies keep a sleeping peer's records alive; a missed mDNS goodbye leaves a gone peer inactive) -- making the release silently environment- and lid-order-dependent. It now accepts either presence signal (isActive or the 30s .ping poll's verdict) and requires a clean fingerprint pin (a TOFU-mismatched peer can't be commanded, so it's no one to hand off to).

Result

Close the lid on the Mac holding the peripherals, open the other one: ~5s wake settle + two 5s probes, peripherals are pairing locally within ~15-20s -- whether or not the sleeping Mac managed to release them first. When the first Mac wakes later, its reclaim's HOLDS_ONE sees the peer using them and leaves them alone.

Pre-existing edge deliberately untouched: a waking Mac's reclaim still treats "peer unreachable" as free-to-take, so a Mac waking while the holder is off-network can pull peripherals back. That ambiguity exists today; adoption's politeness rule intentionally doesn't apply to reclaims so current behaviour is preserved.

Built with CODE_SIGNING_ALLOWED=NO, swift format clean. No new opcodes -- adoption only uses the existing read-only HOLDS_ONE.

A Mac only ever chased peripherals it was holding before its own sleep,
so the 'peripherals follow the awake Mac' story had a missing half: when
the holder slept (lid close, shutdown), nothing made the other Mac pick
them up — it only ever appeared to work via the peripherals' own
bond-level reconnection, which a prior handoff destroys.

Arm the existing auto-reconnect watcher in a new *adoption* flavour for
every registered peripheral not connected locally, from two triggers:

- on wake, alongside the existing reclaim of the pre-sleep set (covers
  'both lids closed, open the other one'), and
- when the reachability poll sees a pinned peer miss two consecutive
  pings (~a minute) — covers 'the holder slept while this Mac was awake'.

Adoption is deliberately more polite than reclaim, since this Mac has no
prior claim: it takes only from a provably absent peer (two consecutive
HOLDS_ONE probes failing at the connect layer), stands down the moment a
live peer answers at all — an explicit 'not holding' included, so the
prior holder's reclaim or the user outranks it and a simultaneous dual
wake can't fight — and caps its pair attempts at 3, because a free Magic
peripheral pairs on the first try while one held by an unreachable-but-
awake peer just hangs the pairing.

An explicit claim (drop, failed handoff, wake reclaim) upgrades an
adoption entry to a full reclaim; an adoption sweep never downgrades an
existing reclaim. With no trusted peer to consult (none registered, or
TOFU mismatch), adoption stands down instead of reclaiming locally.
The release-on-sleep gate keyed solely off Bonjour's isActive, which is
event-driven and persisted, so it goes stale in both directions: a
Bonjour Sleep Proxy keeps a sleeping peer's records alive (stale true),
and a withdrawn record / missed goodbye leaves an awake peer inactive
(stale false) — making the release silently environment- and
lid-order-dependent. Accept either presence signal: isActive, or the
30s .ping reachability poll's verdict. Also require the peer's identity
pin to be clean — a TOFU-mismatched peer can't be commanded, so it's no
one to hand off to.
@MegaManSec MegaManSec merged commit f57af51 into main Jun 11, 2026
2 checks passed
@MegaManSec MegaManSec deleted the feat/adopt-peripherals-from-absent-peer branch June 11, 2026 11:42
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.17.0 🎉

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