ripe-farmer-27598
03/05/2026, 12:28 PMtall-airline-16756
03/05/2026, 12:28 PMtall-airline-16756
03/05/2026, 12:28 PMawait ctx.wait_until_disconnected() so you can wait for post-call data storage before a room is deleted.
Short answer
• LiveKit does not provide a wait_until_disconnected() method or direct equivalent.
• You can reliably ensure data is saved before room deletion by using shutdown callbacks and waiting for them to finish with ctx.aclose() (and by controlling when you call room deletion).
How to do this (recommended)
1. Register a shutdown callback to perform your async data-save logic:
◦ Use ctx.add_shutdown_callback(your_async_save_fn) so your saving runs during shutdown.
1. End the session properly:
◦ Call session.shutdown(drain=True) if you need to finish speech playback, then call await ctx.aclose() — aclose() blocks until shutdown callbacks complete.
1. Delete the room only after aclose() completes:
◦ Either rely on delete_room_on_close=True if you’re certain it runs after your saves, or set delete_room_on_close=False and call the delete API yourself after ctx.aclose() finishes.
1. Increase shutdown timeout if your saves need more time:
◦ Adjust shutdown_process_timeout (for example, AgentServer(shutdown_process_timeout=30)) so callbacks have enough time to finish.
Minimal example (conceptual)
@server.rtc_session(agent_name="my-agent")
async def entrypoint(ctx: JobContext):
async def save_data_hook():
await save_transcript_to_db(ctx.room.name)
await save_call_metadata(ctx.room.name)
ctx.add_shutdown_callback(save_data_hook)
# ... run session ...
session.shutdown(drain=True)
await ctx.aclose() # blocks until save_data_hook finishes
# now safe to delete room
await api_client.room.delete_room(room=ctx.room.name)
Notes and gotchas
• Shutdown callbacks are executed after session.shutdown() and before the job terminates.
• There is a default timeout for shutdown hooks (10s). If your save can take longer, increase the server’s shutdown timeout.
• If you rely on automatic room deletion, make sure the deletion happens after your shutdown callbacks complete — otherwise use manual deletion after ctx.aclose().
If you’d like, tell me:
• Which language/runtime you’re using (Python example above), and
• Whether you’re relying on automatic room deletion or manual deletion.
I can then provide a tailored code example or checklist for your exact setup.
Sources: Job lifecycle | LiveKit Documentationripe-farmer-27598
03/05/2026, 12:32 PMtall-airline-16756
03/05/2026, 12:32 PMawait ctx.wait_until_disconnected() at the end. I’ll address how to make sure your storage finishes before the room is deleted and point out a few improvements you can make so deletion doesn’t happen prematurely.
Key points about your current approach
• wait_until_disconnected() is not provided as a blocking mechanism to guarantee post-call saves complete.
• Spawning a background task with asyncio.create_task(safe_cleanup()) inside the participant_disconnected handler can work, but it may race with process shutdown or room deletion if the runtime exits before the task or its I/O finishes.
• The robust pattern is to register a shutdown callback with ctx.add_shutdown_callback() and then wait for those callbacks to complete with await ctx.aclose() before deleting the room.
Recommended changes (concise)
1. Register your cleanup as a shutdown callback instead of launching an independent background task:
◦ This ensures the runtime waits for the callback during agent shutdown.
1. Use session.shutdown(drain=True) (if needed) and then await ctx.aclose() to block until shutdown callbacks finish.
2. Delete the room only after ctx.aclose() completes (or set delete_room_on_close=False and delete manually after aclose).
3. If your save can take longer than the default 10s, increase the server shutdown timeout.
Suggested structure (conceptual)
1. Add a shutdown callback that performs handle_disconnect_storage(session) (async).
2. When you want to end the session: call session.shutdown(drain=True) then await ctx.aclose().
3. After ctx.aclose() returns, call await ctx.api.room.delete_room(DeleteRoomRequest(room=ctx.room.name)).
Example outline adapted from your code (conceptual — fits your Python style)
# register once inside entrypoint
async def save_data_hook():
await handle_disconnect_storage(session)
ctx.add_shutdown_callback(save_data_hook)
# ... other setup and session usage ...
# when the session should end:
session.shutdown(drain=True)
await ctx.aclose() # waits for save_data_hook to finish
# now safe to delete room
try:
await ctx.api.room.delete_room(DeleteRoomRequest(room=ctx.room.name))
except Exception as e:
print(f"[FAILSAFE ERROR] Room deletion failed: {e}")
Notes and practical tips
• Keep your participant_disconnected handler for logging or starting graceful steps, but avoid relying solely on creating tasks there for critical saves.
• If you must trigger save from the event handler, call a function that sets state and let the shutdown callback perform the actual persistence, or ensure the created task is awaited by the shutdown sequence.
• Increase shutdown_process_timeout if your saves frequently take longer than the default 10 seconds.
If you want, I can:
• Convert your exact code into a ready-to-run snippet with ctx.add_shutdown_callback() integrated.
• Show how to keep automatic room deletion disabled and perform manual deletion after ctx.aclose().
Which would you prefer?ripe-farmer-27598
03/05/2026, 12:37 PMctx.add_shutdown_callback() integrated.tall-airline-16756
03/05/2026, 12:37 PMctx.add_shutdown_callback() so your post-call storage runs as part of the shutdown sequence and completes before the room is deleted.
Notes:
• This keeps your existing flow and handlers but registers the async save as a shutdown callback.
• We call session.shutdown(drain=True) and then await ctx.aclose() to wait for shutdown callbacks to finish.
• Manual room deletion happens after ctx.aclose() returns. If you prefer automatic deletion, set delete_room_on_close=True but ensure your saves complete first.
• If your save can take longer than the default 10s, increase shutdown_process_timeout when creating the server.
Ready-to-run example (Python):
import asyncio
from livekit.api import DeleteRoomRequest
from livekit.server import AgentServer
from livekit.types import UserAgent, RoomInputOptions, RoomOutputOptions
# replace with your actual prompt and prior_ctx variables
PROMPT = "Hello, I'm the agent."
prior_ctx = None
server = AgentServer(shutdown_process_timeout=30) # increase timeout if needed
@server.rtc_session(agent_name="my-agent")
async def entrypoint(ctx: "JobContext"):
session = ctx.session # your session object
# Shutdown callback that will run during agent shutdown.
async def save_data_hook():
try:
# perform your async storage using the session/context
await handle_disconnect_storage(session)
print("[SHUTDOWN] Data saved successfully.")
except Exception as e:
print(f"[SHUTDOWN ERROR] Failed to save data: {e}")
# Register shutdown callback so the runtime waits for it during shutdown
ctx.add_shutdown_callback(save_data_hook)
# Event handlers (logging + non-critical tasks)
@ctx.room.on("participant_disconnected")
def on_participant_disconnected(participant):
print(f"[EVENT] Participant {participant.identity} disconnected")
# Avoid launching critical storage here; let shutdown callback do it.
# If you want to kick off early work, set flags or enqueue work for the shutdown hook.
@session.on("agent_speech_committed")
def on_agent_speech(event):
print("Agent spoke")
@session.on("user_speech_committed")
def on_user_speech(event):
print("User spoke")
# Start the session (your original call)
await session.start(
room=ctx.room,
agent=UserAgent(PROMPT, chat_ctx=prior_ctx),
room_input_options=RoomInputOptions(audio_enabled=True, close_on_disconnect=False),
room_output_options=RoomOutputOptions(audio_enabled=True),
)
# Kick off initial reply
await session.generate_reply(
instructions="Start the conversation naturally by introducing yourself and explaining reason for calling.",
allow_interruptions=False,
)
# Wait for normal disconnection (optional).
# This does not guarantee post-call saves — shutdown callbacks do.
try:
await ctx.wait_until_disconnected()
except Exception:
# if wait_until_disconnected isn't present/usable in your runtime, ignore
pass
# Initiate a clean shutdown: finish speech playback then run shutdown callbacks
session.shutdown(drain=True)
await ctx.aclose() # blocks until save_data_hook completes
# Now safe to delete the room (manual deletion)
print("[FAILSAFE] Cleanup complete. Deleting room to exit job.")
try:
await ctx.api.room.delete_room(DeleteRoomRequest(room=ctx.room.name))
except Exception as e:
print(f"[FAILSAFE ERROR] Room deletion failed: {e}")
# Example stub for the storage function you referenced
async def handle_disconnect_storage(session):
# Insert your actual storage logic here.
# For demonstration, simulate async work:
await asyncio.sleep(1)
print("[STORE] handle_disconnect_storage completed.")
# Run server (depends on how you normally start the AgentServer)
if __name__ == "__main__":
server.run()
If you’d like, I can:
• Inline your existing handle_disconnect_storage implementation in the snippet,
• Show a smaller variant that disables automatic room deletion and waits on aclose() only,
• Or adapt the snippet to a different shutdown timeout value.
Which would you prefer?