fix: harden Google Live tool responses (#72426) (thanks @BsnizND)

This commit is contained in:
Peter Steinberger
2026-04-27 09:45:03 +01:00
parent 409e762810
commit edbab0e2db
3 changed files with 60 additions and 13 deletions

View File

@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: clear queued Gemini Live playback when realtime interruptions arrive, restart Chrome command-pair audio output after clears, and expose Google Live interruption/VAD config knobs for Meet and Voice Call realtime bridges. Fixes #72523. (#72524) Thanks @BsnizND.
- Google Meet: add `realtime.agentId` so live meeting consults can target a named OpenClaw agent instead of always using `main`. (#72381) Thanks @BsnizND.
- Google Meet: route stateful `google_meet` tool actions through the gateway-owned runtime so created or joined realtime sessions remain visible to status, speak, and leave after the agent turn ends. Fixes #72440. (#72441) Thanks @BsnizND.
- Google Meet: preserve Gemini Live function names when replying to realtime tool calls so Google SDK validation accepts the `FunctionResponse` payload. Fixes #72425. (#72426) Thanks @BsnizND.
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
- Cron: apply `cron.maxConcurrentRuns` to a dedicated `cron-nested` isolated agent-turn lane as well as cron dispatch, so parallel cron jobs no longer serialize on inner LLM execution while non-cron nested flows keep their existing lane behavior. Fixes #72707. Thanks @kagura-agent.
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.

View File

@@ -414,4 +414,44 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
}),
);
});
it("reports Google Live tool response send failures without losing the call name", async () => {
const provider = buildGoogleRealtimeVoiceProvider();
const onError = vi.fn();
const bridge = provider.createBridge({
providerConfig: { apiKey: "gemini-key" },
onAudio: vi.fn(),
onClearAudio: vi.fn(),
onError,
});
await bridge.connect();
lastConnectParams().callbacks.onmessage({
setupComplete: { sessionId: "session-1" },
toolCall: {
functionCalls: [{ id: "call-1", name: "lookup", args: { query: "hi" } }],
},
});
const sendError = new Error("SDK send failed");
session.sendToolResponse.mockImplementationOnce(() => {
throw sendError;
});
bridge.submitToolResult("call-1", ["retryable"]);
expect(onError).toHaveBeenCalledWith(sendError);
bridge.submitToolResult("call-1", { result: "ok" });
expect(session.sendToolResponse).toHaveBeenLastCalledWith({
functionResponses: [
{
id: "call-1",
name: "lookup",
response: { result: "ok" },
},
],
});
});
});

View File

@@ -461,19 +461,25 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
);
return;
}
this.pendingFunctionNames.delete(callId);
this.session.sendToolResponse({
functionResponses: [
{
id: callId,
name,
response:
result && typeof result === "object"
? (result as Record<string, unknown>)
: { output: result },
},
],
});
try {
this.session.sendToolResponse({
functionResponses: [
{
id: callId,
name,
response:
result && typeof result === "object" && !Array.isArray(result)
? (result as Record<string, unknown>)
: { output: result },
},
],
});
this.pendingFunctionNames.delete(callId);
} catch (error) {
this.config.onError?.(
error instanceof Error ? error : new Error("Failed to send Google Live function response"),
);
}
}
acknowledgeMark(): void {}