From 6504087b97eab432bd60fc3df2ff5728fce988dd Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:08:47 -0500 Subject: [PATCH] fix: restore voice call replay dedupe keys --- .../voice-call/src/manager.restore.test.ts | 33 +++++++++++++++++++ extensions/voice-call/src/manager/store.ts | 6 ++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/extensions/voice-call/src/manager.restore.test.ts b/extensions/voice-call/src/manager.restore.test.ts index 61dccf0e095..712eff72dc4 100644 --- a/extensions/voice-call/src/manager.restore.test.ts +++ b/extensions/voice-call/src/manager.restore.test.ts @@ -107,4 +107,37 @@ describe("CallManager verification on restore", () => { expect(activeCall.callId).toBe(call.callId); expect(activeCall.state).toBe(call.state); }); + + it("restores dedupe keys from terminal persisted calls so replayed webhooks stay ignored", async () => { + const storePath = createTestStorePath(); + const persisted = makePersistedCall({ + state: "completed", + endedAt: Date.now() - 5_000, + endReason: "completed", + processedEventIds: ["evt-terminal-init"], + }); + writeCallsToStore(storePath, [persisted]); + + const provider = new FakeProvider(); + const config = VoiceCallConfigSchema.parse({ + enabled: true, + provider: "plivo", + fromNumber: "+15550000000", + }); + const manager = new CallManager(config, storePath); + await manager.initialize(provider, "https://example.com/voice/webhook"); + + manager.processEvent({ + id: "evt-terminal-init", + type: "call.initiated", + callId: String(persisted.providerCallId), + providerCallId: String(persisted.providerCallId), + timestamp: Date.now(), + direction: "outbound", + from: "+15550000000", + to: "+15550000001", + }); + + expect(manager.getActiveCalls()).toHaveLength(0); + }); }); diff --git a/extensions/voice-call/src/manager/store.ts b/extensions/voice-call/src/manager/store.ts index a15edaa8277..f412efd0be0 100644 --- a/extensions/voice-call/src/manager/store.ts +++ b/extensions/voice-call/src/manager/store.ts @@ -50,6 +50,9 @@ export function loadActiveCallsFromStore(storePath: string): { const rejectedProviderCallIds = new Set(); for (const [callId, call] of callMap) { + for (const eventId of call.processedEventIds) { + processedEventIds.add(eventId); + } if (TerminalStates.has(call.state)) { continue; } @@ -57,9 +60,6 @@ export function loadActiveCallsFromStore(storePath: string): { if (call.providerCallId) { providerCallIdMap.set(call.providerCallId, callId); } - for (const eventId of call.processedEventIds) { - processedEventIds.add(eventId); - } } return { activeCalls, providerCallIdMap, processedEventIds, rejectedProviderCallIds };