fix(realtime): label pre-ready transcription closes

This commit is contained in:
Vincent Koc
2026-05-03 17:04:37 -07:00
parent 34b3471f85
commit ecd562b2b5
6 changed files with 35 additions and 1 deletions

View File

@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
- Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc.
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
- QA/cache: require the full `CACHE-OK <suffix>` marker before live cache probes stop retrying, so suffix-only prose cannot hide a broken probe response. Thanks @vincentkoc.
- Slack/Matrix: avoid creating blank progress-draft messages when `streaming.progress.label=false` and progress tool lines are disabled. Thanks @vincentkoc.

View File

@@ -232,6 +232,8 @@ function createDeepgramRealtimeTranscriptionSession(
reconnectDelayMs: DEEPGRAM_REALTIME_RECONNECT_DELAY_MS,
maxQueuedBytes: DEEPGRAM_REALTIME_MAX_QUEUED_BYTES,
connectTimeoutMessage: "Deepgram realtime transcription connection timeout",
connectClosedBeforeReadyMessage:
"Deepgram realtime transcription connection closed before ready",
reconnectLimitMessage: "Deepgram realtime transcription reconnect limit reached",
sendAudio: (audio, transport) => {
transport.sendBinary(audio);

View File

@@ -138,6 +138,7 @@ function createOpenAIRealtimeTranscriptionSession(
maxReconnectAttempts: OPENAI_REALTIME_TRANSCRIPTION_MAX_RECONNECT_ATTEMPTS,
reconnectDelayMs: OPENAI_REALTIME_TRANSCRIPTION_RECONNECT_DELAY_MS,
connectTimeoutMessage: "OpenAI realtime transcription connection timeout",
connectClosedBeforeReadyMessage: "OpenAI realtime transcription connection closed before ready",
reconnectLimitMessage: "OpenAI realtime transcription reconnect limit reached",
sendAudio: (audio, transport) => {
transport.sendJson({

View File

@@ -226,6 +226,7 @@ function createXaiRealtimeTranscriptionSession(
reconnectDelayMs: XAI_REALTIME_STT_RECONNECT_DELAY_MS,
maxQueuedBytes: XAI_REALTIME_STT_MAX_QUEUED_BYTES,
connectTimeoutMessage: "xAI realtime transcription connection timeout",
connectClosedBeforeReadyMessage: "xAI realtime transcription connection closed before ready",
reconnectLimitMessage: "xAI realtime transcription reconnect limit reached",
sendAudio: (audio, transport) => {
transport.sendBinary(audio);

View File

@@ -13,6 +13,7 @@ afterEach(async () => {
});
async function createRealtimeServer(params?: {
closeOnConnection?: boolean;
initialEvent?: unknown;
onBinary?: (payload: Buffer) => void;
onText?: (payload: unknown) => void;
@@ -25,6 +26,10 @@ async function createRealtimeServer(params?: {
wss.handleUpgrade(request, socket, head, (ws) => {
clients.add(ws);
ws.on("close", () => clients.delete(ws));
if (params?.closeOnConnection) {
ws.close(1011, "setup failed");
return;
}
if (params?.initialEvent) {
ws.send(JSON.stringify(params.initialEvent));
}
@@ -153,4 +158,27 @@ describe("createRealtimeTranscriptionWebSocketSession", () => {
expect(session.isConnected()).toBe(false);
expect(onError).toHaveBeenCalledWith(expect.any(Error));
});
it("reports pre-ready closes separately from connection timeouts", async () => {
const server = await createRealtimeServer({ closeOnConnection: true });
const onError = vi.fn();
const session = createRealtimeTranscriptionWebSocketSession({
providerId: "test",
callbacks: { onError },
url: server.url,
connectTimeoutMessage: "test realtime transcription connection timeout",
connectClosedBeforeReadyMessage: "test realtime transcription connection closed before ready",
sendAudio: (audio, transport) => {
transport.sendBinary(audio);
},
});
await expect(session.connect()).rejects.toThrow(
"test realtime transcription connection closed before ready",
);
expect(onError).toHaveBeenCalledWith(expect.any(Error));
expect(onError.mock.calls[0]?.[0]).toMatchObject({
message: "test realtime transcription connection closed before ready",
});
});
});

View File

@@ -20,6 +20,7 @@ export type RealtimeTranscriptionWebSocketTransport = {
export type RealtimeTranscriptionWebSocketSessionOptions<Event = unknown> = {
callbacks: RealtimeTranscriptionSessionCallbacks;
connectClosedBeforeReadyMessage?: string;
connectTimeoutMessage?: string;
connectTimeoutMs?: number;
closeTimeoutMs?: number;
@@ -267,7 +268,7 @@ class WebSocketRealtimeTranscriptionSession<Event> implements RealtimeTranscript
if (!opened || !settled) {
failConnect(
new Error(
this.options.connectTimeoutMessage ??
this.options.connectClosedBeforeReadyMessage ??
`${this.options.providerId} realtime transcription connection closed before ready`,
),
);