Environment: @livekit/agents 1.4.7 (Node), @livekit/rtc-node 0.13.29, livekit-server-sdk 2.15.3. Inbound PSTN via SIP (Twilio trunk). Using beta.WarmTransferTask.
What we’re doing (warm transfer):
-
Caller (SIP) is in room R with our agent.
-
WarmTransferTask dials a human rep into a side room, briefs them, then connect_to_caller → moveParticipant merges the rep (human-agent-sip) into room R.
-
The AI agent then closes its AgentSession (session.shutdown) so it stops listening/talking — otherwise, after the task completes, control hands back to our default agent and it interjects into the human-to-human conversation.
Now room R holds two SIP legs (caller + rep) talking directly, AI gone.
The problem: LiveKit doesn’t couple the two SIP legs. When one party hangs up, the other is left alone in R. The room is still non-empty (1 participant), so departureTimeout/empty-reap never fires — the surviving PSTN leg sits in silence and keeps billing until that person also hangs up. We want: when either party leaves a post-transfer room, drop the other leg / end the room.
Why the SDK’s built-in teardown doesn’t fire for us: WarmTransferTask registers onCallerParticipantDisconnected, which on a participant leaving calls:
const rooms = new RoomServiceClient(getJobContext().info.url);
void rooms.deleteRoom(this._callerRoom.name)…
But because we closed the agent session at the bridge (step 3), by the time a party hangs up the job context is gone. The handler fires and throws:
Unhandled promise rejection: no job context found, are you running this code inside a job entrypoint?
at getJobContext (@livekit/agents/src/job.ts:49)
at Room.onCallerParticipantDisconnected (@livekit/agents/src/beta/workflows/warm_transfer.ts:341)
So deleteRoom never runs → caller leg orphaned. (Confirmed on a live call: rep hung up, caller leg stayed up.)
The core tension: we must close/silence the agent at the bridge (or it talks over the humans), but the SDK’s room teardown depends on the job context that closing tears down.
Our question: What’s the recommended way to tear down a post-warm-transfer room (and drop the surviving SIP leg) when one of two SIP participants leaves, after the agent has stepped out? Specifically:
-
Is the intended design to keep the AgentSession alive (so getJobContext() works and the built-in onCallerParticipantDisconnected fires)? If so, what’s the supported way to make the agent silent/non-interjecting post-merge (mute input/output, no LLM turns) while keeping the job alive — and does an idle-but-open session keep billing?
-
Is onCallerParticipantDisconnected calling getJobContext() in an event callback that can outlive the entrypoint a known issue in WarmTransferTask? Should it capture the RoomServiceClient/URL at construction instead?
-
Is the server-side participant_left webhook (we list participants, and deleteRoom when a lone SIP leg remains) the recommended pattern here, rather than relying on the in-process handler?
-
Is there a native room/SIP option to couple the legs so hanging up one ends the other — e.g., end-room-when-participant-count-drops-below-N, a max-duration, or SIP bridge semantics — so we don’t need an external reaper at all?