fix(workflows): cancel warm transfer when caller hangs up before merge#2008
fix(workflows): cancel warm transfer when caller hangs up before merge#2008toubatbrian wants to merge 5 commits into
Conversation
The caller-room ParticipantDisconnected listener was only attached in connect_to_caller, i.e. after the merge. If the caller hung up while the human agent's phone was still ringing (or during the briefing), nothing cancelled the transfer: the phone kept ringing and, on confirmation, the human agent was moved into an empty room. WarmTransferTask now watches the caller room from onEnter and, on caller hangup before the merge, aborts the pending dial, tears down the human agent room/session (ending the SIP call), and completes the task with a ToolError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE
🦋 Changeset detectedLatest commit: 3b32198 The changes in this PR will be included in the next version bump. This PR includes changesets to release 36 packages
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 |
|
|
If the caller hung up while backgroundAudio.start() was awaiting the track publish, setResult had already closed the player, but onEnter resumed and called play() before the done guard — orphaning a play handle nothing would ever stop. Check task.done between start() and play() instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE
davidzhao
left a comment
There was a problem hiding this comment.
should we let the escalation point (remaining human) know that the caller has left? so the hang up is smoother
… the call Per review feedback: when the caller hangs up after the human agent has answered, interrupt the briefing and speak a short notice (configurable via callerHangupMessage, null disables) before shutting the session down, instead of dropping the call abruptly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE
Replace the fixed say() message with generateReply() so the agent phrases the notice in context, and rename the option to callerHangupInstruction (built-in default instruction when not provided). toolChoice is forced to 'none' so the reply can't invoke connect_to_caller on an already-cancelled transfer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE
Guard the onEnter dial-cleanup path against closing a session that the caller-hangup notification flow has taken over, so the announcement can't be torn down mid-playout by a concurrent cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE
|
Good idea — implemented. If the human agent has already answered when the caller hangs up, the transfer agent interrupts the briefing and generates a reply telling them the caller left and the call is ending (via |
Description
If the caller hangs up at the start of a warm transfer — before the transfer destination (human agent) picks up — the transfer is not cancelled. The human agent's phone keeps ringing, they answer, get the AI briefing, and when they confirm with
connect_to_callerthey are moved into a room the caller already left.Root cause:
WarmTransferTaskonly attached its caller-roomParticipantDisconnectedlistener inside theconnect_to_callertool, i.e. after the merge. Nothing watched the caller room during the ringing/briefing phase, so a caller hangup during that window went unnoticed.This PR makes
WarmTransferTaskwatch the caller room fromonEnter. When the caller disconnects before the merge, the transfer is cancelled end to end: the pending SIP dial is aborted, the human agent room/session is torn down (which ends the outbound call viadeleteRoomOnClose), caller-session IO is restored, and the task completes with aToolError('caller hung up before the transfer completed'). If the human agent had already answered, the agent tells them the caller left before their call is ended, so the hangup isn't abrupt.Changes Made
ParticipantDisconnectedlistener on the caller room inonEnter(filtered toDEFAULT_PARTICIPANT_KINDS, same as the existing post-merge listener); it resolves a newcallerHangupFutand completes the task viasetResult.callerHangupFutas a third branch of the dial race inonEnter, so a hangup while the human agent's phone is still ringing aborts the in-flight dial through the existingAbortController/cleanup path.generateReplywith instructions that the caller hung up and the call is ending (newcallerHangupInstructionoption, built-in default when not provided;toolChoice: 'none'so the reply can't invoke tools; playout capped at 10s), then shut the session down.remoteParticipantschecked), caller hanging up while hold audio is starting, and the dial completing in the same tick as the hangup (session discarded via the existingfinallycleanup instead of being kept).connect_to_callerwith atask.donecheck so a hangup that lands while the human agent is confirming can't merge them into an empty room.setResultso every completion path (merge, decline, voicemail, failure, hangup) ends the watch;connect_to_callerstill attaches the post-merge cleanup listener as before.@livekit/agentspatch).Pre-Review Checklist
pnpm build:agents, ESLint, and Prettier pass locallyTesting
restaurant_agent.tsandrealtime_agent.tswork properly (for major changes) — not a major change; onlyworkflows/warm_transfer.tsis touchedAdditional Notes
Manual verification scenarios for the reviewer (require a SIP trunk, e.g. via
examples/src/warm_transfer.ts):ToolError.connect_to_callermerge all behave as before.🤖 Generated with Claude Code
https://claude.ai/code/session_012wowvUn9QorK87g8ytL5bE