b87c735c1e
Android System TTS (and iOS) read-aloud could stop offline and refuse to continue — #4613 "stops at the end of the chapter, won't advance" and #4408 "stops at random intervals" — after which the play/headphone controls felt wedged. Root cause: `TTSController.#speak` only auto-advances when the last event code is `end`. The native client surfaces an offline engine failure as a terminal `error` code (Android `UtteranceProgressListener.onError`). This typically happens on a specific utterance the offline engine can't synthesize — e.g. an unsupported character — characteristically the first utterance after a chapter boundary, even with a local/offline voice (online, engines often fall back to network synthesis, which is why it only breaks offline). On `error` the controller never called `forward()` and left `state` stuck at `playing`, so playback dead-ended and the controls couldn't recover. Edge/Web clients throw instead (handled by `error()`), so only the native client hit this. Fix (native-scoped, no change to the Edge/Web path): when the active client is the native client and an utterance ends with a terminal `error` (still playing, not aborted, not one-time), skip that chunk and advance just as a normal `end` would — re-speaking the same unsynthesizable text would only fail again. A consecutive-error cap stops playback gracefully if the engine can't speak anything, so a wholly-unusable engine doesn't silently race to the end of the book and the state machine always leaves `playing`. Tests: tts-controller covers skip-on-error advancing past a bad chunk, and the consecutive-error cap stopping gracefully (bounded, not wedged in playing). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>