From 3efd1542d382b3e175945c9a01338d9332fce648 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Thu, 21 May 2026 17:18:04 +1000 Subject: [PATCH] fix: persist realtime consult results --- .../[id]/talk/realtime/relay/route.test.ts | 1 + .../[id]/talk/realtime/relay/route.ts | 2 +- src/lib/realtime-voice-client.ts | 18 +++++++- src/lib/realtime-voice-gateway-relay.ts | 44 +++++++++++++------ 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/app/api/runtimes/[id]/talk/realtime/relay/route.test.ts b/src/app/api/runtimes/[id]/talk/realtime/relay/route.test.ts index 9a45309..cfff020 100644 --- a/src/app/api/runtimes/[id]/talk/realtime/relay/route.test.ts +++ b/src/app/api/runtimes/[id]/talk/realtime/relay/route.test.ts @@ -161,6 +161,7 @@ describe("POST /api/runtimes/[id]/talk/realtime/relay", () => { result: { delegated: true, runId: "run_1", + finalText: "The repo is a CrewCMD app.", result: { ok: true }, }, }); diff --git a/src/app/api/runtimes/[id]/talk/realtime/relay/route.ts b/src/app/api/runtimes/[id]/talk/realtime/relay/route.ts index 8539052..d03d3bd 100644 --- a/src/app/api/runtimes/[id]/talk/realtime/relay/route.ts +++ b/src/app/api/runtimes/[id]/talk/realtime/relay/route.ts @@ -137,7 +137,7 @@ async function runRealtimeToolCall( callId: params.callId, result: { result: text }, }); - return { delegated: true, runId, result }; + return { delegated: true, runId, result, finalText: text }; } catch (error) { const message = error instanceof Error ? error.message : "OpenClaw realtime tool call failed"; await client.realtimeRelayToolResult({ diff --git a/src/lib/realtime-voice-client.ts b/src/lib/realtime-voice-client.ts index b348055..e9f2715 100644 --- a/src/lib/realtime-voice-client.ts +++ b/src/lib/realtime-voice-client.ts @@ -46,6 +46,13 @@ export interface RealtimeRelayToolCall { args?: unknown; } +export interface RealtimeRelayToolCallResult { + delegated?: boolean; + runId?: string; + finalText?: string; + result?: unknown; +} + export async function startRealtimeVoiceSession( request: RealtimeVoiceSessionRequest, ): Promise { @@ -114,11 +121,17 @@ export async function sendRealtimeRelayToolResult( }); } -export async function sendRealtimeRelayToolCall(runtimeId: string, toolCall: RealtimeRelayToolCall): Promise { - await postRealtimeRelay(runtimeId, { +export async function sendRealtimeRelayToolCall( + runtimeId: string, + toolCall: RealtimeRelayToolCall, +): Promise { + const data = await postRealtimeRelay(runtimeId, { action: "toolCall", ...toolCall, }); + return data.result && typeof data.result === "object" + ? data.result as RealtimeRelayToolCallResult + : {}; } export async function stopRealtimeRelay(runtimeId: string, relaySessionId: string): Promise { @@ -145,6 +158,7 @@ async function postRealtimeRelay(runtimeId: string, body: Record ({})) as { result?: unknown }; } async function readRealtimeVoiceError(response: Response, fallback: string) { diff --git a/src/lib/realtime-voice-gateway-relay.ts b/src/lib/realtime-voice-gateway-relay.ts index ab4b563..59a5239 100644 --- a/src/lib/realtime-voice-gateway-relay.ts +++ b/src/lib/realtime-voice-gateway-relay.ts @@ -97,6 +97,7 @@ export class RealtimeGatewayRelaySession { private cancelRequestedForPlayback = false; private speechFramesDuringPlayback = 0; private outputStartedAtMs: number | null = null; + private pendingToolCalls = 0; private readonly bargeInProfile = resolveRealtimeBargeInProfile(); constructor( @@ -229,6 +230,7 @@ export class RealtimeGatewayRelaySession { return; case "transcript": if (event.role && event.text) { + if (event.role === "assistant" && this.pendingToolCalls > 0) return; this.callbacks.onTranscript?.({ role: event.role, text: event.text, @@ -299,20 +301,34 @@ export class RealtimeGatewayRelaySession { const name = event.name; this.callbacks.onStatus?.("processing", "Consulting OpenClaw"); - void sendRealtimeRelayToolCall(this.runtimeId, { - relaySessionId, - sessionKey, - callId, - name, - args: event.args ?? {}, - }).catch((error) => { - const message = error instanceof Error ? error.message : String(error); - this.callbacks.onError?.(message); - void sendRealtimeRelayToolResult(this.runtimeId, relaySessionId, callId, { - error: message, - name, - }).catch(() => {}); - }); + this.pendingToolCalls += 1; + void (async () => { + try { + const result = await sendRealtimeRelayToolCall(this.runtimeId, { + relaySessionId, + sessionKey, + callId, + name, + args: event.args ?? {}, + }); + if (result.finalText?.trim()) { + this.callbacks.onTranscript?.({ + role: "assistant", + text: result.finalText.trim(), + final: true, + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.callbacks.onError?.(message); + void sendRealtimeRelayToolResult(this.runtimeId, relaySessionId, callId, { + error: message, + name, + }).catch(() => {}); + } finally { + this.pendingToolCalls = Math.max(0, this.pendingToolCalls - 1); + } + })(); } private cancelOutputForBargeIn(): void {