fix(voice-call): end realtime completed calls

This commit is contained in:
Vincent Koc
2026-05-03 22:09:09 -07:00
parent 66267b5435
commit 20ade148be
3 changed files with 78 additions and 0 deletions

View File

@@ -50,6 +50,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Voice Call: mark realtime calls completed when the realtime provider closes normally, so Twilio/OpenAI/Google realtime stop events do not leave active call records behind. Thanks @vincentkoc.
- Exec approvals: treat POSIX `exec` as a command carrier for inline eval, shell-wrapper, and eval/source detection, so approval explanations and command-risk checks do not miss payloads hidden behind `exec`. Thanks @vincentkoc.
- Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model.
- Diagnostics: handle missing session-tail files in cron recovery context without tripping extension test typecheck. Thanks @vincentkoc.

View File

@@ -337,6 +337,82 @@ describe("RealtimeCallHandler path routing", () => {
}
});
it("marks realtime calls ended when the provider closes normally", async () => {
let callbacks:
| {
onClose?: (reason: "completed" | "error") => void;
}
| undefined;
const processEvent = vi.fn();
const createBridge = vi.fn(
(request: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0]) => {
callbacks = request;
return makeBridge({
close: () => {
callbacks?.onClose?.("completed");
},
});
},
);
const getCallByProviderCallId = vi.fn(
(): CallRecord => ({
callId: "call-1",
providerCallId: "CA-complete",
provider: "twilio",
direction: "inbound",
state: "ringing",
from: "+15550001234",
to: "+15550009999",
startedAt: Date.now(),
transcript: [],
processedEventIds: [],
metadata: {},
}),
);
const handler = makeHandler(undefined, {
manager: {
processEvent,
getCallByProviderCallId,
},
realtimeProvider: makeRealtimeProvider(createBridge),
});
const server = await startRealtimeServer(handler);
try {
const ws = await connectWs(server.url);
try {
ws.send(
JSON.stringify({
event: "start",
start: { streamSid: "MZ-complete", callSid: "CA-complete" },
}),
);
await vi.waitFor(() => {
expect(createBridge).toHaveBeenCalled();
});
ws.send(JSON.stringify({ event: "stop" }));
await vi.waitFor(() => {
expect(processEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: "call.ended",
callId: "call-1",
providerCallId: "CA-complete",
reason: "completed",
}),
);
});
} finally {
if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
ws.close();
}
}
} finally {
await server.close();
}
});
it("submits continuing responses only for realtime agent consult calls", async () => {
let callbacks:
| {

View File

@@ -367,6 +367,7 @@ export class RealtimeCallHandler {
this.activeBridgesByCallId.delete(callSid);
this.partialUserTranscriptsByCallId.delete(callId);
if (reason !== "error") {
emitCallEnd("completed");
return;
}
emitCallEnd("error");