narrow-chef-71655
02/24/2026, 7:31 AMawait self._room.local_participant.publish_transcription(transcription)
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/participant.py", line 285, in publish_transcription
raise PublishTranscriptionError(cb.publish_transcription.error)
livekit.rtc.participant.PublishTranscriptionError: failed to send transcription, room closed: channel closed
WARNING:livekit.agents:failed to publish transcription
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 429, in capture_text
self._writer = await self._create_text_writer()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 405, in _create_text_writer
return await self._room.local_participant.stream_text(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/participant.py", line 578, in stream_text
await writer._send_header()
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/data_stream.py", line 189, in _send_header
raise ConnectionError(cb.send_stream_header.error)
ConnectionError: engine: connection error: engine is closed
WARNING:livekit.agents:failed to publish transcription
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 300, in _publish_transcription
await self._room.local_participant.publish_transcription(transcription)
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/participant.py", line 285, in publish_transcription
raise PublishTranscriptionError(cb.publish_transcription.error)
livekit.rtc.participant.PublishTranscriptionError: failed to send transcription, room closed: channel closed
WARNING:livekit.agents:failed to publish transcription
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 429, in capture_text
self._writer = await self._create_text_writer()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 405, in _create_text_writer
return await self._room.local_participant.stream_text(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/participant.py", line 578, in stream_text
await writer._send_header()
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/data_stream.py", line 189, in _send_header
raise ConnectionError(cb.send_stream_header.error)
ConnectionError: engine: connection error: engine is closed
WARNING:livekit.agents:failed to publish transcription
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/livekit/agents/voice/room_io/_output.py", line 300, in _publish_transcription
await self._room.local_participant.publish_transcription(transcription)
File "/usr/local/lib/python3.11/site-packages/livekit/rtc/participant.py", line 285, in publish_transcription
raise PublishTranscriptionError(cb.publish_transcription.error)
livekit.rtc.participant.PublishTranscriptionError: failed to send transcription, room closed: channel closed
what can i do ?
await self.room.disconnect()
api_client = api.LiveKitAPI()
try:
await api_client.room.delete_room(
DeleteRoomRequest(room=self.room.name)
)
<http://logging.info|logging.info>("☎️ Room deleted – call ended")
except Exception as e:
logging.error(f"☎️ Failed to delete room: {e}")
finally:
await api_client.aclose()tall-airline-16756
02/24/2026, 7:31 AMtall-airline-16756
02/24/2026, 7:32 AMPublishTranscriptionError: failed to send transcription, room closed: channel closed
• ConnectionError: engine: connection error: engine is closed
These errors mean you’re attempting to send transcription/text data after the room/engine has already been closed. Below are focused actions and code patterns to stop those errors and cleanly shut down.
What I’m answering: how to stop publishing transcriptions after the room is closed and how to safely disconnect/delete the room.
1) Stop publishing once the room/engine is closed
• Add an explicit guard before any call that publishes text:
if not self._room or not getattr(self._room, "is_connected", False):
# skip publishing — room is closed or already disconnecting
return
await self._room.local_participant.publish_transcription(transcription)
• Use this wrapper everywhere you would call publish_transcription() or stream_text().
2) Cancel background tasks that publish transcriptions before disconnect
• Long-running STT, capture, or agent tasks can try to publish after you call disconnect(). Cancel those tasks first, wait for them to finish, then disconnect:
# cancel tasks
if self._stt_task:
self._stt_task.cancel()
if self._capture_text_task:
self._capture_text_task.cancel()
# optionally await them with timeout to let cleanup run
try:
await asyncio.wait_for(self._stt_task, timeout=1.0)
except Exception:
pass
# then disconnect the room
if self._room and getattr(self._room, "is_connected", False):
await self._room.disconnect()
self._room = None # prevent accidental reuse
3) React to room lifecycle events and clean up immediately
• Listen for a room disconnected (or similar) event and cancel/pause publishing there so unexpected disconnects don’t trigger further publishes:
@self._room.on("disconnected")
def on_disconnected():
# cancel or flag tasks to stop publishing
if self._capture_text_task:
self._capture_text_task.cancel()
4) Order of operations for shutdown (safe sequence)
1. Cancel/pause any STT/agent tasks that generate transcriptions.
2. Ensure no further publish calls will be made (guards or set a flag).
3. Call await self.room.disconnect().
4. Use the LiveKit API to delete the room:
api_client = api.LiveKitAPI()
try:
await api_client.room.delete_room(DeleteRoomRequest(room=self.room.name))
finally:
await api_client.aclose()
5) Optional: use delete_room_on_close for agent sessions
• If you create agent sessions, delete_room_on_close=True can auto-delete the room when the session ends (useful if agent is the last participant). This avoids manually calling delete_room() in many flows.
6) Logging to diagnose race conditions
• Add clear logs when you cancel tasks, call disconnect, and when publish attempts occur. This helps identify if publishes are coming from tasks you forgot to cancel.
Why these fixes work
• The errors occur because once the underlying engine/connection is closed there is no channel to send data. Preventing further publishes (guards, cancelling tasks, listening for disconnect events) stops attempts to write to closed channels and removes the exceptions.
If you want, I can:
• Show a small complete shutdown example that integrates task cancellation, guards, disconnect, and room deletion.
• Help inspect where your transcription publish calls are scheduled so we can add the correct cancellations/guards.
Which would you prefer: a minimal shutdown snippet you can drop into your code, or help tracing where background tasks might still be running in your current codebase?
Sources: Agent session | LiveKit Documentationnarrow-chef-71655
02/24/2026, 7:36 AMapi_client.room.delete_room(DeleteRoomRequest(room=self.room.name))
finally:
await api_client.aclose()
?tall-airline-16756
02/24/2026, 7:36 AMdelete_room. I’ll show (A) how to track tasks, (B) how to cancel & wait for them, and (C) a safe full shutdown sequence including your delete_room call.
What I’m answering: how to cancel all tasks before calling api_client.room.delete_room(DeleteRoomRequest(room=self.room.name)).
A. Track tasks you start
• Keep a set of active tasks so you can cancel them later.
self.active_tasks = set()
def start_background_task(coro):
task = asyncio.create_task(coro)
self.active_tasks.add(task)
task.add_done_callback(lambda t: self.active_tasks.discard(t))
return task
• Use start_background_task() for any STT/capture/publish tasks (so they’re tracked automatically).
B. Cancel and await tasks safely
• Cancel all tracked tasks, then wait for them to finish cleanup. Awaiting lets those tasks handle CancelledError and do their own cleanup (e.g., stop STT pipelines, close writers).
async def cancel_all_tasks(timeout: float = 5.0):
# Cancel tasks that are still running
for task in list(self.active_tasks):
if not task.done():
task.cancel()
if not self.active_tasks:
return
# Wait for tasks to complete/cancel; ignore exceptions except log them
done, pending = await asyncio.wait(self.active_tasks, timeout=timeout)
for t in done:
try:
# re-raise exceptions other than CancelledError so you can log them
t.result()
except asyncio.CancelledError:
pass
except Exception as e:
logger.exception("Background task ended with error: %s", e)
# If anything still pending, optionally force them (or log)
if pending:
logger.warning("Some tasks did not finish within timeout: %s", pending)
C. Full safe shutdown and room deletion sequence
• 1) Stop/cancel tasks that may publish transcription
• 2) Disconnect the local room (so LiveKit client stops)
• 3) Delete the room via the API
• 4) Close API client
async def cleanup_and_delete_room(self):
api_client = api.LiveKitAPI() # your existing client
# 1) Cancel app-level background tasks (STT, capture, etc.)
await cancel_all_tasks(timeout=5.0)
# 2) Optionally, ensure agent/session shutdown (if using sessions)
# if self.session:
# await self.session.aclose() # or await self.session.shutdown(...) as appropriate
# 3) Disconnect local room if still connected
if self.room and getattr(self.room, "is_connected", False):
try:
await self.room.disconnect()
except Exception as e:
logger.warning("Error disconnecting room: %s", e)
finally:
self.room = None # avoid accidental reuse
# 4) Delete the room via API
try:
await api_client.room.delete_room(DeleteRoomRequest(room=self.room_name))
<http://logger.info|logger.info>("Room deleted – call ended")
except Exception as e:
logger.error("Failed to delete room: %s", e)
finally:
await api_client.aclose()
D. Extra guards to prevent races
• Add a boolean shutdown flag so any in-flight publish checks it before sending:
self.shutting_down = False
# when shutting down:
self.shutting_down = True
# wrapper used when publishing:
if self.shutting_down or not self.room or not getattr(self.room, "is_connected", False):
return
await self.room.local_participant.publish_transcription(transcription)
• This avoids tasks starting new publish attempts while you cancel them.
E. Logging and short timeouts
• Give tasks a short timeout (e.g., 5 seconds) to clean up; log any that remain. That helps identify stray tasks in subsequent runs.
If you’d like, I can:
• Convert this into a drop-in class method for your agent.
• Show how to modify a typical transcription loop to respect self.shutting_down and handle CancelledError.
Which would help you most — a direct patch for your agent’s transcription loop, or a small self-contained shutdown helper you can import?
Sources: SIP Lifecycle Management Agent | LiveKit Documentation | Text and transcriptions | LiveKit Documentation | Agents telephony integration | LiveKit Documentation | Job lifecycle | LiveKit Documentation | Agent speech and audio | LiveKit Documentationnarrow-chef-71655
02/24/2026, 11:22 AMtall-airline-16756
02/24/2026, 11:23 AMfrom livekit import api
from livekit.agents import get_job_context, function_tool
@function_tool()
async def hangup_call(self, ctx):
"""Tool for the agent to end the current call by deleting the room."""
job_ctx = get_job_context()
if job_ctx is None:
return "No active job context found."
# Optional: wait for session playout so final speech finishes
try:
await ctx.session.wait_for_playout()
except Exception:
pass
try:
await job_ctx.api.room.delete_room(
api.DeleteRoomRequest(room=job_ctx.room.name)
)
return "Call ended successfully."
except Exception as e:
return f"Failed to end call: {e}"
2) Make the tool available to the agent
• Register hangup_call in the agent’s tools list and document when to use it (e.g., user says "goodbye" or agent decides to end).
• Example instruction: "Use hangup_call() to terminate the call when the user or agent decides to finish."
3) Graceful behavior & safety
• Before hangup, have the agent confirm or speak a final message. Use ctx.session.wait_for_playout() so the message is delivered before deletion.
• Use a short confirmation step in the agent flow if accidental hangups are a concern.
4) Cancel background tasks and avoid race conditions
• Ensure any background tasks that may publish transcription or audio are cancelled before/when deleting the room (prevents "room closed"/"engine closed" errors).
• Use a tracked-task pattern:
◦ Add tasks with a helper:
self.active_tasks = set()
def start_task(coro):
t = asyncio.create_task(coro)
self.active_tasks.add(t)
t.add_done_callback(lambda _: self.active_tasks.discard(t))
return t
• On shutdown/hangup:
async def cancel_all_tasks(timeout=5.0):
for t in list(self.active_tasks):
if not t.done():
t.cancel()
if self.active_tasks:
await asyncio.wait(self.active_tasks, timeout=timeout)
• Call await cancel_all_tasks() before calling delete_room() inside the tool (or ensure the agent framework has already stopped I/O).
5) Full safe sequence inside your tool (recommended)
1. Set a shutting-down flag (so any in-flight publish checks it).
2. Cancel/await background tasks or ensure session shutdown.
3. Wait for final speech playout (optional).
4. Disconnect local client if needed (optional).
5. Call delete_room(...).
6. Close API client.
6) Notes about Twilio / SIP
• You do not need to call Twilio directly to end the SIP session — deleting the LiveKit room triggers a SIP BYE to Twilio.
• Ensure your Twilio trunk settings are appropriate for transfers/hangups if you rely on particular caller-id or transfer behaviors.
If you want, I can:
• Provide a drop-in hangup_call tool that includes task cancellation and a final playout wait.
• Show how to register it with your agent and a sample prompt/instruction so the LLM knows when to call it.
Which would you prefer — the complete drop-in tool with task cancellation, or just a minimal tool and guidance for integrating it into your existing agent?
Sources: Handling payments with PCI compliance | LiveKit | Agents telephony integration | LiveKit Documentation | Preserve caller ID during SIP transfers with Twilio | LiveKit