Skip to content

fix(tts): don't mark FallbackAdapter primary unavailable on abort-before-first-audio#1290

Open
mrniket wants to merge 3 commits intolivekit:mainfrom
lottiehq-oss:fix/tts-fallback-abort-no-mark-unavailable
Open

fix(tts): don't mark FallbackAdapter primary unavailable on abort-before-first-audio#1290
mrniket wants to merge 3 commits intolivekit:mainfrom
lottiehq-oss:fix/tts-fallback-abort-no-mark-unavailable

Conversation

@mrniket
Copy link
Copy Markdown
Contributor

@mrniket mrniket commented Apr 22, 2026

Description

tts.FallbackAdapter marks the primary provider unavailable whenever a SynthesizeStream is 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 entire recoveryDelayMs — 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 !sawRawAudio guard and throws APIConnectionError("TTS stream completed but no audio was received"). The outer catch cannot distinguish that from a real silent provider failure and calls markUnAvailable(i).

Changes Made

  • agents/src/tts/fallback_adapter.ts — Short-circuit the !sawRawAudio branch in FallbackSynthesizeStream.run() when abortController.signal.aborted. On abort, emit END_OF_STREAM and 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 asserts adapter.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:check clean.
  • Added test fails on main with expected false to be true and passes with the fix.

…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-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 1ac93b5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 26 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-xai Patch

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.
@mrniket mrniket marked this pull request as ready for review April 22, 2026 13:45
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants