From 344ee3782d0aac18a7ab9ff833dd35bde5175940 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 03:57:11 +0100 Subject: [PATCH] fix(google-meet): guide timeout recovery --- CHANGELOG.md | 1 + extensions/google-meet/index.test.ts | 4 +++- extensions/google-meet/index.ts | 5 ++-- scripts/run-node.mjs | 14 +++++++++++- src/infra/run-node.test.ts | 34 ++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c525c707f..3dfd96f711e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index 5f92d0c9a5b..8c2e7ca20c4 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -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"] }, diff --git a/extensions/google-meet/index.ts b/extensions/google-meet/index.ts index 943176a6440..165ea45f895 100644 --- a/extensions/google-meet/index.ts +++ b/extensions/google-meet/index.ts @@ -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); diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index ac266cc718c..f2594f6484b 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -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 { diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index f37b28b671d..c97781d47c3 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -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, {