From 3e6917c8ae1b69f55c67e11a11ee001614f53dd7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 07:28:14 +0000 Subject: [PATCH] fix: restore notify init + Plivo numbers (#846) (thanks @vrknetha) --- CHANGELOG.md | 1 + extensions/voice-call/src/manager.test.ts | 39 +++++++++++++++++++- extensions/voice-call/src/manager.ts | 30 +-------------- extensions/voice-call/src/providers/plivo.ts | 2 +- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e66812ba..694efb3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging. - Security: expand `clawdbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths. - Security: add `SECURITY.md` reporting policy. +- Voice Call: add Plivo provider for the voice-call plugin. (#846) — thanks @vrknetha. - Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba. - Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`. - Docs: expand gateway security hardening guidance and incident response checklist. diff --git a/extensions/voice-call/src/manager.test.ts b/extensions/voice-call/src/manager.test.ts index 71522dd50..a5a5b504f 100644 --- a/extensions/voice-call/src/manager.test.ts +++ b/extensions/voice-call/src/manager.test.ts @@ -20,6 +20,7 @@ import type { VoiceCallProvider } from "./providers/base.js"; class FakeProvider implements VoiceCallProvider { readonly name = "plivo" as const; + readonly playTtsCalls: PlayTtsInput[] = []; verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult { return { ok: true }; @@ -31,7 +32,9 @@ class FakeProvider implements VoiceCallProvider { return { providerCallId: "request-uuid", status: "initiated" }; } async hangupCall(_input: HangupCallInput): Promise {} - async playTts(_input: PlayTtsInput): Promise {} + async playTts(input: PlayTtsInput): Promise { + this.playTtsCalls.push(input); + } async startListening(_input: StartListeningInput): Promise {} async stopListening(_input: StopListeningInput): Promise {} } @@ -69,5 +72,37 @@ describe("CallManager", () => { expect(manager.getCallByProviderCallId("call-uuid")?.callId).toBe(callId); expect(manager.getCallByProviderCallId("request-uuid")).toBeUndefined(); }); -}); + it("speaks initial message on answered for notify mode (non-Twilio)", async () => { + const config = VoiceCallConfigSchema.parse({ + enabled: true, + provider: "plivo", + fromNumber: "+15550000000", + }); + + const storePath = path.join(os.tmpdir(), `clawdbot-voice-call-test-${Date.now()}`); + const provider = new FakeProvider(); + const manager = new CallManager(config, storePath); + manager.initialize(provider, "https://example.com/voice/webhook"); + + const { callId, success } = await manager.initiateCall( + "+15550000002", + undefined, + { message: "Hello there", mode: "notify" }, + ); + expect(success).toBe(true); + + manager.processEvent({ + id: "evt-2", + type: "call.answered", + callId, + providerCallId: "call-uuid", + timestamp: Date.now(), + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(provider.playTtsCalls).toHaveLength(1); + expect(provider.playTtsCalls[0]?.text).toBe("Hello there"); + }); +}); diff --git a/extensions/voice-call/src/manager.ts b/extensions/voice-call/src/manager.ts index 60f553f06..49d690053 100644 --- a/extensions/voice-call/src/manager.ts +++ b/extensions/voice-call/src/manager.ts @@ -672,41 +672,13 @@ export class CallManager { if (!initialMessage) return; - // For outbound notify mode, we already use inline TwiML (provider-specific) to - // deliver the message and hang up; do not double-speak. - const mode = call.metadata?.mode as CallMode | undefined; - if (call.direction === "outbound" && mode === "notify") return; - if (!this.provider || !call.providerCallId) return; // Twilio has provider-specific state for speaking ( fallback) and can // fail for inbound calls; keep existing Twilio behavior unchanged. if (this.provider.name === "twilio") return; - // Clear the initial message so it only plays once. - if (call.metadata) { - delete call.metadata.initialMessage; - } - this.persistCallRecord(call); - - void this.provider - .playTts({ - callId: call.callId, - providerCallId: call.providerCallId, - text: initialMessage, - voice: this.config.tts.voice, - }) - .then(() => { - this.addTranscriptEntry(call, "bot", initialMessage); - this.persistCallRecord(call); - }) - .catch((err) => { - console.warn( - `[voice-call] Failed to speak initial message on answered: ${ - err instanceof Error ? err.message : String(err) - }`, - ); - }); + void this.speakInitialMessage(call.providerCallId); } /** diff --git a/extensions/voice-call/src/providers/plivo.ts b/extensions/voice-call/src/providers/plivo.ts index af5b9c649..df110bfd6 100644 --- a/extensions/voice-call/src/providers/plivo.ts +++ b/extensions/voice-call/src/providers/plivo.ts @@ -404,7 +404,7 @@ export class PlivoProvider implements VoiceCallProvider { private static normalizeNumber(numberOrSip: string): string { const trimmed = numberOrSip.trim(); if (trimmed.toLowerCase().startsWith("sip:")) return trimmed; - return trimmed.startsWith("+") ? trimmed.slice(1) : trimmed; + return trimmed.replace(/[^\d+]/g, ""); } private static xmlEmpty(): string {