Environment
@livekit/agents 1.5.5 (also present on main at time of filing)
@livekit/rtc-node 0.13.31
- Node v22, Debian 12, self-hosted LiveKit server + SIP (telephony pipeline)
Bug
ipc/job_proc_lazy_main.js (the job process teardown, agents/src/ipc/job_proc_lazy_main.ts in source) runs:
try {
await ctx._onSessionEnd();
} catch (error) {
logger.error({ error }, "error in ctx._onSessionEnd");
}
await room.disconnect(); // <-- unguarded
logger.debug("disconnected from room");
const shutdownTasks = [];
for (const callback of ctx.shutdownCallbacks) { ... }
...
safeSend({ case: "done", value: void 0 });
joinFuture.resolve();
If the room was already disconnected by the time this runs (in our case: the agent calls ctx.shutdown() on call end, and the room disconnect races the job teardown), the room's FFI handle has been released and rtc-node throws:
Error: failed to handle request: invalid request: handle not found
(FfiRequest { message: Some(Disconnect(DisconnectRequest { room_handle: 2, ... })) })
at Room.disconnect (@livekit/rtc-node/dist/room.js:588)
at ipc/job_proc_lazy_main.js:157
Because the call is not guarded (unlike its neighbours session.close() and ctx._onSessionEnd(), which both have try/catch), the throw:
- surfaces as an unhandled promise rejection in the job process, and
- skips everything after it: the registered
shutdownCallbacks (where applications typically flush final transcripts/analytics) and the done IPC to the parent process, so the parent only recovers via its close-timeout path.
Observed in production on a normal, successfully-completed telephony call — the error fired ~call end and the loss of shutdown callbacks is silent.
Suggested fix
Wrap the disconnect like its neighbouring teardown steps:
try {
await room.disconnect();
logger.debug("disconnected from room");
} catch (error) {
logger.warn({ error }, "error disconnecting from room (already disconnected?)");
}
An "already disconnected" state check on room would also work, but try/catch matches the surrounding code's style and covers any FFI-level race.
Related
Same teardown-race family as #1871 (AgentSession.closeImplInner reading this.activity across awaits) — both are cleanup paths that assume resources are still alive when a concurrent shutdown may have released them. We are patching both locally in the meantime.
Environment
@livekit/agents1.5.5 (also present onmainat time of filing)@livekit/rtc-node0.13.31Bug
ipc/job_proc_lazy_main.js(the job process teardown,agents/src/ipc/job_proc_lazy_main.tsin source) runs:If the room was already disconnected by the time this runs (in our case: the agent calls
ctx.shutdown()on call end, and the room disconnect races the job teardown), the room's FFI handle has been released andrtc-nodethrows:Because the call is not guarded (unlike its neighbours
session.close()andctx._onSessionEnd(), which both have try/catch), the throw:shutdownCallbacks(where applications typically flush final transcripts/analytics) and thedoneIPC to the parent process, so the parent only recovers via its close-timeout path.Observed in production on a normal, successfully-completed telephony call — the error fired ~call end and the loss of shutdown callbacks is silent.
Suggested fix
Wrap the disconnect like its neighbouring teardown steps:
An "already disconnected" state check on
roomwould also work, but try/catch matches the surrounding code's style and covers any FFI-level race.Related
Same teardown-race family as #1871 (
AgentSession.closeImplInnerreadingthis.activityacross awaits) — both are cleanup paths that assume resources are still alive when a concurrent shutdown may have released them. We are patching both locally in the meantime.