Skip to content

fix(realtime): surface pending-reply drop on reconnect via SpeechHandle.exception()#6436

Open
ByteMaster-1 wants to merge 2 commits into
livekit:mainfrom
ByteMaster-1:fix/pending-reply-drop-on-reconnect
Open

fix(realtime): surface pending-reply drop on reconnect via SpeechHandle.exception()#6436
ByteMaster-1 wants to merge 2 commits into
livekit:mainfrom
ByteMaster-1:fix/pending-reply-drop-on-reconnect

Conversation

@ByteMaster-1

Copy link
Copy Markdown
Contributor

Follow-up agreed in #6352 review ("yes we can make the drop on reconnect inside the plugin observable via SpeechHandle.exception()").

Incident. On a transient WebSocket drop, the OpenAI realtime session reconnects and _reconnect() discards any pending generate_reply future without re-creating the response. The turn silently produced nothing, and the discard was a bare RealtimeError — indistinguishable from a terminal failure, so an application couldn't tell it was safe to re-prompt.

Fix. Discarded futures are now failed with APIConnectionError (retryable=True) — the session is coming back, so re-prompting is sensible. The voice turn loop already routes generate_reply failures onto the SpeechHandle (_mark_done(error=), from #6304); its catch is widened to APIError so the discard deterministically lands on SpeechHandle.exception(). This reuses the same retryable-flag contract #6352 introduced in this file for fatal server errors.

App contract (no new types, no string-matching):

exc = handle.exception()
if isinstance(exc, APIError) and exc.retryable:
# transient reconnect discard — safe to re-prompt
Deliberately out of scope:

generate_reply timeout still raises bare RealtimeError; converting it to APITimeoutError for the same retryable contract is a follow-up PR.
The terminal "realtime session closed" path (fatal error / retries exhausted) stays a bare RealtimeError — nothing to retry against.
A response already streaming when the socket drops is closed via _close_current_generation (partial output, no error) — that's playout truncation, a separate issue.
Timing note: generate_reply has a 10s client-side timeout. If reconnection outlasts it, the timeout wins the race and the handle gets the (bare RealtimeError) timeout instead of the discard — acceptable, and the follow-up timeout PR will fold that into the same retryable contract.

Behavior change for direct callers: anyone consuming a raw RealtimeSession.generate_reply() future and catching only RealtimeError will now see APIConnectionError on reconnect. No in-repo caller is affected (only the voice turn loop consumes that future, and it's updated here).

…le.exception()

On a transient socket drop, the OpenAI realtime session reconnects and discards
any pending generate_reply future, then never re-creates the response — so the
turn silently produced nothing. The discard was raised as a bare
llm.RealtimeError, indistinguishable from a terminal failure.

Fail the discarded futures with APIConnectionError instead (retryable=True):
the session is coming back, so re-prompting is sensible. The voice turn loop
already lands generate_reply failures on the SpeechHandle via _mark_done(error=);
widen its catch to APIError so the discard reaches SpeechHandle.exception().

Apps can now distinguish a retryable drop without string-matching:

    exc = handle.exception()
    if isinstance(exc, APIError) and exc.retryable:
        ...  # transient reconnect discard — safe to re-prompt

The terminal "realtime session closed" path (fatal error / retries exhausted)
stays a bare RealtimeError — nothing to retry against.

Follow-up agreed in the livekit#6352 review.
@ByteMaster-1 ByteMaster-1 requested a review from a team as a code owner July 15, 2026 11:28

@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: 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

…eption()

The say() await in _realtime_reply_task caught only RealtimeError, while the
sibling generate_reply await catches APIError too. Make them symmetric so a
transient reconnect discard (retryable APIError) lands on the SpeechHandle for
any realtime plugin that implements say() via the response-future mechanism.
@ByteMaster-1

Copy link
Copy Markdown
Contributor Author

@longcw This is the follow-up we agreed on in the #6352 — the pending-reply discard on reconnect now lands on SpeechHandle.exception() as a retryable APIConnectionError, so apps can detect the dropped turn and re-prompt. PTAL when you get a chance.

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