Gemini Live: add safety timeout to EndFrame deferral to prevent indefinite pipeline hang

When an EndFrame arrives while the bot is mid-response, it is deferred
until turn_complete is received. If turn_complete never arrives, the
EndFrame gets stuck forever and the pipeline hangs indefinitely.

Add a 30-second timeout: if turn_complete hasn't arrived by then, the
deferred EndFrame is released anyway with a warning log. The timeout
is cancelled if turn_complete arrives normally.
This commit is contained in:
Paul Kompfner
2026-03-24 12:50:07 -04:00
parent 7e42998e9e
commit 4abd4d031d

View File

@@ -846,6 +846,7 @@ class GeminiLiveLLMService(LLMService):
# Bookkeeping for ending gracefully (i.e. after the bot is finished)
self._end_frame_pending_bot_turn_finished: Optional[EndFrame] = None
self._end_frame_deferral_timeout_task: Optional[asyncio.Task] = None
# Initialize the API client. Subclasses can override this if needed.
self.create_client()
@@ -1022,6 +1023,7 @@ class GeminiLiveLLMService(LLMService):
if self._bot_is_responding:
logger.debug("Deferring handling EndFrame until bot turn is finished")
self._end_frame_pending_bot_turn_finished = frame
self._create_end_frame_deferral_timeout()
return
await super().process_frame(frame, direction)
@@ -1173,8 +1175,46 @@ class GeminiLiveLLMService(LLMService):
self._bot_is_responding = responding
if not self._bot_is_responding and self._end_frame_pending_bot_turn_finished:
await self.queue_frame(self._end_frame_pending_bot_turn_finished)
await self._release_deferred_end_frame()
async def _release_deferred_end_frame(self):
"""Release a deferred EndFrame and cancel the deferral timeout."""
if self._end_frame_pending_bot_turn_finished:
self._cancel_end_frame_deferral_timeout()
frame = self._end_frame_pending_bot_turn_finished
self._end_frame_pending_bot_turn_finished = None
await self.queue_frame(frame)
# Timeout (in seconds) for the EndFrame deferral. If turn_complete is not
# received within this time, the EndFrame is released anyway to prevent the
# pipeline from hanging indefinitely.
_END_FRAME_DEFERRAL_TIMEOUT_SECS = 30.0
def _create_end_frame_deferral_timeout(self):
"""Start a timeout that releases the deferred EndFrame if turn_complete never arrives."""
self._cancel_end_frame_deferral_timeout()
async def _timeout():
await asyncio.sleep(self._END_FRAME_DEFERRAL_TIMEOUT_SECS)
if self._end_frame_pending_bot_turn_finished:
logger.warning(
f"EndFrame deferral timed out after {self._END_FRAME_DEFERRAL_TIMEOUT_SECS}s "
"without receiving turn_complete — releasing EndFrame"
)
await self._release_deferred_end_frame()
self._end_frame_deferral_timeout_task = self.create_task(
_timeout(), "end_frame_deferral_timeout"
)
def _cancel_end_frame_deferral_timeout(self):
"""Cancel the EndFrame deferral timeout if active."""
if (
self._end_frame_deferral_timeout_task
and not self._end_frame_deferral_timeout_task.done()
):
self._end_frame_deferral_timeout_task.cancel()
self._end_frame_deferral_timeout_task = None
async def _connect(self, session_resumption_handle: Optional[str] = None):
"""Establish client connection to Gemini Live API."""