fix(tts): don't mark FallbackAdapter primary unavailable on abort-before-first-audio#1290
fix(tts): don't mark FallbackAdapter primary unavailable on abort-before-first-audio#1290mrniket wants to merge 3 commits intolivekit:mainfrom
Conversation
…ore-first-audio A caller interruption within the window between text push and first audio frame was hitting the !sawRawAudio guard in FallbackSynthesizeStream.run and being re-raised as APIConnectionError, which the outer catch translated into markUnAvailable(primary) — forcing subsequent utterances onto the fallback TTS for the recovery window. Treat abort-before-first-audio as a clean interruption: emit END_OF_STREAM and return without throwing. Real silent provider failures are unaffected.
🦋 Changeset detectedLatest commit: 1ac93b5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 26 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…abort - Match the success-path cleanup: await readInputLLMStream on the abort branch too, avoiding a potential dangling promise if input later throws. - Trim verbose test comments and the new source comment.
There was a problem hiding this comment.
🟡 Missing abort-before-first-audio fix in the non-streaming FallbackChunkedStream path
The PR fixes the abort-before-first-audio scenario in FallbackSynthesizeStream (streaming path) at agents/src/tts/fallback_adapter.ts:568-573, but the identical bug pattern exists in FallbackChunkedStream.run() at agents/src/tts/fallback_adapter.ts:377-381. When a FallbackChunkedStream is aborted (via close()) before the inner TTS produces any audio, the for await loop at line 340 exits with 0 iterations (the abort check inside the loop at line 341 never fires because there are no items). Then !sawRawAudio is true and an APIConnectionError is thrown unconditionally, which the catch block at line 386 catches and calls this.adapter.markUnAvailable(i) — incorrectly marking a healthy provider as unavailable due to a user-initiated interruption, not a provider failure.
(Refers to lines 377-381)
Was this helpful? React with 👍 or 👎 to provide feedback.
Description
tts.FallbackAdaptermarks the primary provider unavailable whenever aSynthesizeStreamis aborted before its first audio frame arrives. Caller interruptions within the provider's typical TTFA window (a few hundred ms) routinely trip this, forcing subsequent utterances onto the fallback TTS for the entirerecoveryDelayMs— silent dead air during that window if the fallback is misbehaving.Root cause: when the outer abort fires before any audio is received,
FallbackSynthesizeStream.run()drops through to the!sawRawAudioguard and throwsAPIConnectionError("TTS stream completed but no audio was received"). The outer catch cannot distinguish that from a real silent provider failure and callsmarkUnAvailable(i).Changes Made
agents/src/tts/fallback_adapter.ts— Short-circuit the!sawRawAudiobranch inFallbackSynthesizeStream.run()whenabortController.signal.aborted. On abort, emitEND_OF_STREAMand return cleanly instead of throwing. Real silent provider failures (where abort is not signalled) still throw and still mark the primary unavailable.agents/src/tts/fallback_adapter.test.ts— Added a regression test. The primary emits no audio;stream.close()is called before the first frame; the test assertsadapter.status[0].available === true. Without the fix the primary is marked unavailable. The two existing "silent failure triggers fallback" tests still pass, guarding the regression in the other direction.Testing
pnpm vitest run agents/src— 684 passed, 2 skipped.pnpm format:checkclean.mainwithexpected false to be trueand passes with the fix.