| name | pairing-diagnose |
| description | Diagnose Sonar pairing failures between two iPhones — QR-scan path silent, Tailscale shown connected but inactive, peer never goes online, distance ring flickering. Use when the user reports "they don't see each other", "QR-scan does nothing", "Tailscale is green but no audio", or any pairing-related bug. Walks through the four connection paths (AWDL → BLE → Tailscale → Internet) and isolates the failing one against real symptoms in the current code. |
pairing-diagnose
Sonar tries four connection paths in parallel, prioritised by latency: AWDL → BLE → Tailscale → LiveKit. Pairing problems almost always come down to one of those paths failing silently while the others either don't apply or are cosmetic. This skill is a decision tree to find the failing one fast.
When to invoke
- "the other phone doesn't show up"
- "QR scan does nothing on the receiving side"
- "Tailscale says connected but Sonar says no peer"
- "distance ring is flickering / 0 m / wrong"
- Anything tagged "pairing" or "verbinden" by the user
Step 0 — Establish baseline
Before guessing, get these facts:
- Both devices on
main and same version? Open SideStore → check version. If different, ship a release first (release-sidestore skill).
- Which path does the TopBar claim is active? It will say
AWDL, BT, Tailscale, or nothing. That's the claimed truth — we'll verify it.
- Is the user on real hardware or simulator? The simulator-relay E2E flow is its own world; if both ends are sims, jump straight to
scripts/e2e/run-simulator-e2e.sh and skip this tree.
Decision tree
A. QR-scan side never reacts
Symptom: side B scans the QR shown by side A, dialog appears or doesn't, but B never marks A online.
Anchor files:
sonar/UI/PairingView.swift — QR rendering and scanner.
sonar/Core/Pairing/PairingToken.swift — decode path; rejects unknown v != currentVersion.
sonar/Core/Pairing/PairingService.swift — token acceptance.
Check:
- Decode actually succeeded — log line
PairingToken.decode returns nil if base64url is corrupt or schema version mismatched. Two devices on different builds = guaranteed silent failure.
appState.pendingPairing is set after scan. If yes but no peer comes online, the token decoded but no transport could act on it — go to B/C/D.
- Known issue (open): scan-receive flow has been flaky in v0.2.7. See
project_open_issues.md. Treat any scan-side stall as a P0 bug, not a config problem.
B. AWDL path silent (no _sonar._tcp discovery)
Symptom: TopBar shows nothing or only BT, both phones are physically close, same WiFi.
Anchor: sonar/Core/Transport/NearTransport.swift.
Check:
- Both phones on WiFi enabled, even if no internet — AWDL needs the WiFi radio. WiFi-off + Hotspot-on is not the same.
- Same AWDL mesh — if one phone is on a corporate WiFi with client isolation, AWDL won't bridge. Toggle WiFi off/on on both.
- Bonjour service registers — search logs for
_sonar._tcp. Absent = NetService failed to publish, usually a permission prompt that was dismissed.
C. Tailscale shows green but no path active
Symptom: Tailscale app says Connected, both phones in same tailnet, but TopBar doesn't show Tailscale.
Anchor files:
sonar/Core/Pairing/PairingTokenGenerator.swift:33 — tailscaleIP() populates tsIP.
sonar/Core/Transport/TailscaleTransport.swift:75 — applyPairingToken no-ops if tsIP is empty.
Known issue (open): the token's tsIP is frequently empty even when Tailscale is up. Confirm with: paste the QR token's base64 into a decoder and check tsIP. If empty:
- Verify the device actually has a
100.x.x.x IP via getifaddrs (LSP/network inspector, or check Tailscale app).
PairingTokenGenerator.tailscaleIP() only finds it if the utun interface is up at token-generation time. Re-generate the QR (the regenerate button in PairingView) after Tailscale is fully connected.
- Both phones must be in the same tailnet — the classic trap is logging in with Google on one and GitHub on the other, which silently splits them. See
docs/connection-guide.md §3.4.
D. BLE-only path
Symptom: only path that works is BLE; you'd expect AWDL or Tailscale to be primary but they aren't.
Usually a consequence of A/B/C failing, not a root cause. If user complains about quality, that's because BLE is ~30 ms latency + ~50 kBit/s — Opus runs at 16 kBit/s on BLE. Don't blame audio code; surface the upstream path failure.
E. Distance ring flickers / wrong distance
Anchor: NIDiscoveryToken exchange in docs/connection-guide.md §2 ("NIDiscoveryToken-Austausch"), RSSIFallback for non-UWB devices.
Known issue (open): distance-ring stroke flicker is a known cosmetic bug. If the user is asking about the underlying distance value being wrong (not just visual flicker):
- Both phones have U1/U2 chip? iPhone 11+ for U1, iPhone 15 Pro+ for U2. Without UWB, fallback is RSSI → less accurate by design.
- NIDiscoveryToken bytes — the wire format is
[0x02][len:2B][NSSecureCoding payload]. Truncation here gives garbage distances.
Output to user
When done, summarise:
- Which path is actually working (verified, not what the UI claims).
- Which path is failing and why (one of A–E, with the specific anchor file).
- Whether it's a config problem (user fixes it) or a known bug (we fix it).
Do not recommend "restart the app" unless you've ruled out the above. That's the lazy answer.