mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 14:48:55 +02:00
fix(voice-call): persist rejected replay keys
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
|
||||
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
|
||||
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
|
||||
- Voice calls: persist rejected inbound-call replay keys so duplicate carrier webhook retries stay ignored after a Gateway restart.
|
||||
- Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags.
|
||||
- Configure: show one OpenAI provider entry with ChatGPT/Codex sign-in and API key choices, and keep browsed Codex models in the saved `/model` picker allowlist.
|
||||
- Agents/model fallback: preserve auto fallback chains across deferred config reloads when session fallback provenance survives but `modelOverrideSource` is missing. Fixes #81982. Thanks @joshavant.
|
||||
|
||||
@@ -107,6 +107,32 @@ function createWebhookCall(params: {
|
||||
return callRecord;
|
||||
}
|
||||
|
||||
function persistRejectedInboundCall(params: {
|
||||
ctx: EventContext;
|
||||
event: NormalizedEvent;
|
||||
dedupeKey: string;
|
||||
providerCallId: string;
|
||||
}): void {
|
||||
const callId = params.event.callId || params.providerCallId;
|
||||
const now = Date.now();
|
||||
const rejectedCall: CallRecord = {
|
||||
callId,
|
||||
providerCallId: params.providerCallId,
|
||||
provider: params.ctx.provider?.name || "twilio",
|
||||
direction: "inbound",
|
||||
state: "hangup-bot",
|
||||
from: params.event.from || "unknown",
|
||||
to: params.event.to || params.ctx.config.fromNumber || "unknown",
|
||||
startedAt: params.event.timestamp || now,
|
||||
endedAt: now,
|
||||
endReason: "hangup-bot",
|
||||
transcript: [],
|
||||
processedEventIds: [params.dedupeKey],
|
||||
metadata: { rejectionReason: "inbound-policy" },
|
||||
};
|
||||
persistCallRecord(params.ctx.storePath, rejectedCall);
|
||||
}
|
||||
|
||||
export function processEvent(ctx: EventContext, event: NormalizedEvent): void {
|
||||
const dedupeKey = event.dedupeKey || event.id;
|
||||
if (ctx.processedEventIds.has(dedupeKey)) {
|
||||
@@ -143,6 +169,7 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void {
|
||||
}
|
||||
ctx.rejectedProviderCallIds.add(pid);
|
||||
const callId = event.callId ?? pid;
|
||||
persistRejectedInboundCall({ ctx, event, dedupeKey, providerCallId: pid });
|
||||
console.log(`[voice-call] Rejecting inbound call by policy: ${pid}`);
|
||||
void ctx.provider
|
||||
.hangupCall({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import { VoiceCallConfigSchema, type VoiceCallConfig } from "./config.js";
|
||||
import { CallManager } from "./manager.js";
|
||||
import { createTestStorePath, FakeProvider } from "./manager.test-harness.js";
|
||||
import { flushPendingCallRecordWritesForTest } from "./manager/store.js";
|
||||
import type { WebhookContext, WebhookParseOptions } from "./types.js";
|
||||
import { VoiceCallWebhookServer } from "./webhook.js";
|
||||
|
||||
@@ -132,4 +133,47 @@ describe("Voice-call webhook hangup-once lifecycle", () => {
|
||||
const { first, second, manager } = await runDuplicateInboundReplayLifecycleTest(provider);
|
||||
expectSingleRejectedReplayHangup({ first, second, provider, manager });
|
||||
});
|
||||
|
||||
it("keeps rejected inbound replay keys after manager restart", async () => {
|
||||
const storePath = createTestStorePath();
|
||||
const config = createConfig();
|
||||
const firstProvider = new RejectInboundReplayProvider("plivo");
|
||||
const firstManager = new CallManager(config, storePath);
|
||||
await firstManager.initialize(firstProvider, "https://example.com/voice/webhook");
|
||||
const firstServer = new VoiceCallWebhookServer(config, firstManager, firstProvider);
|
||||
|
||||
try {
|
||||
const baseUrl = await firstServer.start();
|
||||
const first = await postWebhookForm(
|
||||
firstServer,
|
||||
baseUrl,
|
||||
"CallSid=CA123&From=%2B15552222222",
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
} finally {
|
||||
await firstServer.stop();
|
||||
}
|
||||
await flushPendingCallRecordWritesForTest();
|
||||
expect(firstProvider.hangupCalls).toHaveLength(1);
|
||||
|
||||
const secondProvider = new RejectInboundReplayProvider("plivo");
|
||||
const secondManager = new CallManager(config, storePath);
|
||||
await secondManager.initialize(secondProvider, "https://example.com/voice/webhook");
|
||||
const secondServer = new VoiceCallWebhookServer(config, secondManager, secondProvider);
|
||||
|
||||
try {
|
||||
const baseUrl = await secondServer.start();
|
||||
const replay = await postWebhookForm(
|
||||
secondServer,
|
||||
baseUrl,
|
||||
"CallSid=CA123&From=%2B15552222222",
|
||||
);
|
||||
expect(replay.status).toBe(200);
|
||||
} finally {
|
||||
await secondServer.stop();
|
||||
}
|
||||
|
||||
expect(secondProvider.hangupCalls).toHaveLength(0);
|
||||
expect(secondManager.getCallByProviderCallId("provider-inbound-1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user