From 7e42998e9e40faf5d43c840289a0923703c6e99c Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 24 Mar 2026 12:32:14 -0400 Subject: [PATCH 1/5] Gemini Live: fix potential EndFrame-deferral hang by handling turn_complete without usage_metadata We observed a case where a deferred EndFrame was never released in Gemini Live, causing the pipeline to hang indefinitely. The EndFrame deferral mechanism waits for _handle_msg_turn_complete to set _bot_is_responding back to False, but turn_complete messages were only processed if they also contained usage_metadata. If Gemini ever sent turn_complete without usage_metadata, the message would be silently dropped and the deferred EndFrame would never be released. Now turn_complete is always handled regardless of usage_metadata presence, with usage_metadata processing only when available. Note: we have not actually observed a turn_complete without usage_metadata in practice, so this is a theoretical fix for the EndFrame-deferral hang. The actual root cause of the observed hang may lie elsewhere. --- src/pipecat/services/google/gemini_live/llm.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 18b2c2e8a..649697c99 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -1334,13 +1334,12 @@ class GeminiLiveLLMService(LLMService): await self.broadcast_interruption() elif message.server_content and message.server_content.model_turn: await self._handle_msg_model_turn(message) - elif ( - message.server_content - and message.server_content.turn_complete - and message.usage_metadata - ): + elif message.server_content and message.server_content.turn_complete: + if not message.usage_metadata: + logger.warning("Received turn_complete without usage_metadata") await self._handle_msg_turn_complete(message) - await self._handle_msg_usage_metadata(message) + if message.usage_metadata: + await self._handle_msg_usage_metadata(message) elif message.server_content and message.server_content.input_transcription: await self._handle_msg_input_transcription(message) elif message.server_content and message.server_content.output_transcription: From 4abd4d031d1aeeea3d0261047f7c92a8fde0cb31 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 24 Mar 2026 12:50:07 -0400 Subject: [PATCH 2/5] 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. --- .../services/google/gemini_live/llm.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 649697c99..4a4ff43f4 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -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.""" From d05eb02b987e5c37d184298e40fd82da385d5978 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 24 Mar 2026 12:53:00 -0400 Subject: [PATCH 3/5] Add changelog for #4125 --- changelog/4125.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4125.fixed.md diff --git a/changelog/4125.fixed.md b/changelog/4125.fixed.md new file mode 100644 index 000000000..7db696a39 --- /dev/null +++ b/changelog/4125.fixed.md @@ -0,0 +1 @@ +- Fixed Gemini Live pipeline hanging indefinitely when an `EndFrame` was deferred while waiting for the bot to finish responding and `turn_complete` never arrived. As a possible root-cause fix, `turn_complete` messages are now handled even if they lack `usage_metadata`. As a fallback, the deferred `EndFrame` now has a 30-second safety timeout. From 063955b7eb1fe2903489d2d8f7083b48df548eee Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 24 Mar 2026 14:30:14 -0400 Subject: [PATCH 4/5] Gemini Live: clean up EndFrame deferral state on disconnect Cancel the deferral timeout task and clear the pending EndFrame during disconnect, which could otherwise be left dangling after a CancelFrame-triggered shutdown. --- src/pipecat/services/google/gemini_live/llm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 4a4ff43f4..33aaacaa6 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -1459,6 +1459,8 @@ class GeminiLiveLLMService(LLMService): if self._transcription_timeout_task: await self.cancel_task(self._transcription_timeout_task) self._transcription_timeout_task = None + self._cancel_end_frame_deferral_timeout() + self._end_frame_pending_bot_turn_finished = None if self._session: await self._session.close() self._session = None From dc56cb2ccc16f5d640dfca9d9f0d61c167b4aa31 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Tue, 24 Mar 2026 15:01:07 -0400 Subject: [PATCH 5/5] Gemini Live: reset _bot_is_responding when releasing deferred EndFrame Without this, the released EndFrame re-enters process_frame, sees _bot_is_responding is still True, defers again, and loops indefinitely. --- src/pipecat/services/google/gemini_live/llm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 33aaacaa6..e47e99e2f 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -1181,6 +1181,7 @@ class GeminiLiveLLMService(LLMService): """Release a deferred EndFrame and cancel the deferral timeout.""" if self._end_frame_pending_bot_turn_finished: self._cancel_end_frame_deferral_timeout() + self._bot_is_responding = False frame = self._end_frame_pending_bot_turn_finished self._end_frame_pending_bot_turn_finished = None await self.queue_frame(frame)