Skip to content

fix(xai): emit one final user transcript per turn - #6729

Merged
longcw merged 3 commits into
mainfrom
longc/xai-realtime-repeated-final
Aug 7, 2026
Merged

fix(xai): emit one final user transcript per turn#6729
longcw merged 3 commits into
mainfrom
longc/xai-realtime-repeated-final

Conversation

@longcw

@longcw longcw commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #6710

xAI sends the transcript of an input item through conversation.item.input_audio_transcription.completed at every audio commit, and the item stays open across the pause that caused the commit.

Each of those events transcribes the item again from the start, punctuation and wording with it, so a final on every commit sent the same words back in a new form. One turn became several: repeated user_input_transcribed events, a duplicate message in session.history, and a new transcript segment on the client each time.

The plugin now holds the transcript and delivers the last one when the response speaks. That is the only signal xAI gives that the turn ended: it opens a response at every commit and drops the ones the user talks over, with no output and no response.done.

What else this corrects

A dropped response left its streams open, so the speech waiting on them ended only when the interruption timeout cancelled it after five seconds. It is now closed when the interruption sends its cancel.

An item anchored to one that xAI announced and then dropped was rejected by the remote chat context, which stranded every later item behind it. It is appended instead, with a warning.

A rejected item event never gets the conversation.item.deleted reply that settles it, so update_chat_ctx waited out its own five second timeout. Because the interrupted path awaits that update inside the speech, it took the speech down with it. The future now fails on the rejection, and update_chat_ctx logs a warning and continues. This part is in the openai plugin, since nothing about it is specific to xAI.

Also adds grok-voice-think-fast-2.0 to the supported realtime models.

Alternative

#6711 proposed fixing this in RoomIO, by keying the transcript segment on item_id. It is closed in favor of this one, because it reaches only the room transcription output: the console and any other TextOutput still see the repeated finals, as do AgentSession.history and the remote chat context.

xAI sends the transcript of an input item through
conversation.item.input_audio_transcription.completed at every audio
commit, and the item outlives the pause that caused the commit. Each
event re-transcribes the item from the start, wording and punctuation
included, so finalizing on a commit delivered the same words again in a
slightly different form. Hold the transcript instead and deliver the
last one when the response speaks, which is the only signal that the
turn ended.

Three faults around the same behaviour go with it. A response the user
talks over is dropped with no output and no response.done, so its
streams stayed open until the interruption timeout cancelled the speech
five seconds later. An item anchored to one xAI never announced was
rejected by the mirror, which stranded every later item behind it. A
rejected item event never gets its conversation.item.deleted reply, so
update_chat_ctx waited out its own timeout inside the speech.
@longcw
longcw requested a review from a team as a code owner August 6, 2026 13:25
devin-ai-integration[bot]

This comment was marked as resolved.

An error names the client event that drew it, while conversation.item
added and deleted name only the item, so the waiters need both indexes.
The event id map now holds the future itself, and every setter checks
that it is not already done, so a rejected delete no longer fails the
create still in flight beside it. The waiters are dropped in a finally,
so neither a rejection nor a cancellation leaves one parked under its
item id.

A fatal error keeps its old path. The rejection settles its future and
then falls through to the raise, so an exhausted quota still stops the
session instead of being swallowed as a rejected item.

Reconnect resets the input turn state before it snapshots the mirror,
since the xAI override flushes the held transcript there. The turn was
otherwise replayed to the new session without its text.
devin-ai-integration[bot]

This comment was marked as resolved.

The event id map outlived the update that filled it, so an error
arriving after the update gave up popped a waiter that was already
cancelled, settled nothing, and returned before the log and the emit. A
slow server rejection went unreported, where it used to surface as a
recoverable error.

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

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.

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

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.

room_io: user transcript segments ignore item_id, so an in-place item revision renders as N separate segments

3 participants