abundant-father-33863
03/12/2026, 9:17 AMtall-airline-16756
03/12/2026, 9:17 AMtall-airline-16756
03/12/2026, 9:18 AMctx.shutdown and deleting the room the dialler server still isn't getting the disconnect signal from LiveKit. Let’s walk through likely causes and some targeted checks to pinpoint the problem.
What this usually means
• Deleting the room or shutting down the server should terminate SIP participants, but if the SIP participant was removed only from LiveKit’s internal state (or removed with different logic) the downstream dialler might not get the expected SIP BYE/Disconnect.
• In your codebase there are two relevant ways SIP callers are terminated:
◦ A cleanup path that looks for participants whose identity starts with sip_ and calls remove_participant.
◦ Deleting the entire room with delete_room, which terminates all participants including SIP callers.
Quick checklist — please try these and report results
1. Confirm how you are ending the call in practice:
◦ Are you calling remove_participant for the SIP identity (starts with sip_) or only calling ctx.shutdown + delete_room?
◦ If you rely solely on a participant-removal path inside an exception handler, it may not run consistently.
1. Test deleting the room explicitly:
◦ Call delete_room and verify whether the dialler receives the disconnect. If delete_room does cause the dialler to get the disconnect, that indicates the participant-specific cleanup path is not performing the SIP disconnect.
1. Check logs and timing:
◦ Inspect LiveKit server logs around the time of shutdown/deletion for messages about removing sip_ participants or room deletion.
◦ Confirm the order: if ctx.shutdown happens before remove_participant or before delete_room, the cleanup code might exit early and not send the SIP disconnect.
1. Verify identity naming and matching:
◦ Ensure the SIP participant identity actually begins with sip_ when cleanup runs. If the identity differs, the conditional that calls remove_participant won’t run.
1. Confirm dialler-side behavior:
◦ Verify the dialler server is listening for the expected SIP BYE/Disconnect and hasn’t timed out or dropped the session before LiveKit issues the disconnect.
Next diagnostic steps I can help with
• If you can share which method you currently use to end calls (participant removal vs delete_room), any relevant code snippet for the cleanup logic, and logs around the shutdown, I can point to the exact cause and suggest a fix.
• If you want a quick, robust approach: use delete_room to ensure all SIP participants are disconnected. If you need targeted removal, ensure remove_participant is called for identities starting with sip_ and that the cleanup code reliably runs before the process exits.
Which of these checks can you run now, and can you paste the cleanup snippet or a relevant log excerpt? I’ll walk through the next steps with you.abundant-father-33863
03/12/2026, 11:52 AMtall-airline-16756
03/12/2026, 11:52 AMctx.shutdown, and inside that shutdown callback you send a deleteRoom request. Let’s narrow down why the dialler isn’t seeing the disconnect and what to check/change.
What could be happening
• deleteRoom should disconnect all participants, including SIP callers, but if the shutdown sequence or process exit happens before LiveKit finishes sending the SIP disconnect (BYE), the dialler may never receive it.
• If the SIP participant was already removed earlier or its identity doesn’t match the cleanup logic (e.g., not starting with sip_), the code path that triggers the SIP teardown may not run as you expect.
• Race conditions: shutting down the context/process too quickly after requesting deleteRoom can interrupt the outgoing SIP signaling.
Steps to troubleshoot and fix
1. Confirm ordering and wait for completion
◦ After calling deleteRoom, ensure you wait for the delete request to complete (success response) before the process exits or other shutdown steps continue. Make the delete call synchronous or await its completion.
◦ Add a short delay only for debugging (e.g., a few hundred ms) after a successful deleteRoom to see if the dialler receives the BYE — if it does, that indicates a timing/race issue.
1. Check logs for evidence of SIP BYE
◦ Inspect LiveKit server logs for an attempt to disconnect the SIP participant when deleteRoom is processed.
◦ Look for errors or warnings that indicate the server failed to signal the dialler.
1. Verify SIP participant identity and presence
◦ Confirm the SIP participant’s identity actually starts with sip_ at the time of deletion if your cleanup relies on that pattern.
◦ If identities are different or change before shutdown, the SIP-specific removal path might not be executed.
1. Try explicit participant removal as a test
◦ As a diagnostic, call remove_participant for the SIP identity (the sip_ participant) before deleteRoom and see whether the dialler receives the disconnect. If it does, that confirms deleteRoom wasn’t finishing or wasn’t reaching the SIP teardown path before process shutdown.
1. Ensure dialler readiness
◦ Confirm the dialler is ready to receive and process the BYE (not blocked, crashed, or disconnected) at the time LiveKit sends it.
What I can help with next
• If you can paste the shutdown callback code (the snippet that calls deleteRoom), I’ll review it for ordering/race issues and recommend where to await or add checks.
• If you can share the LiveKit logs around the delete call, I’ll look for evidence of the SIP disconnect attempt and any errors.
If you want a quick robust approach: explicitly remove the SIP participant (remove_participant for the sip_ identity) and wait for that operation to complete before finishing the shutdown. That guarantees the SIP disconnect path runs even if room deletion or shutdown sequencing is flaky.abundant-father-33863
03/12/2026, 11:55 AMtall-airline-16756
03/12/2026, 11:55 AMdelete_room from your async hangup_call() shutdown callback, and the code snippet shows you await the delete call. Let’s focus on the most likely causes and a few precise checks you can run to confirm and fix the missing SIP disconnect.
Quick summary of what I’m responding to
• Your shutdown callback hangup_call() awaits ctx.api.room.delete_room(...).
• The dialler still doesn’t receive the SIP disconnect (BYE) after this runs.
Likely causes
• Race/timing: the process may continue shutting down (or the runtime may exit) before LiveKit finishes sending the SIP BYE, even though the delete request returned.
• SIP teardown path may be skipped if the SIP participant was already removed earlier or if identity matching differs.
• Dialler-side: the dialler might be unable to receive/process the BYE at that moment.
Concrete checks and fixes to try
1. Confirm the delete actually completed successfully
◦ After await ctx.api.room.delete_room(...), log the response or catch and log exceptions to ensure it returned success.
◦ Example: log response.status or the returned object so you know the request finished.
1. Add an explicit small delay after successful delete (diagnostic)
◦ Temporarily add await asyncio.sleep(0.5) (500ms) after delete to see if the dialler then receives the BYE. If it does, that shows a race between delete and process shutdown.
1. Ensure process doesn’t exit immediately
◦ Verify the shutdown sequence doesn’t exit the event loop or kill the process immediately after the callback finishes. Make sure the event loop runs long enough for LiveKit to complete any outbound SIP signaling.
1. Try explicit participant removal before deleting the room (diagnostic)
◦ Call await ctx.api.room.remove_participant(...) for the SIP identity (e.g., sip_...) and await its completion, then call delete_room. If the dialler receives disconnect on remove_participant, use that as a reliable teardown step.
1. Log and verify identity matching
◦ At shutdown, enumerate ctx.room.participants (or the API equivalent) and log identities to ensure the SIP participant identity still starts with sip_ and is present when you delete the room.
1. Inspect LiveKit server logs
◦ Look for entries that show the SIP BYE being sent (or errors during deletion). If you see the BYE attempt, the problem is likely on the dialler side. If you see errors, paste them here and I’ll help interpret.
What I can help with next
• Paste your shutdown registration code (how closeCall() registers the callback) so I can confirm the callback ordering and lifecycle.
• If possible, share the delete_room response or relevant server logs around the delete time (timestamped lines showing deletion and any SIP activity).
• If you want a quick robust change: explicitly call remove_participant for the SIP identity and await it; then call delete_room and await that as a final cleanup. This ensures the SIP-specific teardown runs reliably.
If you want, paste the shutdown registration and any logs you have and I’ll review them with you.abundant-father-33863
03/12/2026, 12:07 PMtall-airline-16756
03/12/2026, 12:07 PMctx can become None when you call ctx.shutdown, and how that might affect your shutdown callback.
Short answer
• Yes — ctx can become None after shutdown if the job/context lifecycle clears it as part of the shutdown sequence. If your shutdown callback reads ctx from a global getter (like get_job_context()), that getter can return None once the context is torn down.
Why that matters
• If hangup_call() calls get_job_context() inside the callback and the global/context store has already been cleared by the time the callback runs, you’ll get None and won’t be able to call delete_room or remove_participant.
• That would explain why the dialler never sees the SIP BYE: the code that sends the disconnect never runs because ctx is missing.
What to check now
1. Log the context value right before you call shutdown and at the start of the shutdown callback:
◦ At shutdown entry: log the value of ctx you pass to ctx.shutdown (if you pass it).
◦ Inside `hangup_call()`: immediately log the result of get_job_context() to confirm whether it’s None.
1. Confirm how the shutdown callback is registered and invoked:
◦ If you register a callback that runs after the context is cleared, it will not have access to ctx. Verify the callback runs while the context is still available.
Safe patterns to ensure you have the data you need
• Capture required data before calling `ctx.shutdown`:
◦ Read and stash the room name and SIP participant identity into local variables (or pass them into the shutdown callback) before invoking shutdown, so the callback doesn’t need to call get_job_context().
• Or: perform the delete/remove actions immediately before initiating the shutdown sequence:
◦ Call remove_participant or delete_room while ctx is guaranteed valid, then call ctx.shutdown afterward.
• Make shutdown callback defensive:
◦ If you must use get_job_context() inside the callback, handle None by falling back to previously captured values or skipping non-critical cleanup.
Example pattern (conceptual)
1. Capture room and SIP identity:
◦ room_name = ctx.room.name
◦ sip_id = the SIP participant identity
1. Call delete/remove while ctx is still valid:
◦ await ctx.api.room.remove_participant(room=room_name, participant=sip_id)
◦ await ctx.api.room.delete_room(room=room_name)
1. Then start ctx.shutdown.
Next steps I can help with
• Paste how you register closeCall() as the shutdown callback and the code that calls ctx.shutdown. I’ll point out where the context may be cleared and suggest the exact minimal code changes to ensure the SIP BYE is sent before ctx becomes None.
• If you can share the logs showing the DISCONNECTION REQUEST RECD and any subsequent lines, I’ll check whether the callback ran and whether the delete call returned.abundant-father-33863
03/12/2026, 12:11 PMasync def close_call(_reason: str) -> None:
try:
inactivity_handler.stop()
call_endtime = int(datetime.now(timezone).timestamp())
<http://logger.info|logger.info>(f"######### C A L L - E N D E D ######### [{_reason}]")
# session.input.set_audio_enabled(False)
current_speech = session.current_speech
if current_speech:
await current_speech.wait_for_playout()
if ctx and ctx.api:
try:
# Check if room exists and api is available
await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))
<http://logger.info|logger.info>("...... room deleted and disconnected ......")
except Exception as api_err:
logger.warning(f"Could not delete room via API: {api_err}")
else:
<http://logger.info|logger.info>("ctx.api is None, skipping room deletion.")
<http://logger.info|logger.info>("...... d i s c o n n e c t e d ......")
except Exception as e:
<http://logger.info|logger.info>(f"Error in function close_call :: {e}")tall-airline-16756
03/12/2026, 12:11 PMclose_call function. I’m answering your question about why the dialler might not get the SIP disconnect and whether ctx can be None during shutdown.
Short summary of what I see
• Your close_call awaits ctx.api.room.delete_room(...) only if ctx and ctx.api are truthy.
• If ctx becomes None (or ctx.api is cleared) before close_call runs, your code will skip deletion and no SIP BYE will be sent.
• Even when you do call delete_room, a race can still occur if the process shuts down before LiveKit finishes outbound SIP signaling.
Concrete recommendations (small, practical changes)
1) Capture required values before shutdown
• Before calling ctx.shutdown (or registering the shutdown), read and stash the room name and SIP identity so the cleanup callback does not rely on get_job_context() or a global ctx that might be cleared.
• Example conceptual flow:
◦ room_name = ctx.room.name
◦ sip_identity = ... (if you need it)
◦ register/trigger shutdown passing room_name and sip_identity into the callback
2) Ensure delete/remove runs while ctx is valid
• Prefer performing remove_participant (for the SIP identity) and/or delete_room immediately while ctx is still available rather than relying on a later callback that may run after context cleanup.
• If you must use a shutdown callback, pass the captured room name into it rather than calling get_job_context() inside the callback.
3) Make delete synchronous and wait for completion
• Log the delete response and any exceptions:
◦ After the await, log success (or full exception). This confirms the API call returned.
• Add a short await for debugging to detect race issues:
◦ e.g., await asyncio.sleep(0.5) after a successful delete; if the dialler then receives the BYE, it confirms a timing/race issue.
4) Try explicit participant removal as a robust teardown
• Call await ctx.api.room.remove_participant(...) for the SIP participant (identity starting with sip_) then await it, then call delete_room. Removing the participant explicitly forces the SIP disconnect path and is a reliable way to send BYE.
5) Improve logging to verify behavior
• At the top of close_call, log the value of ctx, ctx.api, and ctx.room.name (or the captured equivalents).
• Log the exact response from delete_room or any exception message so we can see whether the delete completed.
Minimal example pattern (conceptual)
1. Before shutdown:
◦ room_name = ctx.room.name
◦ sip_id = ... (if relevant)
1. In shutdown callback:
◦ try:
await api.room.remove_participant(api.RemoveParticipantRequest(room=room_name, participant=sip_id)) await api.room.delete_room(api.DeleteRoomRequest(room=room_name)) log success except Exception as e: log exception
• await asyncio.sleep(0.5) # temporary diagnostic
Next steps I can help with
• If you paste how you register/invoke close_call (the shutdown registration and where you call ctx.shutdown), I’ll point out exactly where ctx may be cleared and give the minimal code edits to pass the room name into the callback.
• If you can share the logs around the DISCONNECTION REQUEST RECD and any subsequent lines (especially any "ctx.api is None" or delete exceptions), I’ll interpret them and recommend the exact fix.
Which would you like to do first — share the shutdown registration or add the pre-shutdown capture and I’ll show the exact small code change?