Stop writing the Mac-wide default input per dictation; fix Bluetooth start latency and route churn - #1702
Merged
Conversation
…start latency and route churn
The 2026-08-24 Bluetooth audit confirmed four interacting root causes behind
flaky AirPods dictation (retry storms, 1.1-1.5s starts, music artifacts,
default-input flip-flop):
1. Every Bluetooth-fallback dictation wrote kAudioHardwarePropertyDefaultInputDevice
to built-in at start and restored the AirPods at stop — two machine-wide HAL
writes per session, independent of any setting. Capture never needed it: the
per-process AUHAL bind (applyPreferredDictationInputDevice) does the real work.
Removed the write, the restore arming, and the now-orphaned temporary
crash-recovery marker plumbing. The restore/owner-lease seams in
ParakeetSystemInputCoordination stay as permanent no-ops for any future
temporary override.
2. Prewarm skipped all hardware work on exactly that route but still published
inputFormatReady=true, so every start attempted a doomed fast path (~420ms)
then paid the wait-loop floor. Deleted the deferral; prewarm/recovery now
always do the real AUHAL bind + verified format read (safe now that idle
prewarm no longer implies a Mac-wide write).
3. The untracked writes bypassed self-write detection, so
PersistentDictationInputController misread them as user mic choices and
silently relinquished ("Faster Bluetooth dictation" died mid-session; logs
showed 0 flaky starts in 4h45m while it ran, 24 in the hour after). Closed
by construction: dictation makes no Mac-wide writes at all now.
4. isLikelyBluetoothHandsFreeProfile could not flag fully-switched HFP (both
legs <=24kHz) or 0-rate unsettled reads, and settle/failure events never
carried hfp_suspected. Widened the heuristic (unmeasured nil rates still
report false) and added the field to audioFormatContext events.
Contract tests updated accordingly, including a negative pin: audioInputSnapshot
must never contain a system default-input write again. Added
scripts/dev/bluetooth-route-metrics.py to compare post-fix route health against
the captured pre-fix baseline (BT-route median 441ms / p90 1011ms vs 86ms
built-in; 53 Mac-wide writes/day).
Verified: bash build.sh --no-open; bash run-tests.sh (12195 passed).
Remaining: live AirPods go/no-go on real hardware (no CI covers Bluetooth).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Dictation over AirPods/Bluetooth was flaky: mic-start retries, fast starts falling back to a ~1.1–1.5s readiness wait, audible artifacts in music playback around dictation, and the system default input visibly flip-flopping between the AirPods and the built-in mic. A deep audit (13 parallel code sweeps + log forensics + git archaeology, then 3-lens adversarial verification of 8 hypotheses) confirmed four interacting root causes — all app-side.
Root causes → fixes
kAudioHardwarePropertyDefaultInputDevice→ built-in at start and restored the AirPods at stop — two machine-wide HAL writes per dictation, independent of any setting, while the per-process AUHAL bind already did all real capture work. Log ground truth: 53 writes and 29 default-input changes in one day; BT-route start median 441ms / p90 1011ms vs 86ms on built-in routes. Fix: removed the write, the restore-arming, and the orphaned temporary crash-marker plumbing. The owner-lease restore seams inParakeetSystemInputCoordinationremain as deliberate no-ops.inputFormatReady=truewith zero hardware work, so starts attempted a doomed fast path (~420ms) then paid the deterministic wait-loop floor (logged 1089–1523ms). Fix: deletedshouldDeferHardwarePrewarm/shouldDeferHardwareRecovery; prewarm and idle recovery now always do the real AUHAL bind + verified format read (safe now that this implies no Mac-wide write).PersistentDictationInputControllermisread them as user mic choices and permanently relinquished ownership mid-session. Logs: 0 flaky starts in the 4h45m the feature ran; 24 in the hour after it silently died. Fix: closed by construction — dictation performs no Mac-wide writes at all now.hfp_suspectedtelemetry was structurally blind (H3) to fully-switched HFP (both legs ≤24kHz — confirmed live false negative) and 0-rate unsettled reads, and the settle/failure events never carried the field. Fix: widened the heuristic (unmeasured nil rates still reportfalse) and addedhfp_suspectedtoaudioFormatContextevents. Side effect (intended): silent-callback zombie resets now also trigger on fully-switched HFP routes.Rejected after verification against real logs: macOS auto-switch as primary driver, stale-marker launch restore, HFP recovery recordings, idle-poisoning cadences.
Guardrails
Tests/BluetoothRouteContractTests.swiftnow pins the negative invariant:audioInputSnapshotmust never contain a system default-input write; the retired temporary marker may not be reintroduced. New suite covers the widened HFP cases.Sources/Speech/CLAUDE.mddocuments the invariant and its rationale.scripts/dev/bluetooth-route-metrics.pycompares post-fix route health (start-latency distribution, write counts, retry/fallback counts) against the captured pre-fix baseline, with a loud regression tripwire.Verification
bash build.sh --no-open— cleanbash run-tests.sh— 12,195 passed, 0 failedbash scripts/dev/agent-preflight.sh— confirms the above is the required set for this diffNet −128 lines (277+/405−): a removal-shaped fix.
🤖 Generated with Claude Code