happy-kite-78411
03/29/2026, 6:51 PMuserAwayTimeout: 30 and added following code to handle away event:
session.on(
voice.AgentSessionEventTypes.UserStateChanged,
async (event) => {
if (event.newState === "away") {
console.log(
`đź‘‹ User is away (timeout), ending session: ${sessionId}`,
);
try {
await ctx.room.disconnect();
await session.close();
session.shutdown();
} catch (err) {
err instanceof Error && console.log("❌", err.stack);
}
}
},
);
This is only logging "User is away...", but not ending the telephony call. What am I doing wrong?tall-airline-16756
03/29/2026, 6:52 PMtall-airline-16756
03/29/2026, 6:52 PMuserAwayTimeout to a number (seconds) instead of true. userAwayTimeout expects a numeric timeout value.
• Call only session.shutdown() to end the agent session and the telephony call. Do not call ctx.room.disconnect() or session.close() alongside shutdown() — those extra calls can interfere with proper session teardown.
Why this fixes it
• session.shutdown() is the method that gracefully ends the agent session, closes I/O, and disconnects the telephony participant cleanly. Calling other cleanup methods at the same time can conflict with the session state and prevent a clean hang-up.
Suggested code
1. Configure a numeric timeout (example: 10 seconds).
2. Use only session.shutdown() in the away handler.
// when creating the session
const session = new voice.AgentSession({
// ... other config
userAwayTimeout: 10 // seconds
});
session.on(
voice.AgentSessionEventTypes.UserStateChanged,
async (event) => {
if (event.newState === "away") {
console.log(`đź‘‹ User is away (timeout), ending session: ${sessionId}`);
try {
// Only shutdown the session — this handles hang-up and cleanup
await session.shutdown();
} catch (err) {
if (err instanceof Error) console.log("❌", err.stack);
}
}
},
);
If that doesn't end the call
• Tell me what version of the LiveKit agent library you're using and whether the telephony SIP participant is managed by LiveKit or an external SIP gateway.
• Also check for other code paths that might intercept or re-open the room/connection after shutdown (any other event handlers or retries).
I can walk through additional debugging steps once you share the version or confirm whether the SIP leg is external.
Sources: Job lifecycle | LiveKit Documentation, with interactive elementshappy-kite-78411
03/29/2026, 7:00 PMtall-airline-16756
03/29/2026, 7:01 PMsession.shutdown() is non‑awaitable (it does not return a Promise). I’ll explain what that means for your handler and how to ensure the telephony leg is cleanly ended.
What’s happening
• session.shutdown() in this version starts an asynchronous shutdown in the background; it does not return a Promise you can await.
• Because it’s non‑blocking, calling it without other handling can leave your code believing teardown is complete while the session is still finishing its work. Calling other cleanup methods at the same time (like ctx.room.disconnect() or session.close()) can conflict with shutdown and interfere with the SIP hangup.
What to do instead
1. Keep your userAwayTimeout as a numeric value (seconds). Example: userAwayTimeout: 30.
2. Call session.shutdown() from your away handler (do not also call ctx.room.disconnect() or session.close()).
3. If you need to run logic after shutdown has completed (for example logging, metric, or further resource cleanup), register a shutdown callback or listen for the session/ctx shutdown event rather than awaiting shutdown().
Example pattern
• Fire the non‑awaitable shutdown from the away handler:
session.on(
voice.AgentSessionEventTypes.UserStateChanged,
(event) => {
if (event.newState === "away") {
console.log(`đź‘‹ User is away (timeout), ending session: ${sessionId}`);
try {
session.shutdown({ drain: true }); // non-awaitable in v1.0.40
} catch (err) {
if (err instanceof Error) console.log("❌", err.stack);
}
}
},
);
• Register a shutdown callback to run code once shutdown completes (use the API your agent context provides, e.g., ctx.add_shutdown_callback() if available). That ensures post-shutdown actions occur after the telephony leg is actually closed.
If you don’t have a shutdown callback mechanism available in your environment, you can:
• Listen for a session/ctx "closed" or "session ended" event if the SDK emits one, or
• Add a small delay before doing follow-up work (less ideal).
Next steps I can help with
• Show the exact shutdown-callback API usage for your environment (Node browser vs server) if you share whether you’re running the agent in Node or in-browser and whether ctx exposes add_shutdown_callback.
• Inspect other event handlers that might re-open the room or re-connect the SIP leg if the call still doesn’t hang up after calling session.shutdown().
Tell me which environment you’re running in (Node or browser) and whether ctx.add_shutdown_callback() is available — I’ll give the concrete code for registering the post‑shutdown action.
Sources: Job lifecycle | LiveKit Documentation, with interactive elementshappy-kite-78411
03/29/2026, 7:19 PMtall-airline-16756
03/29/2026, 7:19 PMUserStateChanged => away event.
• You called session.shutdown() (v1.0.40 — non‑awaitable) but the SIP/telephony call still stays connected.
Why this happens
• The away event is only a notification — it does not by itself hang up other participants.
• In LiveKit, session.shutdown() ends the agent session but does not guarantee the SIP leg (or the entire room) is disconnected. To reliably terminate the call you must explicitly remove the SIP participant or delete the room using the server API.
What I recommend (three options, in order of reliability)
• Option A — Delete the room (recommended for telephony): use the LiveKit server SDK to delete the room. This will disconnect all participants (including the SIP caller).
• Option B — Remove only the SIP participant: use the server SDK to list participants and remove the SIP participant(s) if you want other participants to remain.
• Option C — Ensure shutdown completes and use shutdown callbacks: call session.shutdown({ drain: true }) to initiate shutdown and register a shutdown callback (or listen for a session/ctx closed event) for any follow-up. This alone may not disconnect SIP participants.
Concrete next steps I can guide you through
1) Tell me where the agent runs
• Are you running the agent in Node.js on a backend with access to LIVEKIT_API_KEY and LIVEKIT_API_SECRET, or is this inside a browser-only environment?
◦ If you’re in a browser-only environment, you cannot call the server APIs — you’ll need a backend to delete rooms or remove participants.
â—¦ If you have a backend, proceed to step 2.
2) If you have a backend (Node) — delete the room (recommended)
• I can give the exact Node code, but you’ll use the server SDK (server-sdk-js) with your LiveKit URL, API key, and secret and call deleteRoom(roomName) when you get the away event.
• Example flow I can provide once you confirm environment:
a. On UserStateChanged => away event, call your backend endpoint (or directly call server SDK if available).
b. Backend calls roomService.deleteRoom(ctx.room.name) (or equivalent) to end the call.
3) If you prefer removing just the SIP participant
• I can provide code to:
a. list participants via the server SDK,
b. identify ones with kind === 'PARTICIPANT_KIND_SIP',
c. call removeParticipant({ room, identity }) for each SIP participant.
4) If you want to continue using shutdown()
• Ensure you call session.shutdown({ drain: true }) to let pending speech finish.
• Register a shutdown callback (for example ctx.add_shutdown_callback() or listen for the SDK’s session closed event) to run logic after shutdown completes, rather than relying on await (shutdown is non‑awaitable in v1.0.40).
What I need from you to give exact code
• Confirm whether your agent runs in Node.js or in a browser.
• Confirm you have server credentials (LIVEKIT_API_KEY/SECRET) available on the backend.
• Share a short snippet of how you create the session / the room name (or tell me how you obtain ctx.room.name) so I can show the precise call to the server SDK.
If you share those details I’ll provide the exact Node.js snippet to delete the room or remove the SIP participant and an updated away handler you can drop into your code.
Sources: How to detect and handle silence during calls | LiveKit | Job lifecycle | LiveKit Documentation | Python Agents: Handling exceptions in your entrypoint | LiveKit | server-sdk-js | livekit/livekit_composite | DeepWiki, with interactive elementshappy-kite-78411
03/29/2026, 7:56 PMconst rsvc = new RoomServiceClient(
process.env.LIVEKIT_URL!,
process.env.LIVEKIT_API_KEY,
process.env.LIVEKIT_API_SECRET,
);
session.on(
voice.AgentSessionEventTypes.UserStateChanged,
async (event) => {
if (event.newState === "away") {
console.log(
`đź‘‹ User is away (timeout), ending session: ${sessionId}`,
);
try {
session.shutdown({ drain: true });
} catch (err) {
err instanceof Error && console.log("❌", err.stack);
}
}
},
);
ctx.room.on("disconnected", async () => {
console.log("❌ Room Disconnected Event triggered");
});
ctx.addShutdownCallback(async () => {
console.log("❌ Shutdown Callback triggered");
await callLogger.updateSessionStatus(sessionId, CallStatus.COMPLETED);
callLogger.clearSequenceCounter(sessionId);
await session.close();
if (ctx.room.name) {
await rsvc.deleteRoom(ctx.room.name);
}
});tall-airline-16756
03/29/2026, 7:56 PMsession.shutdown({ drain: true }) on the away event.
• You register ctx.addShutdownCallback(...) which deletes the room and does session.close().
• Despite that, the SIP call remains connected.
Key facts to know
• session.shutdown() starts an async shutdown but does not guarantee the room or SIP participant is immediately removed.
• ctx.addShutdownCallback() runs only when the agent context initiates a full shutdown flow (not automatically after session.shutdown() unless that triggers the context shutdown).
• To guarantee the SIP leg is disconnected you must explicitly remove the SIP participant or delete the room using the server SDK (RoomServiceClient) as part of the away handler (or ensure ctx.shutdown() is called and completes).
Immediate checks (please confirm)
1. Are you running this code in a Node.js backend with access to LIVEKIT_API_KEY and LIVEKIT_API_SECRET? The server SDK calls require backend credentials.
2. Does ctx.addShutdownCallback() actually run in your logs at any point? (You added a console line — do you see it after an away event?)
3. Does the rsvc.deleteRoom(ctx.room.name) call ever run, and if so does it throw or return an error? Add logging around that call.
4. Is the room name (ctx.room.name) the correct name used by the SIP participant? Confirm it’s not undefined or a different value.
Concrete adjustments to try now
• Call the server SDK to delete the room (or remove the SIP participant) directly inside the away handler so the hangup is immediate and not dependent on shutdown callbacks.
Example: delete the room immediately on away
session.on(
voice.AgentSessionEventTypes.UserStateChanged,
async (event) => {
if (event.newState === "away") {
console.log(`đź‘‹ User is away (timeout), ending session: ${sessionId}`);
try {
// initiate agent shutdown (optional)
session.shutdown({ drain: true });
// immediately delete the room via server SDK to force disconnect
if (ctx.room && ctx.room.name) {
console.log("Deleting room:", ctx.room.name);
await rsvc.deleteRoom(ctx.room.name);
console.log("Room deleted");
} else {
console.log("ctx.room.name is missing");
}
} catch (err) {
console.error("Failed to end call on away:", err);
}
}
},
);
Or, remove just the SIP participant if you want the room to remain:
// pseudo flow: list participants, find SIP ones, remove them
const list = await rsvc.listParticipants({ room: ctx.room.name });
for (const p of list.participants) {
if (p.kind === 'PARTICIPANT_KIND_SIP' || p.identity.includes('sip')) {
await rsvc.removeParticipant({ room: ctx.room.name, identity: p.identity });
console.log("Removed SIP participant:", p.identity);
}
}
Logging to add (so we can see what’s happening)
• Log before/after rsvc.deleteRoom() and catch+log any error (including status code/message).
• Log ctx.room.name and full participant list returned by listParticipants.
• Log whether ctx.addShutdownCallback runs and whether session.close() inside it errors.
Possible reasons deleteRoom/removeParticipant might silently fail
• API key/secret not present or wrong permissions.
• Using server SDK from a process that can’t reach the LiveKit control plane URL.
• Wrong room name or typo.
• Network error or unhandled exception (check logs).
If you try calling deleteRoom directly in the away handler and it still doesn’t hang up, please paste:
• The logs around the away handler (the deleteRoom attempt and any error).
• Whether deleteRoom resolved successfully or threw an error.
• The output of listing participants for that room (so I can confirm the SIP participant identity and kind).
Once you share that, I’ll give the exact code to remove only the SIP participant or to force-delete the room and verify the hangup. If you prefer, I can also provide a short troubleshooting checklist for API permissions and environment connectivity.
, with interactive elements