mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(voice-call): tighten manager outbound behavior
This commit is contained in:
@@ -46,17 +46,44 @@ class FakeProvider implements VoiceCallProvider {
|
||||
}
|
||||
}
|
||||
|
||||
let storeSeq = 0;
|
||||
|
||||
function createTestStorePath(): string {
|
||||
storeSeq += 1;
|
||||
return path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}-${storeSeq}`);
|
||||
}
|
||||
|
||||
function createManagerHarness(
|
||||
configOverrides: Record<string, unknown> = {},
|
||||
provider = new FakeProvider(),
|
||||
): {
|
||||
manager: CallManager;
|
||||
provider: FakeProvider;
|
||||
} {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
...configOverrides,
|
||||
});
|
||||
const manager = new CallManager(config, createTestStorePath());
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
return { manager, provider };
|
||||
}
|
||||
|
||||
function markCallAnswered(manager: CallManager, callId: string, eventId: string): void {
|
||||
manager.processEvent({
|
||||
id: eventId,
|
||||
type: "call.answered",
|
||||
callId,
|
||||
providerCallId: "request-uuid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
describe("CallManager", () => {
|
||||
it("upgrades providerCallId mapping when provider ID changes", async () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(new FakeProvider(), "https://example.com/voice/webhook");
|
||||
const { manager } = createManagerHarness();
|
||||
|
||||
const { callId, success, error } = await manager.initiateCall("+15550000001");
|
||||
expect(success).toBe(true);
|
||||
@@ -81,16 +108,7 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
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(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
const { manager, provider } = createManagerHarness();
|
||||
|
||||
const { callId, success } = await manager.initiateCall("+15550000002", undefined, {
|
||||
message: "Hello there",
|
||||
@@ -113,19 +131,11 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("rejects inbound calls with missing caller ID when allowlist enabled", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550001234"],
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-allowlist-missing",
|
||||
type: "call.initiated",
|
||||
@@ -142,19 +152,11 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("rejects inbound calls with anonymous caller ID when allowlist enabled", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550001234"],
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-allowlist-anon",
|
||||
type: "call.initiated",
|
||||
@@ -172,19 +174,11 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("rejects inbound calls that only match allowlist suffixes", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550001234"],
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-allowlist-suffix",
|
||||
type: "call.initiated",
|
||||
@@ -202,18 +196,10 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("rejects duplicate inbound events with a single hangup call", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
inboundPolicy: "disabled",
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-reject-init",
|
||||
type: "call.initiated",
|
||||
@@ -242,18 +228,11 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("accepts inbound calls that exactly match the allowlist", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager } = createManagerHarness({
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550001234"],
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(new FakeProvider(), "https://example.com/voice/webhook");
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-allowlist-exact",
|
||||
type: "call.initiated",
|
||||
@@ -269,28 +248,14 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("completes a closed-loop turn without live audio", async () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
transcriptTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
const started = await manager.initiateCall("+15550000003");
|
||||
expect(started.success).toBe(true);
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-closed-loop-answered",
|
||||
type: "call.answered",
|
||||
callId: started.callId,
|
||||
providerCallId: "request-uuid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
markCallAnswered(manager, started.callId, "evt-closed-loop-answered");
|
||||
|
||||
const turnPromise = manager.continueCall(started.callId, "How can I help?");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@@ -323,28 +288,14 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("rejects overlapping continueCall requests for the same call", async () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
transcriptTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
const started = await manager.initiateCall("+15550000004");
|
||||
expect(started.success).toBe(true);
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-overlap-answered",
|
||||
type: "call.answered",
|
||||
callId: started.callId,
|
||||
providerCallId: "request-uuid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
markCallAnswered(manager, started.callId, "evt-overlap-answered");
|
||||
|
||||
const first = manager.continueCall(started.callId, "First prompt");
|
||||
const second = await manager.continueCall(started.callId, "Second prompt");
|
||||
@@ -369,28 +320,14 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("tracks latency metadata across multiple closed-loop turns", async () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
transcriptTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
const started = await manager.initiateCall("+15550000005");
|
||||
expect(started.success).toBe(true);
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-multi-answered",
|
||||
type: "call.answered",
|
||||
callId: started.callId,
|
||||
providerCallId: "request-uuid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
markCallAnswered(manager, started.callId, "evt-multi-answered");
|
||||
|
||||
const firstTurn = manager.continueCall(started.callId, "First question");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@@ -436,28 +373,14 @@ describe("CallManager", () => {
|
||||
});
|
||||
|
||||
it("handles repeated closed-loop turns without waiter churn", async () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
const { manager, provider } = createManagerHarness({
|
||||
transcriptTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
const storePath = path.join(os.tmpdir(), `openclaw-voice-call-test-${Date.now()}`);
|
||||
const provider = new FakeProvider();
|
||||
const manager = new CallManager(config, storePath);
|
||||
manager.initialize(provider, "https://example.com/voice/webhook");
|
||||
|
||||
const started = await manager.initiateCall("+15550000006");
|
||||
expect(started.success).toBe(true);
|
||||
|
||||
manager.processEvent({
|
||||
id: "evt-loop-answered",
|
||||
type: "call.answered",
|
||||
callId: started.callId,
|
||||
providerCallId: "request-uuid",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
markCallAnswered(manager, started.callId, "evt-loop-answered");
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const turnPromise = manager.continueCall(started.callId, `Prompt ${i}`);
|
||||
|
||||
@@ -45,6 +45,32 @@ function createProvider(overrides: Partial<VoiceCallProvider> = {}): VoiceCallPr
|
||||
};
|
||||
}
|
||||
|
||||
function createInboundDisabledConfig() {
|
||||
return VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
inboundPolicy: "disabled",
|
||||
});
|
||||
}
|
||||
|
||||
function createInboundInitiatedEvent(params: {
|
||||
id: string;
|
||||
providerCallId: string;
|
||||
from: string;
|
||||
}): NormalizedEvent {
|
||||
return {
|
||||
id: params.id,
|
||||
type: "call.initiated",
|
||||
callId: params.providerCallId,
|
||||
providerCallId: params.providerCallId,
|
||||
timestamp: Date.now(),
|
||||
direction: "inbound",
|
||||
from: params.from,
|
||||
to: "+15550000000",
|
||||
};
|
||||
}
|
||||
|
||||
describe("processEvent (functional)", () => {
|
||||
it("calls provider hangup when rejecting inbound call", () => {
|
||||
const hangupCalls: HangupCallInput[] = [];
|
||||
@@ -55,24 +81,14 @@ describe("processEvent (functional)", () => {
|
||||
});
|
||||
|
||||
const ctx = createContext({
|
||||
config: VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
inboundPolicy: "disabled",
|
||||
}),
|
||||
config: createInboundDisabledConfig(),
|
||||
provider,
|
||||
});
|
||||
const event: NormalizedEvent = {
|
||||
const event = createInboundInitiatedEvent({
|
||||
id: "evt-1",
|
||||
type: "call.initiated",
|
||||
callId: "prov-1",
|
||||
providerCallId: "prov-1",
|
||||
timestamp: Date.now(),
|
||||
direction: "inbound",
|
||||
from: "+15559999999",
|
||||
to: "+15550000000",
|
||||
};
|
||||
});
|
||||
|
||||
processEvent(ctx, event);
|
||||
|
||||
@@ -87,24 +103,14 @@ describe("processEvent (functional)", () => {
|
||||
|
||||
it("does not call hangup when provider is null", () => {
|
||||
const ctx = createContext({
|
||||
config: VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
inboundPolicy: "disabled",
|
||||
}),
|
||||
config: createInboundDisabledConfig(),
|
||||
provider: null,
|
||||
});
|
||||
const event: NormalizedEvent = {
|
||||
const event = createInboundInitiatedEvent({
|
||||
id: "evt-2",
|
||||
type: "call.initiated",
|
||||
callId: "prov-2",
|
||||
providerCallId: "prov-2",
|
||||
timestamp: Date.now(),
|
||||
direction: "inbound",
|
||||
from: "+15551111111",
|
||||
to: "+15550000000",
|
||||
};
|
||||
});
|
||||
|
||||
processEvent(ctx, event);
|
||||
|
||||
@@ -119,24 +125,14 @@ describe("processEvent (functional)", () => {
|
||||
},
|
||||
});
|
||||
const ctx = createContext({
|
||||
config: VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
inboundPolicy: "disabled",
|
||||
}),
|
||||
config: createInboundDisabledConfig(),
|
||||
provider,
|
||||
});
|
||||
const event1: NormalizedEvent = {
|
||||
const event1 = createInboundInitiatedEvent({
|
||||
id: "evt-init",
|
||||
type: "call.initiated",
|
||||
callId: "prov-dup",
|
||||
providerCallId: "prov-dup",
|
||||
timestamp: Date.now(),
|
||||
direction: "inbound",
|
||||
from: "+15552222222",
|
||||
to: "+15550000000",
|
||||
};
|
||||
});
|
||||
const event2: NormalizedEvent = {
|
||||
id: "evt-ring",
|
||||
type: "call.ringing",
|
||||
@@ -228,24 +224,14 @@ describe("processEvent (functional)", () => {
|
||||
},
|
||||
});
|
||||
const ctx = createContext({
|
||||
config: VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "plivo",
|
||||
fromNumber: "+15550000000",
|
||||
inboundPolicy: "disabled",
|
||||
}),
|
||||
config: createInboundDisabledConfig(),
|
||||
provider,
|
||||
});
|
||||
const event: NormalizedEvent = {
|
||||
const event = createInboundInitiatedEvent({
|
||||
id: "evt-fail",
|
||||
type: "call.initiated",
|
||||
callId: "prov-fail",
|
||||
providerCallId: "prov-fail",
|
||||
timestamp: Date.now(),
|
||||
direction: "inbound",
|
||||
from: "+15553333333",
|
||||
to: "+15550000000",
|
||||
};
|
||||
});
|
||||
|
||||
expect(() => processEvent(ctx, event)).not.toThrow();
|
||||
expect(ctx.activeCalls.size).toBe(0);
|
||||
|
||||
@@ -51,6 +51,32 @@ type EndCallContext = Pick<
|
||||
| "maxDurationTimers"
|
||||
>;
|
||||
|
||||
type ConnectedCallContext = Pick<CallManagerContext, "activeCalls" | "provider">;
|
||||
|
||||
type ConnectedCallLookup =
|
||||
| { kind: "error"; error: string }
|
||||
| { kind: "ended"; call: CallRecord }
|
||||
| {
|
||||
kind: "ok";
|
||||
call: CallRecord;
|
||||
providerCallId: string;
|
||||
provider: NonNullable<ConnectedCallContext["provider"]>;
|
||||
};
|
||||
|
||||
function lookupConnectedCall(ctx: ConnectedCallContext, callId: CallId): ConnectedCallLookup {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) {
|
||||
return { kind: "error", error: "Call not found" };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { kind: "error", error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
return { kind: "ended", call };
|
||||
}
|
||||
return { kind: "ok", call, providerCallId: call.providerCallId, provider: ctx.provider };
|
||||
}
|
||||
|
||||
export async function initiateCall(
|
||||
ctx: InitiateContext,
|
||||
to: string,
|
||||
@@ -149,26 +175,25 @@ export async function speak(
|
||||
callId: CallId,
|
||||
text: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
const lookup = lookupConnectedCall(ctx, callId);
|
||||
if (lookup.kind === "error") {
|
||||
return { success: false, error: lookup.error };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
if (lookup.kind === "ended") {
|
||||
return { success: false, error: "Call has ended" };
|
||||
}
|
||||
const { call, providerCallId, provider } = lookup;
|
||||
|
||||
try {
|
||||
transitionState(call, "speaking");
|
||||
persistCallRecord(ctx.storePath, call);
|
||||
|
||||
addTranscriptEntry(call, "bot", text);
|
||||
|
||||
const voice = ctx.provider?.name === "twilio" ? ctx.config.tts?.openai?.voice : undefined;
|
||||
await ctx.provider.playTts({
|
||||
const voice = provider.name === "twilio" ? ctx.config.tts?.openai?.voice : undefined;
|
||||
await provider.playTts({
|
||||
callId,
|
||||
providerCallId: call.providerCallId,
|
||||
providerCallId,
|
||||
text,
|
||||
voice,
|
||||
});
|
||||
@@ -232,16 +257,15 @@ export async function continueCall(
|
||||
callId: CallId,
|
||||
prompt: string,
|
||||
): Promise<{ success: boolean; transcript?: string; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
const lookup = lookupConnectedCall(ctx, callId);
|
||||
if (lookup.kind === "error") {
|
||||
return { success: false, error: lookup.error };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
if (lookup.kind === "ended") {
|
||||
return { success: false, error: "Call has ended" };
|
||||
}
|
||||
const { call, providerCallId, provider } = lookup;
|
||||
|
||||
if (ctx.activeTurnCalls.has(callId) || ctx.transcriptWaiters.has(callId)) {
|
||||
return { success: false, error: "Already waiting for transcript" };
|
||||
}
|
||||
@@ -256,13 +280,13 @@ export async function continueCall(
|
||||
persistCallRecord(ctx.storePath, call);
|
||||
|
||||
const listenStartedAt = Date.now();
|
||||
await ctx.provider.startListening({ callId, providerCallId: call.providerCallId });
|
||||
await provider.startListening({ callId, providerCallId });
|
||||
|
||||
const transcript = await waitForFinalTranscript(ctx, callId);
|
||||
const transcriptReceivedAt = Date.now();
|
||||
|
||||
// Best-effort: stop listening after final transcript.
|
||||
await ctx.provider.stopListening({ callId, providerCallId: call.providerCallId });
|
||||
await provider.stopListening({ callId, providerCallId });
|
||||
|
||||
const lastTurnLatencyMs = transcriptReceivedAt - turnStartedAt;
|
||||
const lastTurnListenWaitMs = transcriptReceivedAt - listenStartedAt;
|
||||
@@ -302,21 +326,19 @@ export async function endCall(
|
||||
ctx: EndCallContext,
|
||||
callId: CallId,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const call = ctx.activeCalls.get(callId);
|
||||
if (!call) {
|
||||
return { success: false, error: "Call not found" };
|
||||
const lookup = lookupConnectedCall(ctx, callId);
|
||||
if (lookup.kind === "error") {
|
||||
return { success: false, error: lookup.error };
|
||||
}
|
||||
if (!ctx.provider || !call.providerCallId) {
|
||||
return { success: false, error: "Call not connected" };
|
||||
}
|
||||
if (TerminalStates.has(call.state)) {
|
||||
if (lookup.kind === "ended") {
|
||||
return { success: true };
|
||||
}
|
||||
const { call, providerCallId, provider } = lookup;
|
||||
|
||||
try {
|
||||
await ctx.provider.hangupCall({
|
||||
await provider.hangupCall({
|
||||
callId,
|
||||
providerCallId: call.providerCallId,
|
||||
providerCallId,
|
||||
reason: "hangup-bot",
|
||||
});
|
||||
|
||||
@@ -329,9 +351,7 @@ export async function endCall(
|
||||
rejectTranscriptWaiter(ctx, callId, "Call ended: hangup-bot");
|
||||
|
||||
ctx.activeCalls.delete(callId);
|
||||
if (call.providerCallId) {
|
||||
ctx.providerCallIdMap.delete(call.providerCallId);
|
||||
}
|
||||
ctx.providerCallIdMap.delete(providerCallId);
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user