diff --git a/extensions/voice-call/src/manager/events.test.ts b/extensions/voice-call/src/manager/events.test.ts index 23409d79a05..f979fb1d92e 100644 --- a/extensions/voice-call/src/manager/events.test.ts +++ b/extensions/voice-call/src/manager/events.test.ts @@ -416,4 +416,45 @@ describe("processEvent (functional)", () => { expect(call.transcript).toHaveLength(1); expect(Array.from(ctx.processedEventIds)).toEqual(["stable-key-1"]); }); + + it("keeps retryable call.error events replayable", () => { + const now = Date.now(); + const ctx = createContext(); + ctx.activeCalls.set("call-retryable-error", { + callId: "call-retryable-error", + providerCallId: "provider-retryable-error", + provider: "plivo", + direction: "outbound", + state: "active", + from: "+15550000000", + to: "+15550000001", + startedAt: now, + transcript: [], + processedEventIds: [], + metadata: {}, + }); + ctx.providerCallIdMap.set("provider-retryable-error", "call-retryable-error"); + + const event: NormalizedEvent = { + id: "evt-retryable-error", + dedupeKey: "stable-retryable-error", + type: "call.error", + callId: "call-retryable-error", + providerCallId: "provider-retryable-error", + timestamp: now + 1, + error: "temporary upstream failure", + retryable: true, + }; + + processEvent(ctx, event); + processEvent(ctx, event); + + const call = ctx.activeCalls.get("call-retryable-error"); + if (!call) { + throw new Error("expected retryable error call to remain active"); + } + expect(call.state).toBe("active"); + expect(Array.from(ctx.processedEventIds)).toEqual([]); + expect(call.processedEventIds).toEqual([]); + }); }); diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index fba7ec2675a..f15fbf0ee8f 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -161,8 +161,6 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void { return; } - ctx.processedEventIds.add(dedupeKey); - if (event.providerCallId && event.providerCallId !== call.providerCallId) { const previousProviderCallId = call.providerCallId; call.providerCallId = event.providerCallId; @@ -175,7 +173,11 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void { } } - call.processedEventIds.push(dedupeKey); + const shouldCommitReplayKey = !(event.type === "call.error" && event.retryable); + if (shouldCommitReplayKey) { + ctx.processedEventIds.add(dedupeKey); + call.processedEventIds.push(dedupeKey); + } switch (event.type) { case "call.initiated": @@ -247,6 +249,8 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void { }); return; } + // Keep retryable provider errors replayable so a redelivery can still + // drive later recovery or terminal handling for the same event key. break; }