Skip to content

Commit f94d970

Browse files
committed
fix: refresh Google Meet speech retry readiness
1 parent cab86dc commit f94d970

7 files changed

Lines changed: 299 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
3232
### Fixes
3333

3434
- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.
35+
- Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose `speechReady` diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf.
3536
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
3637
- Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.
3738
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.

docs/plugins/google-meet.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,10 @@ health such as `inCall`, `manualActionRequired`, `providerConnected`,
166166
timestamps, byte counters, and bridge closed state. If a safe Meet page prompt
167167
appears, browser automation handles it when it can. Login, host admission, and
168168
browser/OS permission prompts are reported as manual action with a reason and
169-
message for the agent to relay.
169+
message for the agent to relay. Managed Chrome sessions only emit the intro or
170+
test phrase after browser health reports `inCall: true`; otherwise status reports
171+
`speechReady: false` and the speech attempt is blocked instead of pretending the
172+
agent spoke into the meeting.
170173

171174
Local Chrome joins through the signed-in OpenClaw browser profile. Realtime mode
172175
requires `BlackHole 2ch` for the microphone/speaker path used by OpenClaw. For
@@ -1006,6 +1009,9 @@ a session ended.
10061009
- `manualActionRequired` / `manualActionReason` / `manualActionMessage`: the
10071010
browser profile needs manual login, Meet host admission, permissions, or
10081011
browser-control repair before speech can work
1012+
- `speechReady` / `speechBlockedReason` / `speechBlockedMessage`: whether
1013+
managed Chrome speech is allowed now. `speechReady: false` means OpenClaw did
1014+
not send the intro/test phrase into the audio bridge.
10091015
- `providerConnected` / `realtimeReady`: realtime voice bridge state
10101016
- `lastInputAt` / `lastOutputAt`: last audio seen from or sent to the bridge
10111017

extensions/google-meet/index.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,6 +1998,9 @@ describe("google-meet plugin", () => {
19981998
details: {
19991999
manualActionRequired?: boolean;
20002000
manualActionReason?: string;
2001+
speechReady?: boolean;
2002+
speechBlockedReason?: string;
2003+
spoken?: boolean;
20012004
session?: { chrome?: { health?: { manualActionRequired?: boolean } } };
20022005
};
20032006
}>;
@@ -2012,17 +2015,157 @@ describe("google-meet plugin", () => {
20122015
expect(result.details).toMatchObject({
20132016
manualActionRequired: true,
20142017
manualActionReason: "google-login-required",
2018+
spoken: false,
2019+
speechReady: false,
2020+
speechBlockedReason: "google-login-required",
20152021
session: {
20162022
chrome: {
20172023
health: {
20182024
manualActionRequired: true,
20192025
manualActionReason: "google-login-required",
2026+
speechReady: false,
2027+
speechBlockedReason: "google-login-required",
20202028
},
20212029
},
20222030
},
20232031
});
20242032
});
20252033

2034+
it("refreshes browser health before blocking an explicit speech retry", async () => {
2035+
let openedTab = false;
2036+
let browserReady = false;
2037+
const { methods, nodesInvoke } = setup(
2038+
{
2039+
defaultTransport: "chrome-node",
2040+
defaultMode: "realtime",
2041+
},
2042+
{
2043+
nodesInvokeHandler: async ({ command, params }) => {
2044+
const raw = params as { path?: string; body?: { url?: string; targetId?: string } };
2045+
if (command === "browser.proxy") {
2046+
if (raw.path === "/tabs") {
2047+
return {
2048+
payload: {
2049+
result: {
2050+
running: true,
2051+
tabs: openedTab
2052+
? [
2053+
{
2054+
targetId: "tab-1",
2055+
title: "Meet",
2056+
url: "https://meet.google.com/abc-defg-hij",
2057+
},
2058+
]
2059+
: [],
2060+
},
2061+
},
2062+
};
2063+
}
2064+
if (raw.path === "/tabs/open") {
2065+
openedTab = true;
2066+
return {
2067+
payload: {
2068+
result: {
2069+
targetId: "tab-1",
2070+
title: "Meet",
2071+
url: raw.body?.url ?? "https://meet.google.com/abc-defg-hij",
2072+
},
2073+
},
2074+
};
2075+
}
2076+
if (raw.path === "/tabs/focus" || raw.path === "/permissions/grant") {
2077+
return { payload: { result: { ok: true } } };
2078+
}
2079+
if (raw.path === "/act") {
2080+
return {
2081+
payload: {
2082+
result: {
2083+
ok: true,
2084+
targetId: raw.body?.targetId ?? "tab-1",
2085+
result: JSON.stringify(
2086+
browserReady
2087+
? {
2088+
inCall: true,
2089+
micMuted: false,
2090+
manualActionRequired: false,
2091+
title: "Meet call",
2092+
url: "https://meet.google.com/abc-defg-hij",
2093+
}
2094+
: {
2095+
inCall: false,
2096+
manualActionRequired: true,
2097+
manualActionReason: "google-login-required",
2098+
manualActionMessage:
2099+
"Sign in to Google in the OpenClaw browser profile, then retry the Meet join.",
2100+
title: "Sign in - Google Accounts",
2101+
url: "https://accounts.google.com/signin",
2102+
},
2103+
),
2104+
},
2105+
},
2106+
};
2107+
}
2108+
}
2109+
if (command === "googlemeet.chrome") {
2110+
return { payload: { launched: true } };
2111+
}
2112+
throw new Error(`unexpected invoke ${command}`);
2113+
},
2114+
},
2115+
);
2116+
2117+
const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", {
2118+
url: "https://meet.google.com/abc-defg-hij",
2119+
message: "Say exactly: hello.",
2120+
})) as {
2121+
session: { id: string; chrome?: { health?: { speechBlockedReason?: string } } };
2122+
spoken: boolean;
2123+
};
2124+
expect(join.spoken).toBe(false);
2125+
expect(join.session.chrome?.health?.speechBlockedReason).toBe("google-login-required");
2126+
2127+
browserReady = true;
2128+
const retry = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.speak", {
2129+
sessionId: join.session.id,
2130+
message: "Say exactly: hello again.",
2131+
})) as {
2132+
found: boolean;
2133+
spoken: boolean;
2134+
session?: {
2135+
chrome?: {
2136+
health?: {
2137+
inCall?: boolean;
2138+
manualActionRequired?: boolean;
2139+
speechBlockedReason?: string;
2140+
};
2141+
};
2142+
};
2143+
};
2144+
2145+
expect(retry).toMatchObject({
2146+
found: true,
2147+
spoken: false,
2148+
session: {
2149+
chrome: {
2150+
health: {
2151+
inCall: true,
2152+
manualActionRequired: false,
2153+
speechBlockedReason: "audio-bridge-unavailable",
2154+
},
2155+
},
2156+
},
2157+
});
2158+
expect(nodesInvoke).toHaveBeenCalledWith(
2159+
expect.objectContaining({
2160+
command: "browser.proxy",
2161+
params: expect.objectContaining({
2162+
path: "/tabs/focus",
2163+
body: { targetId: "tab-1" },
2164+
}),
2165+
}),
2166+
);
2167+
});
2168+
20262169
it("explains when chrome-node has no capable paired node", async () => {
20272170
const { tools } = setup(
20282171
{

extensions/google-meet/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ export default definePluginEntry({
823823
return;
824824
}
825825
const rt = await ensureRuntime();
826-
respond(true, rt.speak(sessionId, normalizeOptionalString(params?.message)));
826+
respond(true, await rt.speak(sessionId, normalizeOptionalString(params?.message)));
827827
} catch (err) {
828828
sendError(respond, err);
829829
}

extensions/google-meet/src/cli.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ function writeDoctorStatus(status: ReturnType<GoogleMeetRuntime["status"]>): voi
268268
writeStdoutLine("manual reason: %s", formatOptional(health.manualActionReason));
269269
writeStdoutLine("manual message: %s", formatOptional(health.manualActionMessage));
270270
}
271+
writeStdoutLine("speech ready: %s", formatBoolean(health?.speechReady));
272+
if (health?.speechReady === false) {
273+
writeStdoutLine("speech blocked reason: %s", formatOptional(health.speechBlockedReason));
274+
writeStdoutLine("speech blocked message: %s", formatOptional(health.speechBlockedMessage));
275+
}
271276
writeStdoutLine("provider connected: %s", formatBoolean(health?.providerConnected));
272277
writeStdoutLine("realtime ready: %s", formatBoolean(health?.realtimeReady));
273278
writeStdoutLine("audio input active: %s", formatBoolean(health?.audioInputActive));
@@ -2017,12 +2022,15 @@ export function registerGoogleMeetCli(params: {
20172022
.argument("[message]", "Realtime instructions to speak now")
20182023
.action(async (sessionId: string, message?: string) => {
20192024
const rt = await params.ensureRuntime();
2020-
const result = rt.speak(sessionId, message);
2025+
const result = await rt.speak(sessionId, message);
20212026
if (!result.found) {
20222027
throw new Error("session not found");
20232028
}
20242029
if (!result.spoken) {
2025-
throw new Error("session has no active realtime audio bridge");
2030+
throw new Error(
2031+
result.session?.chrome?.health?.speechBlockedMessage ??
2032+
"session has no active realtime audio bridge",
2033+
);
20262034
}
20272035
writeStdoutLine("speaking on %s", sessionId);
20282036
});

0 commit comments

Comments
 (0)