Skip to content

fix(xai): reconnect the STT stream when its socket drops - #2538

Open
CodeWithMoin wants to merge 2 commits into
livekit:mainfrom
CodeWithMoin:fix/xai-stt-ws-monitor
Open

CodeWithMoin wants to merge 2 commits into
livekit:mainfrom
CodeWithMoin:fix/xai-stt-ws-monitor

Conversation

@CodeWithMoin

Copy link
Copy Markdown

Description

The xAI half of #2469. When the xAI STT socket drops mid-session, the stream never reconnects, it just stops recognizing. Same root cause #2470 fixed for Deepgram: Promise.all got the wsMonitor Task itself, which isn't thenable, so it resolved instantly and the monitor's rejection never reached the retry loop.

Switching to wsMonitor.result makes the retry reachable, which exposes a second problem, so this also scopes each attempt's audio sender to its connection. Otherwise the old sender keeps pulling frames off the input queue after a reconnect and steals them from the new socket.

Changes Made

  • plugins/xai/src/stt.ts:
    • await wsMonitor.result in #runWS
    • an attempt AbortController per connection: the sender reads input.next({ signal }) so the parked read is cancelled, not just raced, and #runWS aborts it and waits for the sender to settle before returning
    • the sender's wait on transcript.created also ends on abort, so a socket that drops before the server is ready can't hang teardown
    • audio.done only goes out on an open socket
  • plugins/xai/src/stt_reconnect.test.ts: new test with a local ws server that drops the first connection as soon as audio arrives, then checks the stream reconnects and gets a final transcript from the second one.
  • .changeset/xai-stt-reconnect.md: patch for @livekit/agents-plugin-xai.

Same shape as the Deepgram fix, minus the heartbeat. Happy to add that too if you want parity.

Pre-Review Checklist

  • Build passes: All builds (lint, typecheck, tests) pass locally
  • AI-generated code reviewed: Removed unnecessary comments and ensured code quality
  • Changes explained: All changes are properly documented and justified above
  • Scope appropriate: All changes relate to the PR title, or explanations provided for why they're included
  • Video demo: n/a, needs a live xAI key; covered by the local socket test instead

Testing

  • Automated tests added/updated (if applicable)
  • All tests pass
  • Make sure both restaurant_agent.ts and realtime_agent.ts work properly (for major changes): not a major change
pnpm build:plugins
npx vitest run plugins/xai        # 4 files, 27 passed, 1 skipped (live test, no key)
npx tsc --noEmit -p plugins/xai   # clean
eslint + prettier on changed files # clean

The new test passes 3/3 with the fix. On main it times out, since the stream never reconnects.

Additional Notes

Refs #2469. Deepgram was the other half, already merged in #2470.

Promise.all was handed the wsMonitor Task itself, which is not thenable,
so it resolved instantly and an unexpected close never reached the retry
loop: the stream just stopped recognizing.

Await wsMonitor.result instead, the same change livekit#2470 made for Deepgram.
That makes the retry reachable, so also scope each attempt's audio sender
to the connection: cancel its queue read and wait for it to settle before
reconnecting, so a leftover sender cannot steal frames from the new socket.

Refs livekit#2469
@CodeWithMoin
CodeWithMoin requested a review from a team as a code owner September 19, 2026 15:04
@changeset-bot

changeset-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 32ecb6b

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

This PR includes changesets to release 39 packages
Name Type
@livekit/agents-plugin-xai Patch
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure 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-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-meta Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface 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-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen 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

devin-ai-integration[bot]

This comment was marked as resolved.

listenTask runs on the stream-wide abortController, so a closed socket
settled neither branch of its race and the old message handler stayed
attached after a reconnect, still able to feed late events into the
stream. Race it against the attempt signal too, detach the handler when
it ends, and wait for it in teardown alongside the sender.
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