Skip to content

fix(ios): dictation race/lifecycle bugs found in review#26

Merged
metaforismo merged 1 commit into
mainfrom
fix/ios-dictation-races
Jun 11, 2026
Merged

fix(ios): dictation race/lifecycle bugs found in review#26
metaforismo merged 1 commit into
mainfrom
fix/ios-dictation-races

Conversation

@metaforismo

Copy link
Copy Markdown
Owner

A fresh code review of the iOS audio/dictation stack surfaced real bugs (all in recently-written code):

  1. Double-start crash (high): state flips to .recording only after the async recorder.start(), so two quick mic taps both started → a second installTap throws an uncatchable NSException. Added an isStarting guard set synchronously before the await.
  2. Empty-audio dead path (medium): recorder.stop() always returns ≥44 bytes (WAV header), so audio.isEmpty was never true → a no-speech tap got POSTed and errored. Guard on audio.count > WAVEncoder.headerSize.
  3. Stale interim leak (medium): LiveTranscriber.start() cleared interim only after the availability guard → an unavailable recognizer could show the previous session's words. Clear first (+ in stop()).
  4. Minor: failed engine.start() left the session active + observers installed; tear them down in the catch.

Build- + test-verified (31 iOS tests).

🤖 Generated with Claude Code

A fresh code review of the audio/dictation stack surfaced real bugs:

1. Double-start crash (high): `state` only flips to .recording after the async
   recorder.start(), so two quick mic taps both saw .idle and both started —
   installing a second audio tap throws an uncatchable NSException. Add an
   `isStarting` guard set synchronously before the await.
2. Empty-audio dead path (medium): recorder.stop() returns WAVEncoder output,
   which always includes the 44-byte header, so `audio.isEmpty` was never true —
   a tap-with-no-speech got POSTed and came back as an error. Guard on
   `audio.count > WAVEncoder.headerSize` instead.
3. Stale interim leak (medium): LiveTranscriber.start() cleared `interim` only
   after the availability guard, so an unavailable recognizer could leave the
   previous session's words on screen. Clear it first (and in stop()).
4. Minor: a failed engine.start() left the session active + interruption
   observers installed; tear them down in the catch.

Build- + test-verified (31 iOS tests).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
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