diff --git a/src/gateway/talk-realtime-relay.test.ts b/src/gateway/talk-realtime-relay.test.ts index fbe12d2f04d..9065dfe570f 100644 --- a/src/gateway/talk-realtime-relay.test.ts +++ b/src/gateway/talk-realtime-relay.test.ts @@ -41,6 +41,22 @@ describe("talk realtime gateway relay", () => { }; } + it("rejects session creation when relay expiry would exceed Date range", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(8_640_000_000_000_000)); + + expect(() => + createTalkRealtimeRelaySession({ + context: {} as never, + connId: "conn-1", + provider: createIdleRelayProvider(), + providerConfig: {}, + instructions: "brief", + tools: [], + }), + ).toThrow("Realtime relay session expiry is outside the supported Date range"); + }); + function createAbortableRelayRunFixture(provider = createIdleRelayProvider()) { const abortController = new AbortController(); const broadcast = vi.fn(); diff --git a/src/gateway/talk-realtime-relay.ts b/src/gateway/talk-realtime-relay.ts index b3df13e0662..1e5c0565a9c 100644 --- a/src/gateway/talk-realtime-relay.ts +++ b/src/gateway/talk-realtime-relay.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import type { OpenClawConfig } from "../config/types.js"; import type { RealtimeVoiceProviderPlugin } from "../plugins/types.js"; +import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "../shared/number-coercion.js"; import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, @@ -275,8 +276,13 @@ function closeRelaySession(session: RelaySession, reason: "completed" | "error") } function pruneExpiredRelaySessions(nowMs = Date.now()): void { + const validNowMs = asDateTimestampMs(nowMs); + if (validNowMs === undefined) { + return; + } for (const session of relaySessions.values()) { - if (nowMs > session.expiresAtMs) { + const expiresAtMs = asDateTimestampMs(session.expiresAtMs); + if (expiresAtMs === undefined || validNowMs > expiresAtMs) { closeRelaySession(session, "completed"); } } @@ -308,7 +314,10 @@ export function createTalkRealtimeRelaySession( enforceRelaySessionLimits(params.connId); const forceAgentConsultOnFinalTranscript = params.forceAgentConsultOnFinalTranscript === true; const relaySessionId = randomUUID(); - const expiresAtMs = Date.now() + RELAY_SESSION_TTL_MS; + const expiresAtMs = resolveExpiresAtMsFromDurationMs(RELAY_SESSION_TTL_MS); + if (expiresAtMs === undefined) { + throw new Error("Realtime relay session expiry is outside the supported Date range"); + } const talk = createTalkSessionController( { sessionId: relaySessionId, @@ -688,7 +697,15 @@ function ensureRelayTurn(session: RelaySession): string { function getRelaySession(relaySessionId: string, connId: string): RelaySession { const session = relaySessions.get(relaySessionId); - if (!session || session.connId !== connId || Date.now() > session.expiresAtMs) { + const nowMs = asDateTimestampMs(Date.now()); + const expiresAtMs = session ? asDateTimestampMs(session.expiresAtMs) : undefined; + if ( + !session || + session.connId !== connId || + nowMs === undefined || + expiresAtMs === undefined || + nowMs > expiresAtMs + ) { if (session) { closeRelaySession(session, "completed"); }