Skip to content

feat(agents): add warm transfer task#1458

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
seed-yet-adverts
Open

feat(agents): add warm transfer task#1458
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
seed-yet-adverts

Conversation

@rosetta-livekit-bot
Copy link
Copy Markdown
Contributor

Summary

  • Add beta WarmTransferTask workflow for SIP-based human handoffs.
  • Export warm transfer types through the beta workflow barrels.
  • Add SIP warm-transfer env vars to Turbo's declared env list and include a changeset.

Validation

  • pnpm --filter @livekit/agents build
  • pnpm --filter @livekit/agents build:types
  • pnpm --filter @livekit/agents lint (passes with existing warnings)
  • pnpm --filter @livekit/agents api:check (blocked by existing API Extractor export * as limitation in dist/index.d.ts)

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +309 to +329
private setResult(result: WarmTransferResult | Error): void {
if (this.done) {
return;
}

if (this._humanAgentSession) {
this._humanAgentSession.shutdown({ drain: false });
this._humanAgentSession = null;
}

if (this._holdAudioHandle) {
this._holdAudioHandle.stop();
this._holdAudioHandle = null;
}
void this._backgroundAudio.close().catch((error) => {
this._logger.warn({ error }, 'failed to close background audio');
});

this.setIoEnabled(true);
this.complete(result);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Human agent Room never disconnected, leaking a WebSocket connection on every transfer

In setResult, the _humanAgentSession is shut down but _humanAgentRoom is never disconnected. AgentSession.shutdown() calls closeImplInner which closes the RoomIO (agents/src/voice/agent_session.ts:1301) but RoomIO.close() (agents/src/voice/room_io/room_io.ts:555) does not call room.disconnect(). The only place in the codebase that calls room.disconnect() is agents/src/ipc/job_proc_lazy_main.ts:176 for the main job room. This means every completed WarmTransferTask leaves a connected Room object (with its underlying WebSocket to the LiveKit server) that is never cleaned up, accumulating leaked connections over successive transfers.

Suggested change
private setResult(result: WarmTransferResult | Error): void {
if (this.done) {
return;
}
if (this._humanAgentSession) {
this._humanAgentSession.shutdown({ drain: false });
this._humanAgentSession = null;
}
if (this._holdAudioHandle) {
this._holdAudioHandle.stop();
this._holdAudioHandle = null;
}
void this._backgroundAudio.close().catch((error) => {
this._logger.warn({ error }, 'failed to close background audio');
});
this.setIoEnabled(true);
this.complete(result);
}
private setResult(result: WarmTransferResult | Error): void {
if (this.done) {
return;
}
if (this._humanAgentSession) {
this._humanAgentSession.shutdown({ drain: false });
this._humanAgentSession = null;
}
if (this._humanAgentRoom) {
this._humanAgentRoom.off(RoomEvent.Disconnected, this.onHumanAgentRoomClose);
void this._humanAgentRoom.disconnect().catch((error) => {
this._logger.warn({ error }, 'failed to disconnect human agent room');
});
this._humanAgentRoom = null;
}
if (this._holdAudioHandle) {
this._holdAudioHandle.stop();
this._holdAudioHandle = null;
}
void this._backgroundAudio.close().catch((error) => {
this._logger.warn({ error }, 'failed to close background audio');
});
this.setIoEnabled(true);
this.complete(result);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +389 to +404
const sip = new SipClient(jobCtx.info.url);
await sip.createSipParticipant(
this._sipTrunkId ?? '',
this._sipCallTo,
humanAgentRoomName,
{
participantIdentity: this._humanAgentIdentity,
waitUntilAnswered: true,
fromNumber: this._sipNumber || undefined,
headers: this._sipHeaders,
},
this._sipConnection,
);

this._humanAgentRoom = room;
return humanAgentSession;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Room and AgentSession leaked when SIP call creation fails after room connect

In dialHumanAgent(), the Room is connected (line 357) and humanAgentSession is started (line 379-387) before createSipParticipant is called (line 390). If createSipParticipant throws, the method exits without assigning either to instance fields (this._humanAgentRoom at line 403 and the return at line 404 are never reached). The caller in onEnter catches the error and calls setResult, but setResult checks this._humanAgentSession (which is null) and this._humanAgentRoom (also null), so neither the session nor the room is cleaned up. The connected Room and running AgentSession become unreachable leaked resources.

Prompt for agents
In dialHumanAgent(), resources (the Room and AgentSession) are created before the SIP call and only assigned to instance fields after it succeeds. If createSipParticipant throws, these resources leak.

Approach: Assign this._humanAgentRoom = room and this._humanAgentSession = humanAgentSession immediately after they are created/started (before the createSipParticipant call), rather than at the end of the method. This way, if createSipParticipant fails and the error propagates to onEnter's catch block which calls setResult(), the cleanup code in setResult can properly shut down the session and disconnect the room.

Alternatively, wrap the SIP call in a try/catch inside dialHumanAgent that cleans up the room and session on failure before rethrowing.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: 8a74fe3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 31 packages
Name Type
@livekit/agents Major
@livekit/agents-plugin-anam Major
@livekit/agents-plugin-assemblyai Major
@livekit/agents-plugin-baseten Major
@livekit/agents-plugin-bey Major
@livekit/agents-plugin-cartesia Major
@livekit/agents-plugin-cerebras Major
@livekit/agents-plugin-deepgram Major
@livekit/agents-plugin-elevenlabs Major
@livekit/agents-plugin-fishaudio Major
@livekit/agents-plugin-google Major
@livekit/agents-plugin-hedra Major
@livekit/agents-plugin-hume Major
@livekit/agents-plugin-inworld Major
@livekit/agents-plugin-lemonslice Major
@livekit/agents-plugin-liveavatar Major
@livekit/agents-plugin-livekit Major
@livekit/agents-plugin-minimax Major
@livekit/agents-plugin-mistral Major
@livekit/agents-plugin-mistralai Major
@livekit/agents-plugin-neuphonic Major
@livekit/agents-plugin-openai Major
@livekit/agents-plugin-phonic Major
@livekit/agents-plugin-resemble Major
@livekit/agents-plugin-rime Major
@livekit/agents-plugin-runway Major
@livekit/agents-plugin-sarvam Major
@livekit/agents-plugin-silero Major
@livekit/agents-plugins-test Major
@livekit/agents-plugin-trugen Major
@livekit/agents-plugin-xai Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants