Repository navigation
fix(realtime): keep the reply in progress when the session reconnects - #7907
Rehansanjay wants to merge 1 commit into
Conversation
A response that finishes writes each message's transcript into its mirror item in `_handle_response_done`. A connection that drops mid-reply never delivers that event, so the item is left with empty content and the reconnection replay drops it, since it copies the context with `exclude_empty_message=True`. The replayed conversation then ends on the user's request with nothing answering it, and the model answers that request again on the next turn: the caller hears the agent stop mid-sentence and then reply to the question before last. On a provider that recycles sessions this repeats for the life of the call. Flush the transcripts the current generation has accumulated into the mirror before taking the snapshot that is replayed, which keeps what was already said as a truncated assistant turn the way an interruption does. Each transcript is cleared once written, so a reconnection whose sends fail and is retried cannot append the same text twice. Addresses the first half of livekit#7867. The second half -- that DEFAULT_MAX_SESSION_DURATION is 20 minutes for every provider while Gemini Live caps audio sessions at 15 -- is a default worth deciding separately.
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings β view it in Devin Review. (Configure)
| remote_item.item.content.append(item_generation.audio_transcript) | ||
| item_generation.audio_transcript = "" |
There was a problem hiding this comment.
π‘ Replayed reply includes unspoken words
If transcript deltas precede audio at disconnect, _flush_generation_transcripts replays words the caller never heard. The next reply treats those words as already spoken.
Learn more
Transcript deltas and audio deltas arrive in separate server events; audio handling buffers each independently. On a dropped connection, the last received transcript can describe audio that never arrived or played. The replay serializes that transcript as an assistant answer, although realtime playout has no corresponding audio to forward. The reverse ordering also leaves spoken audio with an empty transcript, which this flush skips entirely.
Example: The server sends transcript The code is 1234 but only sends audio for The code is before disconnecting. The replay claims the assistant already said 1234, so a follow-up can proceed without giving the caller the code.
Recommended fix: Reconcile the replayed transcript with audio actually forwarded by the voice generation task, or preserve only the portion whose audio was confirmed delivered. Account for audio received before any transcript as well; do not treat the full accumulated transcript as proof of speech.
Was this helpful? React with π or π to provide feedback.
|
This flag is right about the mechanism, and I had a counter-argument that does not survive reading I was going to say the flush is consistent with how an interruption already behaves. It isn't. An interruption sends Why I would still land it, and I'd rather state the trade than have a reviewer find it: Without the flush the replayed conversation ends on the user's request with nothing answering it, and the measured consequence is the model answering that request again on the next turn β the caller hears the agent stop mid-sentence and then reply to the question before last, recurring every ~15 minutes for the life of the call on The principled version needs the played length reconciled from the voice generation task, which is the only place that knows it, and getting it into the mirror before the replay is sent on a socket that is already gone. That is a bigger change than this, and the ordering is the hard part, not the arithmetic. Happy to go either way: land this as the smaller improvement with the limitation documented, or close it and open an issue for the reconciled version. I don't think leaving the turn out entirely is the better default, but it's your call. |
Addresses the first half of #7867.
The bug
A response that finishes writes each message's accumulated transcript into its mirror item, in
_handle_response_done:A connection that drops mid-reply never delivers that event. The assistant item the response created is in the mirror, but with empty content, and
_reconnectreplaysso the item is dropped. The replayed conversation ends on the user's request with nothing answering it, and the model answers that request again on the next turn. What the caller hears is the agent stopping mid-sentence and then replying to the question before last.
The reporter saw it recur for the life of a call: on
google/gemini-3.8-livethrough LiveKit Inference,server_errorroughly 15 minutes after each (re)connection β errors at 18.0, 33.4 and 48.8 minutes in one 65-minute session β because Gemini Live caps audio sessions at 15 minutes whileDEFAULT_MAX_SESSION_DURATIONrecycles at 20. A routine recycle is clean, since it waits for the current generation to finish; only the error path loses the reply.The fix
Flush the transcripts the current generation has accumulated into the mirror before taking the snapshot that gets replayed. That keeps what was already said as a truncated assistant turn, which is what an interruption produces anyway β
_handle_response_doneruns on a cancelled response too, so this makes the dropped-connection path behave like every other way a reply can end early.Each transcript is cleared once written.
_reconnectrestoresold_chat_ctxif its sends fail and is then retried, and the transcript is still on the generation at that point, so without clearing a retry would append the same text twice.Nothing else in the reconnect path moves:
_close_current_generationstill runs where it did, after the replay events are sent, so the generation's channels and futures are torn down in the same order.Tests
tests/test_openai_realtime_reconnect_replay.py, three cases built on theRealtimeSession.__new__harness the existing chat-context tests use, each asserting against exactly the copy_reconnectreplays:Verified by mutation β with the flush returning early and everything else unchanged:
The assistant turn disappears from the replay and the conversation ends on the user's question β the reported symptom.
Not in this PR
The issue's second half:
DEFAULT_MAX_SESSION_DURATIONis 20 minutes for every provider, while Gemini Live caps audio sessions at 15 without context-window compression, so the limit surfaces as a provider error rather than a clean recycle. A shorter default forgoogle/models, or handling Google'sGoAwaywith session resumption, are both defaults-and-policy calls rather than a bug fix β happy to send whichever you'd prefer.