fix(workflows): forward Twilio CallToken during warm transfers - #2540
piyush-gambhir wants to merge 6 commits into
Conversation
Warm transfers place a new outbound call, so the supervisor sees the business number instead of the customer's. Add optional `originalCallerNumber` and `twilioCallToken` to TwilioConnectorWarmTransferTask so applications can present the inbound caller's number using Twilio's CallToken. - `twilioCallToken` opts in and requires `originalCallerNumber`. - Without a token, dial from `twilioFromNumber` as before. - On HTTP 400 / Twilio error 21210, retry once from the business number without the token. No retry on other errors or after a call exists. - The token is sent only as the `CallToken` form field of the Calls API request. It is kept out of TwiML, connector requests, participant attributes, logs and error messages.
Wrap the async request handlers so the http server callback returns void, satisfying @typescript-eslint/no-misused-promises on main.
🦋 Changeset detectedLatest commit: 42a26f2 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 5 potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
|
|
||
| /** Cancel a still-ringing Twilio call. */ | ||
| 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 cancellation leaves calls active
When Twilio rejects cancellation, cancelTwilioCall logs and returns while the supervisor call remains active. The call can answer after the transfer fails and tears down its room.
Learn more
The task enforces ringingTimeout only while waiting locally for a connector audio track. Twilio receives no matching call timeout in createTwilioCall, so it controls the call until cancellation succeeds or its provider default expires. A transient 500 response therefore leaves the outbound call live even though the workflow reports failure and deletes the consultation room.
Example: The task gives up after 30 seconds and posts Status=canceled. Twilio returns 500, so the task finishes, but the supervisor's phone keeps ringing and can still be answered.
Recommended fix: For non-null ringingTimeout, send Twilio's Timeout field on both initial and fallback Calls requests, converted and bounded to Twilio's supported seconds range. Also retry transient cancellation failures with a short bounded backoff when practical.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let body = ''; | ||
| for await (const chunk of req) body += chunk; | ||
| const form = new URLSearchParams(body); |
Description
Warm transfers normally show the business number to the supervisor. This adds optional per-call caller-ID preservation to
TwilioConnectorWarmTransferTaskthrough Twilio's CallToken, keeping the business number as the default and fallback.Replaces #2510 with linear history. Includes the two dependency commits from #2402 by @anunaym14 with original authorship because the connector workflow was absent from the base. The contribution also has its own patch changeset for
@livekit/agents.Changes Made
twilioFromNumberremains the business/verified number. SupplyoriginalCallerNumberand a nonemptytwilioCallTokenfrom the same validated inbound webhook to preserve the caller ID. Without a token, use the business number.Pre-Review Checklist
Testing
Automated tests added/updated.
All local agent tests pass: 2,733 passed, 5 skipped (credentialed inference tests require separate credentials).
restaurant_agent.tsandrealtime_agent.tslive smoke tests: Not run; this opt-in telephony change does not modify them. Both compile in the full workspace build.CallToken and general warm-transfer Vitest suites: 51 passed across three files. Includes 21212 fallback, failed cancellation reporting, sensitive-error redaction, abort during connector setup, late call creation, and suppression of fallback after abort.
New lifecycle coverage uses real
AgentSessionandAgentActivityinstances, mocking provider/transport boundaries. It verifies parent resumption after abort during connector setup, no subsequent dial, session shutdown while Twilio creation is pending, and cancellation of the late-created SID.With Node 24 and the locked pnpm 11.13.1 dependencies:
pnpm build(40 tasks),pnpm test agents --silent(163 files),pnpm typecheck,pnpm format:check,pnpm throws:check, core API Extractor, and REUSE licensing validation passed. The initialpnpm -w lint:fixpassed with 193 warnings; the finalpnpm lintafter all packages were built reports@typescript-eslint/no-misused-promisesatplugins/openai/src/ws/llm.ts:127, which is unchanged from the base. Changed-file lint passes. No unrelated plugin edits are included.The first full test attempt hit sandbox socket/subprocess restrictions; the rerun with required local permissions passed.
Historical live QA on 2026-09-16 verified valid-token caller ID, business-number defaults, and invalid-token fallback in both SDKs. This revision uses deterministic tests; no new live PSTN calls were placed.
Upstream fork workflows may require maintainer approval; local validation is not a claim that GitHub Build/Test jobs ran.
Additional Notes
Feature scope discussion: livekit/agents#7375. This issue was opened after the implementation PRs; it is a request for maintainer scope review, not prior approval.
The transfer owns setup and the created call SID. Abort settles the caller-facing task promptly; late creation has its own cancellation handler and does not block activity/session teardown. Cancellation remains best effort while the process is alive. Success follows the existing consultation flow, while provider errors propagate through task settlement.
Python counterpart: livekit/agents#7309.
References