Skip to content

fix(parakeet): config-change restart loop killed the mic in v1.0.12 (hotfix) - #227

Merged
initcore0 merged 2 commits into
mainfrom
fix/parakeet-config-restart-loop
Jul 30, 2026
Merged

fix(parakeet): config-change restart loop killed the mic in v1.0.12 (hotfix)#227
initcore0 merged 2 commits into
mainfrom
fix/parakeet-config-restart-loop

Conversation

@initcore0

Copy link
Copy Markdown
Owner

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 AVAudioEngineConfigurationChange handler 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:

notification → teardown+rebuild → engine.start() posts notification → teardown+rebuild → …

~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):

  • v1.0.12 + pinned Shure MV7+: 35 restarts in a 10s session, no transcript.
  • v1.0.12 + BlackHole fixture harness: 65 restarts in 15s, no speech within the time limit; identical harness on v1.0.11 transcribes correctly.
  • stderr shows the storm: [Parakeet] input device configuration changed — restarting capture every ~240ms.

Fix

The restart is no longer reflexive:

  • A notification now schedules a settle-check (settleDelay 0.3s) instead of restarting inline; further notifications in the window coalesce into the pending check.
  • The check rebuilds only when the engine genuinely stopped (AVAudioEngine.isRunning == false at 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.
  • Rebuilds are budgeted (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

  • This build, same BlackHole harness, warm model: "The quick brown fox jumps over the lazy dog", exit 0, zero capture restarts, across three consecutive sessions.
  • swift test: 1908 green (new policy matrix cases: running→ignore, stopped→restart, budget→giveUp, stale fences).
  • ./build.sh clean.

Notes

🤖 Generated with Claude Code

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
initcore0 merged commit c6ed5bc into main Jul 30, 2026
10 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant