fix(workflows): forward Twilio CallToken during warm transfers - #2510
piyush-gambhir wants to merge 5 commits into
Conversation
🦋 Changeset detectedLatest commit: c54ace2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 39 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 |
There was a problem hiding this comment.
Devin Review found 4 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| const callSid = await createTwilioCall(auth, { | ||
| to: phoneNumber, | ||
| from: twilioFromNumber, | ||
| twiml, | ||
| callToken: twilioCallToken, | ||
| }); |
There was a problem hiding this comment.
🟡 Cancelled transfers still place calls
When cancellation occurs during connectTwilioCall, createTwilioCall still runs without checking signal. The human agent can receive a call after the transfer ended.
Learn more
The outer dial race stops waiting when its abort controller fires, but it cannot stop the already-running origination hook. That hook resumes after connectTwilioCall settles and unconditionally creates the Twilio call. Its later answer wait notices the aborted signal and requests cancellation, but the unwanted call has already been placed and can ring or answer first.
Example: The caller hangs up while connectTwilioCall takes two seconds. The warm-transfer task completes and deletes the briefing room. When the connector request returns, the hook still calls Twilio, so the supervisor's phone rings for a cancelled transfer.
Recommended fix: Check signal.aborted before each side effect, especially before createTwilioCall. Also pass signal into the underlying connector and Twilio requests where their APIs support cancellation, while retaining post-creation cancellation for races after Twilio returns a call SID.
Was this helpful? React with 👍 or 👎 to provide feedback.
| async function cancelTwilioCall(auth: TwilioRestAuth, callSid: string): Promise<void> { | ||
| await twilioRequest(auth, `/Calls/${encodeURIComponent(callSid)}.json`, { Status: 'canceled' }); |
There was a problem hiding this comment.
🟡 Rejected cancellations leave calls ringing
When Twilio rejects Status=canceled, cancelTwilioCall still resolves because fetch accepts HTTP errors. The abandoned outbound call can continue ringing.
Learn more
A Fetch promise resolves normally for 4xx and 5xx responses. Therefore the current cancellation helper reports success for every HTTP response, and the surrounding .catch() only handles transport failures. The briefing room is then torn down while Twilio still owns a live ringing call.
Example: The answer timeout expires and Twilio returns HTTP 500 for the cancellation request. cancelTwilioCall resolves, the transfer reports failure, and the supervisor's phone keeps ringing until Twilio ends the call independently.
Recommended fix: Make cancelTwilioCall inspect resp.ok and throw a redacted error for non-success responses. Handle that rejection explicitly at the call site with an appropriate log and, if cancellation reliability requires it, a bounded retry policy.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| const callSid = form.get('CallSid') ?? ''; | ||
| const caller = form.get('From') ?? ''; | ||
| console.log(`Inbound call ${callSid} from ${mask(caller)}`); |
| /** Route one entry of the webhook's `calls` array to the connector API. */ | ||
| async function handleCallEvent(call: WhatsAppCall, phoneNumberId: string): Promise<void> { | ||
| const { id: callId, event, direction } = call; | ||
| console.log(`Call event ${event} (${direction}) for ${callId}`); |
A warm transfer creates a new outbound call, so human recipients normally see the business number rather than the inbound customer's number. This adds per-call caller-ID preservation through Twilio's CallToken, while keeping the business number as the default and fallback. It lets applications use the SDK's consultation and cleanup flow instead of replacing Twilio call creation to preserve customer identity.
API and behavior
twilioFromNumberstays the agent/business Twilio number or verified caller ID.originalCallerNumbercarries the original inbound webhook'sFrom.twilioCallTokenopts into preservation and requires the matching original caller number. No separate enable flag or fallback-number option is needed.Keep the number/token pair in server-side state keyed by the incoming
CallSid, obtained from a validated Twilio webhook. Tokens are not included in connector requests, TwiML, prompts, or participant attributes. The token is not permission to choose arbitrary caller IDs.This can help recipients identify the customer and support caller-ID-based lookup/callback records; the PR does not implement CRM matching. Caller-ID presentation remains subject to Twilio acceptance and downstream carrier behavior. The existing Calls API is used, with no conference or LiveKit server API change.
Twilio references
From,CallSid, andCallToken.Prerequisite
This PR targets
mainand depends on #2402, which introduces the JavaScript connector-transfer helper. The branch includes its two prerequisite commits with original authorship and merges currentmain. Once #2402 merges, the branch can be rebased to leave only this feature. The public API report and examples are updated.Python counterpart: livekit/agents#7309.
Validation
Live QA — 2026-09-16, current fallback behavior
Ran both local SDK workers against UAT LiveKit Cloud and real Twilio calls. Two temporary phone numbers were used: a customer/receiver in a temporary subaccount and a business number in the SDK account. This makes the customer's number unverified in the dialing account. Each SDK passed all four cases below (8/8 total):
Every successful case had one receiver webhook and published audio tracks for the customer and transferred human in the caller room. The invalid-token Python attempt also recorded the SDK's explicit caller-ID fallback warning. Unit tests verify both request bodies, the exact single-retry limit, and failure exclusions.
One Python harness attempt raced room initialization before dialing. After adding an explicit room connection in the harness, that case passed using the same temporary numbers; interrupted harness attempts are excluded from the final matrix. No SDK behavior was changed to work around that harness issue.
Independent API reads confirmed both purchased numbers released (404), the temporary subaccount closed, 38 associated call legs checked with zero active, and zero QA rooms remaining. Local QA workers and the webhook tunnel were stopped.
Earlier live QA on the pre-fallback revision also covered supervisor decline, no answer, busy/rejected calls, and caller hangup during ringing in both SDKs. Those lifecycle cases are historical validation, not a fresh full-matrix run of this revision. Current regression tests cover cancellation of an unanswered fallback call.
Limits: automated endpoints verify caller-ID signaling and published audio tracks, not physical handset display, subjective two-way audio quality, or every carrier. Telnyx is not included in these results.