fix(google-meet): guide timeout recovery

This commit is contained in:
Peter Steinberger
2026-04-25 03:57:11 +01:00
parent 37c2450124
commit 344ee3782d
5 changed files with 54 additions and 4 deletions

View File

@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
- Media tools: honor the configured web-fetch SSRF policy for media understanding, image/music/video generation references, and PDF inputs, so explicit RFC2544 opt-ins cover WebChat OSS uploads without weakening defaults. Fixes #71300. (#71321) Thanks @neeravmakwana.
- Agents/TTS: suppress successful spoken transcripts from verbose chat tool output when structured voice media is already queued, while preserving text output for non-builtin tool-name collisions. Fixes #71282. Thanks @neeravmakwana.
- Plugins/Google Meet: reuse existing Meet tabs and active sessions across harmless URL query differences, avoiding duplicate Chrome windows when agents retry a join. Thanks @steipete.
- Plugins/Google Meet: tell agents to recover already-open Meet tabs after browser timeouts, and make the dev CLI release its build lock if compiler spawning fails. Thanks @steipete.
- Gateway/sessions: recover main-agent turns interrupted by a gateway restart from stale transcript-lock evidence, avoiding stuck `status: "running"` sessions without broad post-boot transcript scans. Fixes #70555. Thanks @bitloi.
- Codex approvals: keep command approval responses within Codex app-server `availableDecisions`, including deny/cancel fallbacks for prompts that do not offer `decline`. (#71338) Thanks @Lucenx9.
- Codex harness: reject same-thread app-server notifications without `turnId` or `turn.id` after a bound turn starts, preventing unscoped events from mutating or completing the active reply. (#71317) Thanks @Lucenx9.

View File

@@ -202,8 +202,9 @@ describe("google-meet plugin", () => {
it("uses a provider-safe flat tool parameter schema", () => {
const { tools } = setup();
const tool = tools[0] as { parameters: unknown };
const tool = tools[0] as { description?: string; parameters: unknown };
expect(tool.description).toContain("recover_current_tab");
expect(JSON.stringify(tool.parameters)).not.toContain("anyOf");
expect(tool.parameters).toMatchObject({
type: "object",
@@ -222,6 +223,7 @@ describe("google-meet plugin", () => {
"speak",
"test_speech",
],
description: expect.stringContaining("recover_current_tab"),
},
transport: { type: "string", enum: ["chrome", "chrome-node", "twilio"] },
mode: { type: "string", enum: ["realtime", "transcribe"] },

View File

@@ -150,7 +150,7 @@ const GoogleMeetToolSchema = Type.Object({
"test_speech",
],
description:
"Google Meet action to run. create creates a meeting and joins it by default; pass join=false to only mint a meeting URL.",
"Google Meet action to run. create creates and joins by default; pass join=false to only mint a URL. After a timeout or unclear browser state, call recover_current_tab before retrying join.",
}),
join: Type.Optional(
Type.Boolean({
@@ -391,7 +391,8 @@ export default definePluginEntry({
api.registerTool({
name: "google_meet",
label: "Google Meet",
description: "Join and track Google Meet sessions through Chrome or Twilio.",
description:
"Join and track Google Meet sessions through Chrome or Twilio. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
parameters: GoogleMeetToolSchema,
async execute(_toolCallId, params) {
const raw = asParamRecord(params);

View File

@@ -535,8 +535,20 @@ const waitForSpawnedProcess = async (childProcess, deps) => {
try {
return await new Promise((resolve) => {
let settled = false;
const settle = (res) => {
if (settled) {
return;
}
settled = true;
resolve(res);
};
childProcess.on("error", (error) => {
logRunner(`Spawn failed: ${error?.message ?? String(error)}`, deps);
settle({ exitCode: 1, exitSignal: null, forwardedSignal });
});
childProcess.on("exit", (exitCode, exitSignal) => {
resolve({ exitCode, exitSignal, forwardedSignal });
settle({ exitCode, exitSignal, forwardedSignal });
});
});
} finally {

View File

@@ -807,6 +807,40 @@ describe("run-node script", () => {
});
});
it("returns failure and releases the build lock when the compiler spawn errors", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
const spawn = (cmd: string, args: string[] = []) => {
if (cmd === process.execPath && args[0] === "scripts/tsdown-build.mjs") {
const events = new EventEmitter();
queueMicrotask(() => events.emit("error", new Error("spawn failed")));
return {
on: (event: string, cb: (code: number | null, signal: string | null) => void) => {
events.on(event, cb);
return undefined;
},
};
}
return createExitedProcess(0);
};
const exitCode = await runNodeMain({
cwd: tmp,
args: ["status"],
env: {
...process.env,
OPENCLAW_FORCE_BUILD: "1",
OPENCLAW_RUNNER_LOG: "0",
},
spawn,
execPath: process.execPath,
platform: process.platform,
});
expect(exitCode).toBe(1);
expect(fsSync.existsSync(path.join(tmp, ".artifacts", "run-node-build.lock"))).toBe(false);
});
});
it("forwards wrapper SIGTERM to the active openclaw child and returns 143", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {