From 26bf8f0dc87bce8e464ab198e4820cd50f58ffae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 03:00:00 -0400 Subject: [PATCH] fix(voice-call): cap CLI gateway timeouts --- extensions/voice-call/src/cli.test.ts | 33 +++++++++++++++++++++++++++ extensions/voice-call/src/cli.ts | 29 ++++++++++++++++++----- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/extensions/voice-call/src/cli.test.ts b/extensions/voice-call/src/cli.test.ts index e9e446ecef4..d55699e9e3d 100644 --- a/extensions/voice-call/src/cli.test.ts +++ b/extensions/voice-call/src/cli.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { describe, expect, it } from "vitest"; import { testing } from "./cli.js"; @@ -35,3 +36,35 @@ describe("parseVoiceCallIntOption", () => { ).toThrow("Invalid numeric value for --port: 65536"); }); }); + +describe("voice-call CLI timeout helpers", () => { + it("caps gateway operation timeout grace", () => { + expect(testing.resolveGatewayOperationTimeoutMs({ ringTimeoutMs: 10_000 } as never)).toBe( + 30_000, + ); + expect(testing.resolveGatewayOperationTimeoutMs({ ringTimeoutMs: 60_000 } as never)).toBe( + 65_000, + ); + expect( + testing.resolveGatewayOperationTimeoutMs({ ringTimeoutMs: Number.MAX_SAFE_INTEGER } as never), + ).toBe(MAX_TIMER_TIMEOUT_MS); + }); + + it("caps gateway continue timeout totals", () => { + expect(testing.resolveGatewayContinueTimeoutMs({ transcriptTimeoutMs: 180_000 } as never)).toBe( + 220_000, + ); + expect( + testing.resolveGatewayContinueTimeoutMs({ + transcriptTimeoutMs: Number.MAX_SAFE_INTEGER, + } as never), + ).toBe(MAX_TIMER_TIMEOUT_MS); + }); + + it("caps gateway polling deadlines", () => { + expect(testing.resolveVoiceCallDeadlineMs(5_000, 10_000)).toBe(15_000); + expect(testing.resolveVoiceCallDeadlineMs(Number.MAX_SAFE_INTEGER, 10_000)).toBe( + 10_000 + MAX_TIMER_TIMEOUT_MS, + ); + }); +}); diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index 25ce8aceff8..a1f2b21f094 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -5,7 +5,12 @@ import { format } from "node:util"; import type { Command } from "commander"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime"; -import { MAX_TCP_PORT, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime"; +import { + clampTimerTimeoutMs, + MAX_TIMER_TIMEOUT_MS, + MAX_TCP_PORT, + parseStrictNonNegativeInteger, +} from "openclaw/plugin-sdk/number-runtime"; import { isRecord, normalizeOptionalLowercaseString, @@ -66,6 +71,9 @@ export const testing = { }, isGatewayUnavailableForLocalFallback, parseVoiceCallIntOption, + resolveGatewayContinueTimeoutMs, + resolveGatewayOperationTimeoutMs, + resolveVoiceCallDeadlineMs, }; function writeStdoutLine(...values: unknown[]): void { @@ -128,17 +136,26 @@ async function callVoiceCallGateway( } function resolveGatewayOperationTimeoutMs(config: VoiceCallConfig): number { - return Math.max(VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS, config.ringTimeoutMs + 5000); + return Math.max( + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS, + clampTimerTimeoutMs(config.ringTimeoutMs + 5000) ?? 1, + ); } function resolveGatewayContinueTimeoutMs(config: VoiceCallConfig): number { return ( - config.transcriptTimeoutMs + - VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + - VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS + clampTimerTimeoutMs( + config.transcriptTimeoutMs + + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + + VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS, + ) ?? 1 ); } +function resolveVoiceCallDeadlineMs(timeoutMs: number, nowMs = Date.now()): number { + return nowMs + (clampTimerTimeoutMs(timeoutMs) ?? MAX_TIMER_TIMEOUT_MS); +} + function isUnknownGatewayMethod(err: unknown, method: VoiceCallGatewayMethod): boolean { return formatErrorMessage(err).includes(`unknown method: ${method}`); } @@ -185,7 +202,7 @@ async function pollVoiceCallContinueGateway(params: { operationId: string; timeoutMs: number; }): Promise { - const deadlineMs = Date.now() + params.timeoutMs; + const deadlineMs = resolveVoiceCallDeadlineMs(params.timeoutMs); while (Date.now() <= deadlineMs) { const gateway = await callVoiceCallGateway(