fix(parakeet): config-change restart loop killed the mic in v1.0.12 (hotfix) - #227
Merged
Conversation
The v1.0.12 regression (mic dead on every dictation, fine on v1.0.11): PR #224's AVAudioEngineConfigurationChange handler restarted capture on EVERY notification. The notification also fires for changes the handler's own teardown+rebuild causes — on this hardware a capture start itself posts one — so the first notification at session start entered a self-sustaining teardown→rebuild→notification loop (~4 restarts/second, 65 restarts observed in one 15s session). Capture never lived long enough to feed the decoder; every session timed out with no speech. Fix — the restart is no longer reflexive: - A notification schedules a settle-check (0.3s, storm-coalescing) instead of restarting inline. - The check rebuilds only when the engine genuinely STOPPED (AVAudioEngine.isRunning == false — the real device-loss case the observer exists for). A notification that leaves the engine running is a transient renegotiation and is ignored; restarting a running engine is exactly what fed the loop. - Rebuilds are budgeted (3 per capture session). A device that still stops capture after that fails the session loudly (gaveUpMessage) instead of looping silently. Gates live in core CaptureConfigChangePolicy (settleDelay, budget, the action matrix) and are pinned by CaptureConfigChangePolicyTests. Verified live against the released artifacts on the reporting machine: v1.0.12 reproduces the loop with both the pinned Shure MV7+ (35 restarts /10s) and a BlackHole fixture harness (65 restarts/15s, empty transcript); this build transcribes the fixture correctly across consecutive sessions with ZERO capture restarts. swift test green (1908). Co-Authored-By: Claude <noreply@anthropic.com>
…ing engines Resolves the conflict with #226, which ported the ORIGINAL (reflexive) restart handler to WhisperKit/AppleSpeech/SpeechAnalyzer — those three call sites had both stopped compiling against the new 4-arg policy and carried the exact same self-sustaining restart-loop bug this hotfix kills. Each now schedules the same coalesced settle-check: rebuild only if the engine actually stopped, budgeted per session, loud give-up past the budget. Co-Authored-By: Claude <noreply@anthropic.com>
initcore0
added a commit
that referenced
this pull request
Jul 30, 2026
Both sides added independent state: keep #227's settle-check fields and observer teardown alongside this branch's pendingFinalizeStops counter (the runStop defer stays first so every exit path balances the stop enqueue's increment). Co-Authored-By: Claude <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.
The v1.0.12 regression
Symptom (user report): mic completely dead on every dictation in v1.0.12; downgrading to v1.0.11 fixes it. v1.0.12's only functional change is #224.
Root cause: #224's
AVAudioEngineConfigurationChangehandler restarts capture on every notification. But the notification also fires for io-unit changes the handler's own teardown+rebuild causes — and on the reporting machine a capture start itself posts one. So the first notification at session start enters a self-sustaining loop:~4 restarts/second for the whole session; capture never lives long enough to feed the decoder; every session times out with no speech.
Reproduced live against the released artifacts on the affected machine (warm model, cold-load noise excluded):
no speech within the time limit; identical harness on v1.0.11 transcribes correctly.[Parakeet] input device configuration changed — restarting captureevery ~240ms.Fix
The restart is no longer reflexive:
settleDelay0.3s) instead of restarting inline; further notifications in the window coalesce into the pending check.AVAudioEngine.isRunning == falseat settle time) — the real device-loss case the observer exists for (fix(parakeet): restart capture on mid-session input-device configuration change #224's goal). A notification that leaves the engine running is a transient renegotiation and is ignored; restarting a running engine is exactly what fed the loop.maxRestartsPerSession= 3, reset per capture session). A device that still stops capture after that fails the session loudly (gaveUpMessage) instead of looping silently — fix(parakeet): restart capture on mid-session input-device configuration change #224's word-loss guarantee is preserved, without the loop.Decision matrix lives in core
CaptureConfigChangePolicy(tested); the engine holds only the observer, the coalescing flag, and the rebuild.Verification
swift test: 1908 green (new policy matrix cases: running→ignore, stopped→restart, budget→giveUp, stale fences)../build.shclean.Notes
isRunning == false, which a real device loss produces, but that's asserted by reasoning + tests, not by a live unplug in this run.🤖 Generated with Claude Code