@openclaw/google-meet 2026.7.1 → 2026.7.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/{chrome-create-BhgM0jQO.js → chrome-create-DM1XJCOH.js} +87 -61
  2. package/dist/{cli-D364Yrym.js → cli-SodPSOFX.js} +47 -90
  3. package/dist/{config-DDYRr7-P.js → config-BeoHH_z5.js} +49 -13
  4. package/dist/{create-CkkaRlbI.js → create-CWcFI0ub.js} +2 -2
  5. package/dist/index.js +1236 -361
  6. package/dist/{oauth-75iPDzIn.js → oauth-DOfMKlGp.js} +3 -1
  7. package/node_modules/core-util-is/LICENSE +19 -0
  8. package/node_modules/core-util-is/README.md +3 -0
  9. package/node_modules/core-util-is/lib/util.js +107 -0
  10. package/node_modules/core-util-is/package.json +38 -0
  11. package/node_modules/immediate/LICENSE.txt +20 -0
  12. package/node_modules/immediate/README.md +93 -0
  13. package/node_modules/immediate/dist/immediate.js +75 -0
  14. package/node_modules/immediate/dist/immediate.min.js +1 -0
  15. package/node_modules/immediate/lib/browser.js +69 -0
  16. package/node_modules/immediate/lib/index.js +73 -0
  17. package/node_modules/immediate/package.json +42 -0
  18. package/node_modules/inherits/LICENSE +16 -0
  19. package/node_modules/inherits/README.md +42 -0
  20. package/node_modules/inherits/inherits.js +9 -0
  21. package/node_modules/inherits/inherits_browser.js +27 -0
  22. package/node_modules/inherits/package.json +29 -0
  23. package/node_modules/isarray/.npmignore +1 -0
  24. package/node_modules/isarray/.travis.yml +4 -0
  25. package/node_modules/isarray/Makefile +6 -0
  26. package/node_modules/isarray/README.md +60 -0
  27. package/node_modules/isarray/component.json +19 -0
  28. package/node_modules/isarray/index.js +5 -0
  29. package/node_modules/isarray/package.json +45 -0
  30. package/node_modules/isarray/test.js +20 -0
  31. package/node_modules/jszip/.codeclimate.yml +16 -0
  32. package/node_modules/jszip/.editorconfig +8 -0
  33. package/node_modules/jszip/.eslintrc.js +43 -0
  34. package/node_modules/jszip/.github/workflows/pr.yaml +58 -0
  35. package/node_modules/jszip/.jekyll-metadata +0 -0
  36. package/node_modules/jszip/.travis.yml +17 -0
  37. package/node_modules/jszip/CHANGES.md +204 -0
  38. package/node_modules/jszip/LICENSE.markdown +651 -0
  39. package/node_modules/jszip/README.markdown +33 -0
  40. package/node_modules/jszip/deps.js +37 -0
  41. package/node_modules/jszip/dist/jszip.js +11577 -0
  42. package/node_modules/jszip/dist/jszip.min.js +13 -0
  43. package/node_modules/jszip/graph.svg +601 -0
  44. package/node_modules/jszip/index.d.ts +330 -0
  45. package/node_modules/jszip/lib/base64.js +106 -0
  46. package/node_modules/jszip/lib/compressedObject.js +74 -0
  47. package/node_modules/jszip/lib/compressions.js +14 -0
  48. package/node_modules/jszip/lib/crc32.js +77 -0
  49. package/node_modules/jszip/lib/defaults.js +11 -0
  50. package/node_modules/jszip/lib/external.js +18 -0
  51. package/node_modules/jszip/lib/flate.js +85 -0
  52. package/node_modules/jszip/lib/generate/ZipFileWorker.js +539 -0
  53. package/node_modules/jszip/lib/generate/index.js +57 -0
  54. package/node_modules/jszip/lib/index.js +55 -0
  55. package/node_modules/jszip/lib/license_header.js +11 -0
  56. package/node_modules/jszip/lib/load.js +88 -0
  57. package/node_modules/jszip/lib/nodejs/NodejsStreamInputAdapter.js +74 -0
  58. package/node_modules/jszip/lib/nodejs/NodejsStreamOutputAdapter.js +42 -0
  59. package/node_modules/jszip/lib/nodejsUtils.js +57 -0
  60. package/node_modules/jszip/lib/object.js +384 -0
  61. package/node_modules/jszip/lib/readable-stream-browser.js +10 -0
  62. package/node_modules/jszip/lib/reader/ArrayReader.js +57 -0
  63. package/node_modules/jszip/lib/reader/DataReader.js +116 -0
  64. package/node_modules/jszip/lib/reader/NodeBufferReader.js +19 -0
  65. package/node_modules/jszip/lib/reader/StringReader.js +38 -0
  66. package/node_modules/jszip/lib/reader/Uint8ArrayReader.js +22 -0
  67. package/node_modules/jszip/lib/reader/readerFor.js +28 -0
  68. package/node_modules/jszip/lib/signature.js +7 -0
  69. package/node_modules/jszip/lib/stream/ConvertWorker.js +26 -0
  70. package/node_modules/jszip/lib/stream/Crc32Probe.js +24 -0
  71. package/node_modules/jszip/lib/stream/DataLengthProbe.js +29 -0
  72. package/node_modules/jszip/lib/stream/DataWorker.js +116 -0
  73. package/node_modules/jszip/lib/stream/GenericWorker.js +263 -0
  74. package/node_modules/jszip/lib/stream/StreamHelper.js +214 -0
  75. package/node_modules/jszip/lib/support.js +38 -0
  76. package/node_modules/jszip/lib/utf8.js +275 -0
  77. package/node_modules/jszip/lib/utils.js +501 -0
  78. package/node_modules/jszip/lib/zipEntries.js +261 -0
  79. package/node_modules/jszip/lib/zipEntry.js +293 -0
  80. package/node_modules/jszip/lib/zipObject.js +133 -0
  81. package/node_modules/jszip/package.json +67 -0
  82. package/node_modules/jszip/sponsors.md +21 -0
  83. package/node_modules/jszip/tsconfig.json +101 -0
  84. package/node_modules/jszip/vendor/FileSaver.js +247 -0
  85. package/node_modules/lie/README.md +62 -0
  86. package/node_modules/lie/dist/lie.js +350 -0
  87. package/node_modules/lie/dist/lie.min.js +1 -0
  88. package/node_modules/lie/dist/lie.polyfill.js +358 -0
  89. package/node_modules/lie/dist/lie.polyfill.min.js +1 -0
  90. package/node_modules/lie/lib/browser.js +273 -0
  91. package/node_modules/lie/lib/index.js +298 -0
  92. package/node_modules/lie/license.md +7 -0
  93. package/node_modules/lie/lie.d.ts +244 -0
  94. package/node_modules/lie/package.json +69 -0
  95. package/node_modules/lie/polyfill.js +4 -0
  96. package/node_modules/pako/CHANGELOG.md +164 -0
  97. package/node_modules/pako/LICENSE +21 -0
  98. package/node_modules/pako/README.md +191 -0
  99. package/node_modules/pako/dist/pako.js +6818 -0
  100. package/node_modules/pako/dist/pako.min.js +1 -0
  101. package/node_modules/pako/dist/pako_deflate.js +3997 -0
  102. package/node_modules/pako/dist/pako_deflate.min.js +1 -0
  103. package/node_modules/pako/dist/pako_inflate.js +3300 -0
  104. package/node_modules/pako/dist/pako_inflate.min.js +1 -0
  105. package/node_modules/pako/index.js +14 -0
  106. package/node_modules/pako/lib/deflate.js +400 -0
  107. package/node_modules/pako/lib/inflate.js +423 -0
  108. package/node_modules/pako/lib/utils/common.js +105 -0
  109. package/node_modules/pako/lib/utils/strings.js +187 -0
  110. package/node_modules/pako/lib/zlib/README +59 -0
  111. package/node_modules/pako/lib/zlib/adler32.js +51 -0
  112. package/node_modules/pako/lib/zlib/constants.js +68 -0
  113. package/node_modules/pako/lib/zlib/crc32.js +59 -0
  114. package/node_modules/pako/lib/zlib/deflate.js +1874 -0
  115. package/node_modules/pako/lib/zlib/gzheader.js +58 -0
  116. package/node_modules/pako/lib/zlib/inffast.js +345 -0
  117. package/node_modules/pako/lib/zlib/inflate.js +1556 -0
  118. package/node_modules/pako/lib/zlib/inftrees.js +343 -0
  119. package/node_modules/pako/lib/zlib/messages.js +32 -0
  120. package/node_modules/pako/lib/zlib/trees.js +1222 -0
  121. package/node_modules/pako/lib/zlib/zstream.js +47 -0
  122. package/node_modules/pako/package.json +44 -0
  123. package/node_modules/parse-ms/index.d.ts +30 -0
  124. package/node_modules/parse-ms/index.js +45 -0
  125. package/node_modules/parse-ms/license +9 -0
  126. package/node_modules/parse-ms/package.json +47 -0
  127. package/node_modules/parse-ms/readme.md +46 -0
  128. package/node_modules/pretty-ms/index.d.ts +157 -0
  129. package/node_modules/pretty-ms/index.js +149 -0
  130. package/node_modules/pretty-ms/license +9 -0
  131. package/node_modules/pretty-ms/package.json +55 -0
  132. package/node_modules/pretty-ms/readme.md +179 -0
  133. package/node_modules/process-nextick-args/index.js +45 -0
  134. package/node_modules/process-nextick-args/license.md +19 -0
  135. package/node_modules/process-nextick-args/package.json +25 -0
  136. package/node_modules/process-nextick-args/readme.md +18 -0
  137. package/node_modules/readable-stream/.travis.yml +34 -0
  138. package/node_modules/readable-stream/CONTRIBUTING.md +38 -0
  139. package/node_modules/readable-stream/GOVERNANCE.md +136 -0
  140. package/node_modules/readable-stream/LICENSE +47 -0
  141. package/node_modules/readable-stream/README.md +58 -0
  142. package/node_modules/readable-stream/doc/wg-meetings/2015-01-30.md +60 -0
  143. package/node_modules/readable-stream/duplex-browser.js +1 -0
  144. package/node_modules/readable-stream/duplex.js +1 -0
  145. package/node_modules/readable-stream/lib/_stream_duplex.js +131 -0
  146. package/node_modules/readable-stream/lib/_stream_passthrough.js +47 -0
  147. package/node_modules/readable-stream/lib/_stream_readable.js +1019 -0
  148. package/node_modules/readable-stream/lib/_stream_transform.js +214 -0
  149. package/node_modules/readable-stream/lib/_stream_writable.js +685 -0
  150. package/node_modules/readable-stream/lib/internal/streams/BufferList.js +78 -0
  151. package/node_modules/readable-stream/lib/internal/streams/destroy.js +84 -0
  152. package/node_modules/readable-stream/lib/internal/streams/stream-browser.js +1 -0
  153. package/node_modules/readable-stream/lib/internal/streams/stream.js +1 -0
  154. package/node_modules/readable-stream/package.json +52 -0
  155. package/node_modules/readable-stream/passthrough.js +1 -0
  156. package/node_modules/readable-stream/readable-browser.js +7 -0
  157. package/node_modules/readable-stream/readable.js +19 -0
  158. package/node_modules/readable-stream/transform.js +1 -0
  159. package/node_modules/readable-stream/writable-browser.js +1 -0
  160. package/node_modules/readable-stream/writable.js +8 -0
  161. package/node_modules/safe-buffer/LICENSE +21 -0
  162. package/node_modules/safe-buffer/README.md +584 -0
  163. package/node_modules/safe-buffer/index.d.ts +187 -0
  164. package/node_modules/safe-buffer/index.js +62 -0
  165. package/node_modules/safe-buffer/package.json +37 -0
  166. package/node_modules/{commander/LICENSE → setimmediate/LICENSE.txt} +8 -10
  167. package/node_modules/setimmediate/package.json +30 -0
  168. package/node_modules/setimmediate/setImmediate.js +186 -0
  169. package/node_modules/string_decoder/.travis.yml +50 -0
  170. package/node_modules/string_decoder/LICENSE +48 -0
  171. package/node_modules/string_decoder/README.md +47 -0
  172. package/node_modules/string_decoder/lib/string_decoder.js +296 -0
  173. package/node_modules/string_decoder/package.json +31 -0
  174. package/node_modules/util-deprecate/History.md +16 -0
  175. package/node_modules/util-deprecate/LICENSE +24 -0
  176. package/node_modules/util-deprecate/README.md +53 -0
  177. package/node_modules/util-deprecate/browser.js +67 -0
  178. package/node_modules/util-deprecate/node.js +6 -0
  179. package/node_modules/util-deprecate/package.json +27 -0
  180. package/npm-shrinkwrap.json +127 -9
  181. package/openclaw.plugin.json +1 -1
  182. package/package.json +8 -6
  183. package/node_modules/commander/Readme.md +0 -1172
  184. package/node_modules/commander/index.js +0 -21
  185. package/node_modules/commander/lib/argument.js +0 -147
  186. package/node_modules/commander/lib/command.js +0 -2790
  187. package/node_modules/commander/lib/error.js +0 -36
  188. package/node_modules/commander/lib/help.js +0 -731
  189. package/node_modules/commander/lib/option.js +0 -377
  190. package/node_modules/commander/lib/suggestSimilar.js +0 -99
  191. package/node_modules/commander/package-support.json +0 -19
  192. package/node_modules/commander/package.json +0 -64
  193. package/node_modules/commander/typings/index.d.ts +0 -1113
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { a as buildGoogleMeetCalendarDayWindow, i as resolveGoogleMeetGatewayOperationTimeoutMs, n as DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND, o as findGoogleMeetCalendarEvent, r as resolveGoogleMeetConfig, s as listGoogleMeetCalendarEvents, t as DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND } from "./config-DDYRr7-P.js";
2
- import { _ as fetchLatestGoogleMeetConferenceRecord, a as forceMeetEnglishUi, c as readBrowserTab, d as buildGoogleMeetPreflightReport, g as fetchGoogleMeetSpace, h as fetchGoogleMeetAttendance, i as callBrowserProxyOnNode, l as resolveChromeNode, m as fetchGoogleMeetArtifacts, n as isGoogleMeetBrowserManualActionError, o as isSameMeetUrlForReuse, p as endGoogleMeetActiveConference, r as asBrowserTabs, s as normalizeMeetUrlForReuse, t as createMeetWithBrowserProxyOnNode, u as resolveChromeNodeInfo } from "./chrome-create-BhgM0jQO.js";
1
+ import { a as buildGoogleMeetCalendarDayWindow, c as normalizeMeetUrl, i as resolveGoogleMeetGatewayOperationTimeoutMs, n as DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND, o as findGoogleMeetCalendarEvent, r as resolveGoogleMeetConfig, s as listGoogleMeetCalendarEvents, t as DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND } from "./config-BeoHH_z5.js";
2
+ import { _ as fetchGoogleMeetSpace, a as forceMeetEnglishUi, c as normalizeMeetUrlForReuse, d as resolveChromeNodeInfo, f as buildGoogleMeetPreflightReport, g as fetchGoogleMeetAttendance, h as fetchGoogleMeetArtifacts, i as callBrowserProxyOnNode, l as readBrowserTab, m as endGoogleMeetActiveConference, n as isGoogleMeetBrowserManualActionError, o as isEnglishMeetTab, r as asBrowserTabs, s as isSameMeetUrlForReuse, t as createMeetWithBrowserProxyOnNode, u as resolveChromeNode, v as fetchLatestGoogleMeetConferenceRecord } from "./chrome-create-DM1XJCOH.js";
3
3
  import { optionalPositiveIntegerSchema, readPositiveIntegerParam } from "openclaw/plugin-sdk/channel-actions";
4
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
5
  import { ErrorCodes, GatewayClient, callGatewayFromCli, errorShape, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
6
6
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
7
7
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
8
- import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
8
+ import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
9
9
  import { asRecord, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
10
10
  import { jsonResult } from "openclaw/plugin-sdk/tool-results";
11
11
  import { Type } from "typebox";
@@ -20,6 +20,7 @@ import fs from "node:fs";
20
20
  import os from "node:os";
21
21
  import path from "node:path";
22
22
  import { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } from "openclaw/plugin-sdk/realtime-transcription";
23
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
23
24
  //#region extensions/google-meet/src/transports/chrome-audio-device.ts
24
25
  const GOOGLE_MEET_SYSTEM_PROFILER_COMMAND = "/usr/sbin/system_profiler";
25
26
  function outputMentionsBlackHole2ch(output) {
@@ -77,23 +78,21 @@ function wake(session) {
77
78
  for (const waiter of waiters) waiter();
78
79
  }
79
80
  function stopSession(session) {
80
- const wasClosed = session.closed;
81
+ if (session.closed) return;
81
82
  session.closed = true;
82
- session.closedAt ??= (/* @__PURE__ */ new Date()).toISOString();
83
+ session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
83
84
  terminateChild(session.input);
84
85
  terminateChild(session.output);
85
- if (!wasClosed) wake(session);
86
+ wake(session);
86
87
  }
87
88
  function attachOutputProcessHandlers(session, outputProcess) {
88
- outputProcess.on("exit", () => {
89
+ const stopIfCurrent = () => {
89
90
  if (session.output === outputProcess) stopSession(session);
90
- });
91
- outputProcess.on("error", () => {
92
- if (session.output === outputProcess) stopSession(session);
93
- });
94
- outputProcess.stdin?.on?.("error", () => {
95
- if (session.output === outputProcess) stopSession(session);
96
- });
91
+ };
92
+ outputProcess.on("exit", stopIfCurrent);
93
+ outputProcess.on("error", stopIfCurrent);
94
+ outputProcess.stdin?.on("error", stopIfCurrent);
95
+ outputProcess.stderr?.on("error", stopIfCurrent);
97
96
  }
98
97
  function startOutputProcess(command) {
99
98
  return spawn(command.command, command.args, { stdio: [
@@ -134,9 +133,12 @@ function startCommandPair(params) {
134
133
  if (session.chunks.length > 200) session.chunks.splice(0, session.chunks.length - 200);
135
134
  wake(session);
136
135
  });
137
- inputProcess.on("exit", () => stopSession(session));
136
+ const stop = () => stopSession(session);
137
+ inputProcess.on("exit", stop);
138
+ inputProcess.on("error", stop);
139
+ inputProcess.stdout?.on("error", stop);
140
+ inputProcess.stderr?.on("error", stop);
138
141
  attachOutputProcessHandlers(session, outputProcess);
139
- inputProcess.on("error", () => stopSession(session));
140
142
  sessions.set(session.id, session);
141
143
  return session;
142
144
  }
@@ -211,8 +213,7 @@ function clearAudio(params) {
211
213
  };
212
214
  }
213
215
  function startChrome(params) {
214
- const url = readString$2(params.url);
215
- if (!url) throw new Error("url required");
216
+ const url = normalizeMeetUrl(params.url);
216
217
  const timeoutMs = readNumber(params.joinTimeoutMs, 3e4);
217
218
  const mode = readString$2(params.mode);
218
219
  let bridgeId;
@@ -399,6 +400,179 @@ async function handleGoogleMeetNodeHostCommand(paramsJSON) {
399
400
  return JSON.stringify(result);
400
401
  }
401
402
  //#endregion
403
+ //#region extensions/google-meet/src/node-invoke-policy.ts
404
+ const GOOGLE_MEET_CHROME_NODE_COMMAND = "googlemeet.chrome";
405
+ const START_MODES = /* @__PURE__ */ new Set([
406
+ "agent",
407
+ "bidi",
408
+ "realtime",
409
+ "transcribe"
410
+ ]);
411
+ function asRecord$2(value) {
412
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
413
+ }
414
+ function readString$1(value) {
415
+ return typeof value === "string" && value.length > 0 ? value : void 0;
416
+ }
417
+ function readPositiveNumber(value) {
418
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
419
+ }
420
+ function copyCommand(command) {
421
+ return command && command.length > 0 ? [...command] : void 0;
422
+ }
423
+ function denied(message, code = "GOOGLE_MEET_NODE_POLICY_DENIED") {
424
+ return {
425
+ ok: false,
426
+ code,
427
+ message
428
+ };
429
+ }
430
+ function approved(params) {
431
+ return {
432
+ approved: true,
433
+ params
434
+ };
435
+ }
436
+ function buildStartParams(params, config) {
437
+ let url;
438
+ try {
439
+ url = normalizeMeetUrl(params.url);
440
+ } catch (error) {
441
+ return {
442
+ approved: false,
443
+ result: denied(error instanceof Error ? error.message : "googlemeet.chrome start requires url")
444
+ };
445
+ }
446
+ const mode = readString$1(params.mode);
447
+ if (mode && !START_MODES.has(mode)) return {
448
+ approved: false,
449
+ result: denied(`googlemeet.chrome start mode is unsupported: ${mode}`)
450
+ };
451
+ const startParams = {
452
+ action: "start",
453
+ url,
454
+ launch: params.launch === false ? false : config.chrome.launch,
455
+ browserProfile: config.chrome.browserProfile,
456
+ joinTimeoutMs: config.chrome.joinTimeoutMs
457
+ };
458
+ if (mode) startParams.mode = mode;
459
+ const audioInputCommand = copyCommand(config.chrome.audioInputCommand);
460
+ if (audioInputCommand) startParams.audioInputCommand = audioInputCommand;
461
+ const audioOutputCommand = copyCommand(config.chrome.audioOutputCommand);
462
+ if (audioOutputCommand) startParams.audioOutputCommand = audioOutputCommand;
463
+ const audioBridgeCommand = copyCommand(config.chrome.audioBridgeCommand);
464
+ if (audioBridgeCommand) startParams.audioBridgeCommand = audioBridgeCommand;
465
+ const audioBridgeHealthCommand = copyCommand(config.chrome.audioBridgeHealthCommand);
466
+ if (audioBridgeHealthCommand) startParams.audioBridgeHealthCommand = audioBridgeHealthCommand;
467
+ return approved(startParams);
468
+ }
469
+ function denyMissing(action, field) {
470
+ return {
471
+ approved: false,
472
+ result: denied(`googlemeet.chrome ${action} requires ${field}`)
473
+ };
474
+ }
475
+ function buildForwardParams(params) {
476
+ const action = readString$1(params.action);
477
+ switch (action) {
478
+ case "setup": return approved({ action });
479
+ case "status": {
480
+ const bridgeId = readString$1(params.bridgeId);
481
+ return approved(bridgeId ? {
482
+ action,
483
+ bridgeId
484
+ } : { action });
485
+ }
486
+ case "list": {
487
+ const forwarded = { action };
488
+ const url = readString$1(params.url);
489
+ const mode = readString$1(params.mode);
490
+ if (url) try {
491
+ forwarded.url = normalizeMeetUrl(url);
492
+ } catch (error) {
493
+ return {
494
+ approved: false,
495
+ result: denied(error instanceof Error ? error.message : "googlemeet.chrome list url")
496
+ };
497
+ }
498
+ if (mode) forwarded.mode = mode;
499
+ return approved(forwarded);
500
+ }
501
+ case "stopByUrl": {
502
+ const forwarded = { action };
503
+ const url = readString$1(params.url);
504
+ const mode = readString$1(params.mode);
505
+ const exceptBridgeId = readString$1(params.exceptBridgeId);
506
+ if (!url) return denyMissing(action, "url");
507
+ try {
508
+ forwarded.url = normalizeMeetUrl(url);
509
+ } catch (error) {
510
+ return {
511
+ approved: false,
512
+ result: denied(error instanceof Error ? error.message : "googlemeet.chrome stopByUrl url")
513
+ };
514
+ }
515
+ if (mode) forwarded.mode = mode;
516
+ if (exceptBridgeId) forwarded.exceptBridgeId = exceptBridgeId;
517
+ return approved(forwarded);
518
+ }
519
+ case "pullAudio": {
520
+ const forwarded = { action };
521
+ const bridgeId = readString$1(params.bridgeId);
522
+ const timeoutMs = readPositiveNumber(params.timeoutMs);
523
+ if (!bridgeId) return denyMissing(action, "bridgeId");
524
+ forwarded.bridgeId = bridgeId;
525
+ if (timeoutMs) forwarded.timeoutMs = timeoutMs;
526
+ return approved(forwarded);
527
+ }
528
+ case "pushAudio": {
529
+ const forwarded = { action };
530
+ const bridgeId = readString$1(params.bridgeId);
531
+ const base64 = readString$1(params.base64);
532
+ if (!bridgeId) return denyMissing(action, "bridgeId");
533
+ if (!base64) return denyMissing(action, "base64");
534
+ forwarded.bridgeId = bridgeId;
535
+ forwarded.base64 = base64;
536
+ return approved(forwarded);
537
+ }
538
+ case "clearAudio": {
539
+ const bridgeId = readString$1(params.bridgeId);
540
+ if (!bridgeId) return denyMissing(action, "bridgeId");
541
+ return approved({
542
+ action,
543
+ bridgeId
544
+ });
545
+ }
546
+ case "stop": {
547
+ const bridgeId = readString$1(params.bridgeId);
548
+ return approved(bridgeId ? {
549
+ action,
550
+ bridgeId
551
+ } : { action });
552
+ }
553
+ default: return null;
554
+ }
555
+ }
556
+ function createGoogleMeetChromeNodeInvokePolicy(config) {
557
+ return {
558
+ commands: [GOOGLE_MEET_CHROME_NODE_COMMAND],
559
+ dangerous: true,
560
+ async handle(ctx) {
561
+ if (ctx.command !== "googlemeet.chrome") return denied(`unsupported Google Meet node command: ${ctx.command}`);
562
+ const params = asRecord$2(ctx.params);
563
+ const action = readString$1(params.action);
564
+ let decision;
565
+ if (action === "start") decision = buildStartParams(params, config);
566
+ else decision = buildForwardParams(params) ?? {
567
+ approved: false,
568
+ result: denied("unsupported googlemeet.chrome action")
569
+ };
570
+ if (!decision.approved) return decision.result;
571
+ return await ctx.invokeNode({ params: decision.params });
572
+ }
573
+ };
574
+ }
575
+ //#endregion
402
576
  //#region extensions/google-meet/src/setup.ts
403
577
  function resolveUserPath(input) {
404
578
  if (input === "~") return os.homedir();
@@ -555,9 +729,9 @@ const GOOGLE_MEET_CONSULT_SYSTEM_PROMPT = [
555
729
  function resolveGoogleMeetRealtimeTools(policy) {
556
730
  return resolveRealtimeVoiceAgentConsultTools(policy);
557
731
  }
558
- function submitGoogleMeetConsultWorkingResponse(session, callId) {
732
+ async function submitGoogleMeetConsultWorkingResponse(session, callId) {
559
733
  if (!session.bridge.supportsToolResultContinuation) return;
560
- session.submitToolResult(callId, buildRealtimeVoiceAgentConsultWorkingResponse("participant"), { willContinue: true });
734
+ await session.submitToolResult(callId, buildRealtimeVoiceAgentConsultWorkingResponse("participant"), { willContinue: true });
561
735
  }
562
736
  async function consultOpenClawAgentForGoogleMeet(params) {
563
737
  const agentId = normalizeAgentId(params.config.realtime.agentId);
@@ -584,34 +758,37 @@ async function consultOpenClawAgentForGoogleMeet(params) {
584
758
  extraSystemPrompt: GOOGLE_MEET_CONSULT_SYSTEM_PROMPT
585
759
  });
586
760
  }
587
- function handleGoogleMeetRealtimeConsultToolCall(params) {
761
+ async function handleGoogleMeetRealtimeConsultToolCall(params) {
588
762
  const callId = params.event.callId || params.event.itemId;
589
763
  if (params.strategy !== "bidi") {
764
+ const error = `Tool "${params.event.name}" is only available in bidi realtime strategy`;
765
+ await params.session.submitToolResult(callId, { error });
590
766
  params.onTalkEvent?.({
591
767
  type: "tool.error",
592
768
  callId,
593
769
  payload: {
594
770
  name: params.event.name,
595
- error: `Tool "${params.event.name}" is only available in bidi realtime strategy`
771
+ error
596
772
  },
597
773
  final: true
598
774
  });
599
- params.session.submitToolResult(callId, { error: `Tool "${params.event.name}" is only available in bidi realtime strategy` });
600
775
  return;
601
776
  }
602
777
  if (params.event.name !== REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
778
+ const error = `Tool "${params.event.name}" not available`;
779
+ await params.session.submitToolResult(callId, { error });
603
780
  params.onTalkEvent?.({
604
781
  type: "tool.error",
605
782
  callId,
606
783
  payload: {
607
784
  name: params.event.name,
608
- error: `Tool "${params.event.name}" not available`
785
+ error
609
786
  },
610
787
  final: true
611
788
  });
612
- params.session.submitToolResult(callId, { error: `Tool "${params.event.name}" not available` });
613
789
  return;
614
790
  }
791
+ await submitGoogleMeetConsultWorkingResponse(params.session, callId);
615
792
  params.onTalkEvent?.({
616
793
  type: "tool.progress",
617
794
  callId,
@@ -620,38 +797,41 @@ function handleGoogleMeetRealtimeConsultToolCall(params) {
620
797
  status: "working"
621
798
  }
622
799
  });
623
- submitGoogleMeetConsultWorkingResponse(params.session, callId);
624
- consultOpenClawAgentForGoogleMeet({
625
- config: params.config,
626
- fullConfig: params.fullConfig,
627
- runtime: params.runtime,
628
- logger: params.logger,
629
- meetingSessionId: params.meetingSessionId,
630
- requesterSessionKey: params.requesterSessionKey,
631
- args: params.event.args,
632
- transcript: params.transcript
633
- }).then((result) => {
634
- params.onTalkEvent?.({
635
- type: "tool.result",
636
- callId,
637
- payload: {
638
- name: params.event.name,
639
- result
640
- },
641
- final: true
800
+ let result;
801
+ try {
802
+ result = await consultOpenClawAgentForGoogleMeet({
803
+ config: params.config,
804
+ fullConfig: params.fullConfig,
805
+ runtime: params.runtime,
806
+ logger: params.logger,
807
+ meetingSessionId: params.meetingSessionId,
808
+ requesterSessionKey: params.requesterSessionKey,
809
+ args: params.event.args,
810
+ transcript: params.transcript
642
811
  });
643
- params.session.submitToolResult(callId, result);
644
- }).catch((error) => {
812
+ } catch (error) {
813
+ const message = formatErrorMessage(error);
814
+ await params.session.submitToolResult(callId, { error: message });
645
815
  params.onTalkEvent?.({
646
816
  type: "tool.error",
647
817
  callId,
648
818
  payload: {
649
819
  name: params.event.name,
650
- error: formatErrorMessage(error)
820
+ error: message
651
821
  },
652
822
  final: true
653
823
  });
654
- params.session.submitToolResult(callId, { error: formatErrorMessage(error) });
824
+ return;
825
+ }
826
+ await params.session.submitToolResult(callId, result);
827
+ params.onTalkEvent?.({
828
+ type: "tool.result",
829
+ callId,
830
+ payload: {
831
+ name: params.event.name,
832
+ result
833
+ },
834
+ final: true
655
835
  });
656
836
  }
657
837
  //#endregion
@@ -814,7 +994,7 @@ function readLogString(value) {
814
994
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
815
995
  }
816
996
  function formatLogValue(value) {
817
- return value?.replace(/\s+/g, "_").slice(0, 180) || "unknown";
997
+ return (value ? truncateUtf16Safe(value.replace(/\s+/g, "_"), 180) : void 0) || "unknown";
818
998
  }
819
999
  function resolveProviderModelForLog(params) {
820
1000
  return readLogString(params.providerConfig.model) ?? readLogString(params.providerConfig.modelId) ?? readLogString(params.fallbackModel) ?? readLogString(params.provider.defaultModel) ?? "provider-default";
@@ -1499,7 +1679,7 @@ async function startCommandRealtimeAudioBridge(params) {
1499
1679
  }
1500
1680
  });
1501
1681
  const turnId = ensureTalkTurn();
1502
- handleGoogleMeetRealtimeConsultToolCall({
1682
+ return handleGoogleMeetRealtimeConsultToolCall({
1503
1683
  strategy,
1504
1684
  session,
1505
1685
  event,
@@ -1592,10 +1772,10 @@ async function startCommandRealtimeAudioBridge(params) {
1592
1772
  }
1593
1773
  //#endregion
1594
1774
  //#region extensions/google-meet/src/realtime-node.ts
1595
- function asRecord$2(value) {
1775
+ function asRecord$1(value) {
1596
1776
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1597
1777
  }
1598
- function readString$1(value) {
1778
+ function readString(value) {
1599
1779
  return typeof value === "string" && value.trim() ? value : void 0;
1600
1780
  }
1601
1781
  function normalizeGoogleMeetTtsPromptText(text) {
@@ -1626,10 +1806,10 @@ function startGoogleMeetNodeAudioInputLoop(params) {
1626
1806
  },
1627
1807
  timeoutMs: 2e3
1628
1808
  });
1629
- const result = asRecord$2(asRecord$2(raw).payload ?? raw);
1809
+ const result = asRecord$1(asRecord$1(raw).payload ?? raw);
1630
1810
  consecutiveInputErrors = 0;
1631
1811
  lastInputError = void 0;
1632
- const base64 = readString$1(result.base64);
1812
+ const base64 = readString(result.base64);
1633
1813
  if (base64) {
1634
1814
  const audio = Buffer.from(base64, "base64");
1635
1815
  if (params.isInputSuppressed()) {
@@ -2084,7 +2264,7 @@ async function startNodeRealtimeAudioBridge(params) {
2084
2264
  }
2085
2265
  });
2086
2266
  const turnId = ensureTalkTurn();
2087
- handleGoogleMeetRealtimeConsultToolCall({
2267
+ return handleGoogleMeetRealtimeConsultToolCall({
2088
2268
  strategy,
2089
2269
  session,
2090
2270
  event,
@@ -2173,11 +2353,22 @@ async function startNodeRealtimeAudioBridge(params) {
2173
2353
  };
2174
2354
  }
2175
2355
  //#endregion
2356
+ //#region extensions/google-meet/src/transports/types.ts
2357
+ const GOOGLE_MEET_TRANSCRIPT_MAX_LINES = 2e3;
2358
+ //#endregion
2176
2359
  //#region extensions/google-meet/src/transports/chrome.ts
2177
- const chromeTransportDeps = { callGatewayFromCli };
2360
+ const GOOGLE_MEET_CAPTION_SETTLE_MS = 1e3;
2178
2361
  function isGoogleMeetTalkBackMode$2(mode) {
2179
2362
  return mode === "agent" || mode === "bidi";
2180
2363
  }
2364
+ function readMeetAuthUser(url) {
2365
+ if (!url) return;
2366
+ try {
2367
+ return new URL(url).searchParams.get("authuser") ?? void 0;
2368
+ } catch {
2369
+ return;
2370
+ }
2371
+ }
2181
2372
  async function assertBlackHole2chAvailable(params) {
2182
2373
  if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
2183
2374
  const result = await params.runtime.system.runCommandWithTimeout([GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"], { timeoutMs: params.timeoutMs });
@@ -2250,9 +2441,10 @@ async function launchChromeMeet(params) {
2250
2441
  audioBridge: await startRealtimeAudioBridge()
2251
2442
  };
2252
2443
  const result = await openMeetWithBrowserRequest({
2253
- callBrowser: callLocalBrowserRequest,
2444
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
2254
2445
  config: params.config,
2255
2446
  mode: params.mode,
2447
+ meetingSessionId: params.meetingSessionId,
2256
2448
  url: params.url
2257
2449
  });
2258
2450
  const audioBridge = isGoogleMeetTalkBackMode$2(params.mode) && result.browser?.inCall === true && result.browser.micMuted !== true && result.browser.manualActionRequired !== true ? await startRealtimeAudioBridge() : void 0;
@@ -2300,7 +2492,7 @@ function parseMeetBrowserStatus(result) {
2300
2492
  };
2301
2493
  }
2302
2494
  async function callLocalBrowserRequest(params) {
2303
- return await chromeTransportDeps.callGatewayFromCli("browser.request", {
2495
+ return await callGatewayFromCli("browser.request", {
2304
2496
  json: true,
2305
2497
  timeout: String(resolveBrowserGatewayTimeoutMs(params.timeoutMs))
2306
2498
  }, {
@@ -2310,6 +2502,18 @@ async function callLocalBrowserRequest(params) {
2310
2502
  timeoutMs: params.timeoutMs
2311
2503
  }, { progress: false });
2312
2504
  }
2505
+ async function resolveLocalBrowserRequest(runtime) {
2506
+ if (!await runtime.gateway.isAvailable()) return callLocalBrowserRequest;
2507
+ return async (params) => await runtime.gateway.request("browser.request", {
2508
+ method: params.method,
2509
+ path: params.path,
2510
+ body: params.body,
2511
+ timeoutMs: params.timeoutMs
2512
+ }, {
2513
+ timeoutMs: resolveBrowserGatewayTimeoutMs(params.timeoutMs),
2514
+ scopes: ["operator.admin"]
2515
+ });
2516
+ }
2313
2517
  function resolveBrowserGatewayTimeoutMs(timeoutMs) {
2314
2518
  return addTimerTimeoutGraceMs(timeoutMs) ?? 1;
2315
2519
  }
@@ -2350,6 +2554,7 @@ function meetStatusScript(params) {
2350
2554
  return `async () => {
2351
2555
  const text = (node) => (node?.innerText || node?.textContent || "").trim();
2352
2556
  const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
2557
+ const captionSessionId = ${JSON.stringify(params.captionSessionId)};
2353
2558
  const captureCaptions = ${JSON.stringify(params.captureCaptions)};
2354
2559
  const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
2355
2560
  const buttons = [...document.querySelectorAll('button')];
@@ -2405,8 +2610,9 @@ function meetStatusScript(params) {
2405
2610
  mic.click();
2406
2611
  notes.push("Muted Meet microphone for observe-only mode.");
2407
2612
  }
2613
+ const joinElsewhere = findButton(/join here too/i);
2408
2614
  const join = !readOnly && ${JSON.stringify(params.autoJoin)}
2409
- ? findButton(/join now|ask to join|join here too/i)
2615
+ ? findButton(/join now|ask to join/i)
2410
2616
  : null;
2411
2617
  if (join) join.click();
2412
2618
  const microphoneChoice = findButton(/\\buse microphone\\b/i);
@@ -2480,42 +2686,127 @@ function meetStatusScript(params) {
2480
2686
  if (!captureCaptions) return undefined;
2481
2687
  const w = window;
2482
2688
  if (!inCall && !w.__openclawMeetCaptions) return undefined;
2483
- if (!w.__openclawMeetCaptions) {
2689
+ // A reused tab starts a fresh logical transcript for each OpenClaw session.
2690
+ // Status refreshes omit the id, so they preserve the active page-owned buffer.
2691
+ if (!w.__openclawMeetCaptions || (captionSessionId && w.__openclawMeetCaptions.sessionId !== captionSessionId)) {
2692
+ if (w.__openclawMeetCaptions?.settleTimer !== undefined) {
2693
+ clearTimeout(w.__openclawMeetCaptions.settleTimer);
2694
+ }
2695
+ w.__openclawMeetCaptions?.observer?.disconnect?.();
2484
2696
  w.__openclawMeetCaptions = {
2697
+ sessionId: captionSessionId,
2698
+ // Epochs cross document lifetimes in the runtime transcript cursor.
2699
+ // Strong UUIDs keep a reloaded page distinct from its prior buffer.
2700
+ epoch: crypto.randomUUID(),
2485
2701
  enabledAttempted: false,
2486
2702
  observerInstalled: false,
2703
+ observer: undefined,
2704
+ droppedLines: 0,
2487
2705
  lines: [],
2488
- seen: {}
2706
+ settleTimer: undefined,
2707
+ visible: []
2489
2708
  };
2490
2709
  }
2491
2710
  return w.__openclawMeetCaptions;
2492
2711
  })();
2493
- const recordCaption = (speaker, captionText) => {
2712
+ const normalizeCaption = (speaker, captionText) => {
2494
2713
  if (!captionState) return;
2495
2714
  const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
2496
2715
  const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
2497
- if (!clean || clean.length < 2) return;
2498
- if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return;
2499
- const key = (cleanSpeaker + "\\n" + clean).toLowerCase();
2500
- if (captionState.seen[key]) return;
2501
- captionState.seen[key] = true;
2502
- const entry = { at: new Date().toISOString(), speaker: cleanSpeaker || undefined, text: clean };
2503
- captionState.lines.push(entry);
2504
- if (captionState.lines.length > 50) captionState.lines.splice(0, captionState.lines.length - 50);
2716
+ if (!clean || clean.length < 2) return undefined;
2717
+ if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return undefined;
2718
+ return { speaker: cleanSpeaker || undefined, text: clean };
2719
+ };
2720
+ const commitLines = (state, entries) => {
2721
+ state.lines.push(...entries.map((entry) => ({
2722
+ at: entry.at,
2723
+ speaker: entry.speaker,
2724
+ text: entry.text
2725
+ })));
2726
+ const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
2727
+ if (excess > 0) {
2728
+ state.lines.splice(0, excess);
2729
+ state.droppedLines = (state.droppedLines || 0) + excess;
2730
+ }
2505
2731
  };
2506
2732
  const scrapeCaptions = () => {
2507
2733
  if (!captionState) return;
2508
2734
  const regions = [...document.querySelectorAll(captionSelector)];
2735
+ const rows = [];
2509
2736
  for (const region of regions) {
2510
2737
  const raw = text(region);
2511
2738
  if (!raw) continue;
2512
2739
  const pieces = raw.split(/\\n+/).map((part) => part.trim()).filter(Boolean);
2513
- if (pieces.length >= 2) {
2514
- recordCaption(pieces[0], pieces.slice(1).join(" "));
2515
- } else {
2516
- recordCaption("", pieces[0] || raw);
2740
+ const row = pieces.length >= 2
2741
+ ? normalizeCaption(pieces[0], pieces.slice(1).join(" "))
2742
+ : normalizeCaption("", pieces[0] || raw);
2743
+ if (row) rows.push({ ...row, node: region });
2744
+ }
2745
+ if (rows.length === 0) {
2746
+ // Meet briefly removes caption rows while rerendering. Keep them mutable
2747
+ // for one settle window so a DOM gap cannot fabricate a repeated line.
2748
+ if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
2749
+ const pendingState = captionState;
2750
+ pendingState.settleTimer = setTimeout(() => {
2751
+ if (window.__openclawMeetCaptions !== pendingState) return;
2752
+ commitLines(pendingState, pendingState.visible);
2753
+ pendingState.visible = [];
2754
+ pendingState.settleTimer = undefined;
2755
+ }, ${GOOGLE_MEET_CAPTION_SETTLE_MS});
2517
2756
  }
2757
+ return;
2518
2758
  }
2759
+ if (captionState.settleTimer !== undefined) {
2760
+ clearTimeout(captionState.settleTimer);
2761
+ captionState.settleTimer = undefined;
2762
+ }
2763
+ const previous = Array.isArray(captionState.visible) ? captionState.visible : [];
2764
+ const unmatchedPrevious = [...previous];
2765
+ const nextVisible = [];
2766
+ const now = Date.now();
2767
+ for (let index = 0; index < rows.length; index += 1) {
2768
+ const row = rows[index];
2769
+ const priorIndex = unmatchedPrevious.findIndex((candidate) => {
2770
+ const sameTextLifecycle =
2771
+ candidate.text === row.text ||
2772
+ row.text.startsWith(candidate.text) ||
2773
+ candidate.text.startsWith(row.text);
2774
+ const sameDomLifecycle =
2775
+ candidate.node === row.node || now - candidate.seenAt <= ${GOOGLE_MEET_CAPTION_SETTLE_MS};
2776
+ return candidate.speaker === row.speaker && sameTextLifecycle && sameDomLifecycle;
2777
+ });
2778
+ const prior = priorIndex >= 0 ? unmatchedPrevious.splice(priorIndex, 1)[0] : undefined;
2779
+ const sameSpeaker = Boolean(prior) && prior.speaker === row.speaker;
2780
+ if (sameSpeaker && prior.text === row.text) {
2781
+ prior.node = row.node;
2782
+ prior.seenAt = now;
2783
+ nextVisible.push(prior);
2784
+ continue;
2785
+ }
2786
+ if (sameSpeaker && row.text.startsWith(prior.text)) {
2787
+ prior.text = row.text;
2788
+ prior.node = row.node;
2789
+ prior.seenAt = now;
2790
+ nextVisible.push(prior);
2791
+ continue;
2792
+ }
2793
+ if (sameSpeaker && prior.text.startsWith(row.text)) {
2794
+ prior.node = row.node;
2795
+ prior.seenAt = now;
2796
+ nextVisible.push(prior);
2797
+ continue;
2798
+ }
2799
+ const entry = {
2800
+ at: new Date().toISOString(),
2801
+ node: row.node,
2802
+ seenAt: now,
2803
+ speaker: row.speaker,
2804
+ text: row.text
2805
+ };
2806
+ nextVisible.push(entry);
2807
+ }
2808
+ commitLines(captionState, unmatchedPrevious);
2809
+ captionState.visible = nextVisible;
2519
2810
  };
2520
2811
  if (captionState) {
2521
2812
  if (!readOnly && inCall && !captionState.enabledAttempted) {
@@ -2534,7 +2825,8 @@ function meetStatusScript(params) {
2534
2825
  }
2535
2826
  if (inCall && !captionState.observerInstalled) {
2536
2827
  captionState.observerInstalled = true;
2537
- new MutationObserver(scrapeCaptions).observe(document.body, {
2828
+ captionState.observer = new MutationObserver(scrapeCaptions);
2829
+ captionState.observer.observe(document.body, {
2538
2830
  childList: true,
2539
2831
  subtree: true,
2540
2832
  characterData: true
@@ -2544,17 +2836,19 @@ function meetStatusScript(params) {
2544
2836
  if (inCall) {
2545
2837
  scrapeCaptions();
2546
2838
  }
2547
- const lines = Array.isArray(captionState.lines) ? captionState.lines : [];
2839
+ const committedLines = Array.isArray(captionState.lines) ? captionState.lines : [];
2840
+ const visibleLines = Array.isArray(captionState.visible) ? captionState.visible : [];
2841
+ const lines = [...committedLines, ...visibleLines];
2548
2842
  const last = lines[lines.length - 1];
2549
2843
  captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
2550
- transcriptLines = lines.length;
2844
+ transcriptLines = (captionState.droppedLines || 0) + lines.length;
2551
2845
  lastCaptionAt = last?.at;
2552
2846
  lastCaptionSpeaker = last?.speaker;
2553
2847
  lastCaptionText = last?.text;
2554
2848
  recentTranscript = lines.slice(-5);
2555
2849
  }
2556
2850
  const lobbyWaiting = !inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText);
2557
- const leaveReason = /you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i.test(pageText)
2851
+ const leaveReason = !inCall && /you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i.test(pageText)
2558
2852
  ? pageText.match(/you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i)?.[0]
2559
2853
  : undefined;
2560
2854
  let manualActionReason;
@@ -2562,6 +2856,9 @@ function meetStatusScript(params) {
2562
2856
  if (!inCall && (host === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText))) {
2563
2857
  manualActionReason = "google-login-required";
2564
2858
  manualActionMessage = "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.";
2859
+ } else if (!inCall && joinElsewhere) {
2860
+ manualActionReason = "meet-session-conflict";
2861
+ manualActionMessage = "Meet is already active in another tab or device. Leave that session or reuse an English-pinned tab before retrying.";
2565
2862
  } else if (!inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText)) {
2566
2863
  manualActionReason = "meet-admission-required";
2567
2864
  manualActionMessage = "Admit the OpenClaw browser participant in Google Meet, then retry speech.";
@@ -2602,63 +2899,404 @@ function meetStatusScript(params) {
2602
2899
  });
2603
2900
  }`;
2604
2901
  }
2605
- async function openMeetWithBrowserProxy(params) {
2606
- return await openMeetWithBrowserRequest({
2607
- callBrowser: async (request) => await callBrowserProxyOnNode({
2608
- runtime: params.runtime,
2609
- nodeId: params.nodeId,
2610
- method: request.method,
2611
- path: request.path,
2612
- body: request.body,
2613
- timeoutMs: request.timeoutMs
2614
- }),
2615
- config: params.config,
2616
- mode: params.mode,
2617
- url: params.url
2618
- });
2902
+ function meetTranscriptScript(meetingUrl, meetingSessionId, finalize) {
2903
+ const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
2904
+ return `() => {
2905
+ const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
2906
+ const expectedSessionId = ${JSON.stringify(meetingSessionId)};
2907
+ let currentMeetingUrl;
2908
+ try {
2909
+ const currentUrl = new URL(location.href);
2910
+ currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
2911
+ } catch {
2912
+ return JSON.stringify({ urlMatched: false });
2913
+ }
2914
+ if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
2915
+ return JSON.stringify({ urlMatched: false });
2916
+ }
2917
+ const state = window.__openclawMeetCaptions;
2918
+ if (state?.sessionId && state.sessionId !== expectedSessionId) {
2919
+ return JSON.stringify({ urlMatched: true, sessionMatched: false });
2920
+ }
2921
+ if (${JSON.stringify(finalize)} && Array.isArray(state?.visible) && state.visible.length > 0) {
2922
+ if (state.settleTimer !== undefined) clearTimeout(state.settleTimer);
2923
+ state.settleTimer = undefined;
2924
+ state.lines = Array.isArray(state.lines) ? state.lines : [];
2925
+ state.lines.push(...state.visible.map((entry) => ({
2926
+ at: entry.at,
2927
+ speaker: entry.speaker,
2928
+ text: entry.text
2929
+ })));
2930
+ state.visible = [];
2931
+ const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
2932
+ if (excess > 0) {
2933
+ state.lines.splice(0, excess);
2934
+ state.droppedLines = (state.droppedLines || 0) + excess;
2935
+ }
2936
+ }
2937
+ const lines = Array.isArray(state?.lines) ? state.lines : [];
2938
+ return JSON.stringify({
2939
+ urlMatched: true,
2940
+ sessionMatched: true,
2941
+ epoch: typeof state?.epoch === "string" ? state.epoch : undefined,
2942
+ droppedLines: Number.isFinite(state?.droppedLines) ? Math.max(0, Math.trunc(state.droppedLines)) : 0,
2943
+ lines: lines.map((line) => ({
2944
+ at: typeof line?.at === "string" ? line.at : undefined,
2945
+ speaker: typeof line?.speaker === "string" ? line.speaker : undefined,
2946
+ text: typeof line?.text === "string" ? line.text : ""
2947
+ })).filter((line) => line.text)
2948
+ });
2949
+ }`;
2619
2950
  }
2620
- async function openMeetWithBrowserRequest(params) {
2621
- if (!params.config.chrome.launch) return { launched: false };
2622
- const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
2623
- let targetId;
2624
- let tab;
2625
- if (params.config.chrome.reuseExistingTab) {
2626
- tab = asBrowserTabs(await params.callBrowser({
2627
- method: "GET",
2628
- path: "/tabs",
2629
- timeoutMs: Math.min(timeoutMs, 5e3)
2630
- })).find((entry) => isSameMeetUrlForReuse(entry.url, params.url));
2631
- targetId = tab?.targetId;
2632
- if (targetId) await params.callBrowser({
2633
- method: "POST",
2634
- path: "/tabs/focus",
2635
- body: { targetId },
2636
- timeoutMs: Math.min(timeoutMs, 5e3)
2637
- });
2638
- }
2639
- if (!targetId) {
2640
- tab = readBrowserTab(await params.callBrowser({
2641
- method: "POST",
2642
- path: "/tabs/open",
2643
- body: { url: forceMeetEnglishUi(params.url) },
2644
- timeoutMs
2645
- }));
2646
- targetId = tab?.targetId;
2951
+ function parseMeetTranscriptSnapshot(result) {
2952
+ const raw = (result && typeof result === "object" ? result : {}).result;
2953
+ if (typeof raw !== "string" || !raw.trim()) return {
2954
+ droppedLines: 0,
2955
+ lines: []
2956
+ };
2957
+ let parsed;
2958
+ try {
2959
+ parsed = JSON.parse(raw);
2960
+ } catch {
2961
+ throw new Error("Google Meet transcript JSON is malformed.");
2647
2962
  }
2648
- if (!targetId) return {
2649
- launched: true,
2650
- browser: {
2651
- status: "browser-control",
2652
- notes: ["Browser proxy opened Meet but did not return a targetId."],
2653
- browserUrl: tab?.url,
2654
- browserTitle: tab?.title
2655
- }
2963
+ if (!parsed || typeof parsed !== "object") throw new Error("Google Meet transcript payload is invalid.");
2964
+ const payload = parsed;
2965
+ const droppedLines = typeof payload.droppedLines === "number" && Number.isSafeInteger(payload.droppedLines) ? Math.max(0, payload.droppedLines) : 0;
2966
+ const lines = Array.isArray(payload.lines) ? payload.lines.flatMap((value) => {
2967
+ if (!value || typeof value !== "object") return [];
2968
+ const line = value;
2969
+ if (typeof line.text !== "string" || !line.text.trim()) return [];
2970
+ return [{
2971
+ ...typeof line.at === "string" ? { at: line.at } : {},
2972
+ ...typeof line.speaker === "string" ? { speaker: line.speaker } : {},
2973
+ text: line.text
2974
+ }];
2975
+ }) : [];
2976
+ return {
2977
+ droppedLines,
2978
+ ...typeof payload.epoch === "string" ? { epoch: payload.epoch } : {},
2979
+ lines,
2980
+ ...typeof payload.urlMatched === "boolean" ? { urlMatched: payload.urlMatched } : {},
2981
+ ...typeof payload.sessionMatched === "boolean" ? { sessionMatched: payload.sessionMatched } : {}
2656
2982
  };
2657
- const permissionNotes = await grantMeetMediaPermissions({
2658
- allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
2659
- callBrowser: params.callBrowser,
2660
- targetId,
2661
- timeoutMs
2983
+ }
2984
+ async function readMeetTranscriptWithBrowserRequest(params) {
2985
+ const snapshot = parseMeetTranscriptSnapshot(await params.callBrowser({
2986
+ method: "POST",
2987
+ path: "/act",
2988
+ body: {
2989
+ kind: "evaluate",
2990
+ targetId: params.tab.targetId,
2991
+ fn: meetTranscriptScript(params.meetingUrl, params.meetingSessionId, params.finalize)
2992
+ },
2993
+ timeoutMs: params.timeoutMs
2994
+ }));
2995
+ if (snapshot.urlMatched === false) throw new Error("The tracked Meet tab no longer shows this session's meeting URL.");
2996
+ if (snapshot.sessionMatched === false) throw new Error("The tracked Meet tab now belongs to another OpenClaw meeting session.");
2997
+ return {
2998
+ droppedLines: snapshot.droppedLines,
2999
+ ...snapshot.epoch ? { epoch: snapshot.epoch } : {},
3000
+ lines: snapshot.lines
3001
+ };
3002
+ }
3003
+ function meetLeaveScript(meetingUrl) {
3004
+ const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
3005
+ return `() => {
3006
+ const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
3007
+ let currentMeetingUrl;
3008
+ try {
3009
+ const currentUrl = new URL(location.href);
3010
+ currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
3011
+ } catch {
3012
+ return JSON.stringify({ departed: false });
3013
+ }
3014
+ if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
3015
+ return JSON.stringify({ departed: true, urlMatched: false });
3016
+ }
3017
+ const text = (node) => (node?.innerText || node?.textContent || "").trim();
3018
+ // Locale-independent fallback: Meet renders the leave control as a Material
3019
+ // Symbols icon whose ligature text is "call_end" in every UI language, so a
3020
+ // localized aria-label (e.g. "Anruf verlassen") still resolves to the button.
3021
+ const hasLeaveIcon = (button) => {
3022
+ const icon = button.querySelector ? button.querySelector("i") : null;
3023
+ return icon ? (icon.textContent || "").trim() === "call_end" : false;
3024
+ };
3025
+ const buttons = [...document.querySelectorAll('button')];
3026
+ const label = (button) => [
3027
+ button.getAttribute("aria-label"),
3028
+ button.getAttribute("data-tooltip"),
3029
+ text(button),
3030
+ ]
3031
+ .filter(Boolean)
3032
+ .join(" ");
3033
+ const postCall = buttons.some((button) => /\\b(rejoin|return to home screen)\\b/i.test(label(button)));
3034
+ if (postCall) {
3035
+ return JSON.stringify({ departed: true, urlMatched: true });
3036
+ }
3037
+ // Managed join tabs are reused only after the English-tab gate or opened
3038
+ // through the English-UI helper, so follow-up labels are pinned to English.
3039
+ const confirmation = buttons.find((button) => {
3040
+ return !button.disabled && /\\bleave meeting\\b/i.test(label(button));
3041
+ });
3042
+ if (confirmation) {
3043
+ confirmation.click();
3044
+ return JSON.stringify({ departed: false, leaveAction: "confirm", urlMatched: true });
3045
+ }
3046
+ const leave = buttons.find((button) => {
3047
+ if (button.disabled) return false;
3048
+ return /leave call/i.test(label(button)) || hasLeaveIcon(button);
3049
+ });
3050
+ if (leave) {
3051
+ leave.click();
3052
+ return JSON.stringify({ departed: false, leaveAction: "leave", urlMatched: true });
3053
+ }
3054
+ return JSON.stringify({ departed: false, urlMatched: true });
3055
+ }`;
3056
+ }
3057
+ function parseMeetLeaveResult(result) {
3058
+ const raw = (result && typeof result === "object" ? result : {}).result;
3059
+ if (typeof raw !== "string" || !raw.trim()) return { departed: false };
3060
+ try {
3061
+ const parsed = JSON.parse(raw);
3062
+ const leaveAction = parsed.leaveAction === "leave" || parsed.leaveAction === "confirm" ? parsed.leaveAction : void 0;
3063
+ return {
3064
+ departed: parsed.departed === true,
3065
+ ...leaveAction ? { leaveAction } : {},
3066
+ ...typeof parsed.urlMatched === "boolean" ? { urlMatched: parsed.urlMatched } : {}
3067
+ };
3068
+ } catch {
3069
+ return { departed: false };
3070
+ }
3071
+ }
3072
+ async function leaveMeetInPage(params) {
3073
+ const deadline = Date.now() + params.timeoutMs;
3074
+ let clickedLeave = false;
3075
+ let clickedConfirmation = false;
3076
+ do {
3077
+ const step = parseMeetLeaveResult(await params.callBrowser({
3078
+ method: "POST",
3079
+ path: "/act",
3080
+ body: {
3081
+ kind: "evaluate",
3082
+ targetId: params.targetId,
3083
+ fn: meetLeaveScript(params.meetingUrl)
3084
+ },
3085
+ timeoutMs: params.timeoutMs
3086
+ }));
3087
+ clickedLeave ||= step.leaveAction === "leave";
3088
+ clickedConfirmation ||= step.leaveAction === "confirm";
3089
+ if (step.departed || step.urlMatched !== true) return {
3090
+ departed: step.departed,
3091
+ clickedLeave,
3092
+ clickedConfirmation,
3093
+ urlMatched: step.urlMatched
3094
+ };
3095
+ if (!step.leaveAction && !clickedLeave) return {
3096
+ departed: false,
3097
+ clickedLeave,
3098
+ clickedConfirmation,
3099
+ urlMatched: true
3100
+ };
3101
+ if (!step.leaveAction) await new Promise((resolve) => {
3102
+ setTimeout(resolve, 100);
3103
+ });
3104
+ } while (Date.now() < deadline);
3105
+ return {
3106
+ departed: false,
3107
+ clickedLeave,
3108
+ clickedConfirmation,
3109
+ urlMatched: true
3110
+ };
3111
+ }
3112
+ async function leaveMeetWithBrowserRequest(params) {
3113
+ if (!params.config.chrome.launch) return {
3114
+ left: false,
3115
+ note: "Browser leave skipped because chrome.launch is disabled."
3116
+ };
3117
+ const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 5e3);
3118
+ const { targetId, openedByPlugin } = params.tab;
3119
+ try {
3120
+ if (!asBrowserTabs(await params.callBrowser({
3121
+ method: "GET",
3122
+ path: "/tabs",
3123
+ timeoutMs
3124
+ })).find((entry) => entry.targetId === targetId)) return {
3125
+ left: true,
3126
+ note: "Meet tab is already closed."
3127
+ };
3128
+ let leaveResult;
3129
+ try {
3130
+ leaveResult = await leaveMeetInPage({
3131
+ callBrowser: params.callBrowser,
3132
+ meetingUrl: params.meetingUrl,
3133
+ targetId,
3134
+ timeoutMs
3135
+ });
3136
+ } catch (error) {
3137
+ return {
3138
+ left: false,
3139
+ note: `Browser control could not verify the Meet tab before leaving: ${error instanceof Error ? error.message : String(error)}`
3140
+ };
3141
+ }
3142
+ if (leaveResult.urlMatched === false) return {
3143
+ left: true,
3144
+ note: "Meet tab moved away from this session; left its current page untouched."
3145
+ };
3146
+ if (leaveResult.urlMatched !== true) return {
3147
+ left: false,
3148
+ note: "Browser control could not verify that the tracked tab still showed this meeting."
3149
+ };
3150
+ const { clickedLeave, departed } = leaveResult;
3151
+ if (!openedByPlugin) return {
3152
+ left: departed,
3153
+ note: departed ? "Clicked Meet's Leave call button; kept the reused browser tab open." : clickedLeave ? "Clicked Meet's Leave call button, but could not verify departure; leave it manually." : "Could not find Meet's Leave call button in the reused browser tab; leave it manually."
3154
+ };
3155
+ await params.callBrowser({
3156
+ method: "DELETE",
3157
+ path: `/tabs/${targetId}`,
3158
+ timeoutMs
3159
+ });
3160
+ return {
3161
+ left: true,
3162
+ note: clickedLeave ? "Clicked Meet's Leave call button and closed the Meet tab." : "Closed the Meet tab to leave the meeting (Leave call button was not found)."
3163
+ };
3164
+ } catch (error) {
3165
+ return {
3166
+ left: false,
3167
+ note: `Browser control could not leave the Meet tab: ${error instanceof Error ? error.message : String(error)}`
3168
+ };
3169
+ }
3170
+ }
3171
+ async function leaveChromeMeet(params) {
3172
+ return await leaveMeetWithBrowserRequest({
3173
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
3174
+ config: params.config,
3175
+ meetingUrl: params.meetingUrl,
3176
+ tab: params.tab
3177
+ });
3178
+ }
3179
+ async function readChromeMeetTranscript(params) {
3180
+ return await readMeetTranscriptWithBrowserRequest({
3181
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
3182
+ finalize: params.finalize === true,
3183
+ meetingUrl: params.meetingUrl,
3184
+ meetingSessionId: params.meetingSessionId,
3185
+ tab: params.tab,
3186
+ timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
3187
+ });
3188
+ }
3189
+ async function readChromeMeetTranscriptOnNode(params) {
3190
+ const nodeId = params.nodeId ?? await resolveChromeNode({
3191
+ runtime: params.runtime,
3192
+ requestedNode: params.config.chromeNode.node
3193
+ });
3194
+ const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
3195
+ return await readMeetTranscriptWithBrowserRequest({
3196
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3197
+ runtime: params.runtime,
3198
+ nodeId,
3199
+ method: request.method,
3200
+ path: request.path,
3201
+ body: request.body,
3202
+ timeoutMs: request.timeoutMs
3203
+ }),
3204
+ finalize: params.finalize === true,
3205
+ meetingUrl: params.meetingUrl,
3206
+ meetingSessionId: params.meetingSessionId,
3207
+ tab: params.tab,
3208
+ timeoutMs
3209
+ });
3210
+ }
3211
+ async function leaveChromeMeetOnNode(params) {
3212
+ const nodeId = params.nodeId ?? await resolveChromeNode({
3213
+ runtime: params.runtime,
3214
+ requestedNode: params.config.chromeNode.node
3215
+ });
3216
+ return await leaveMeetWithBrowserRequest({
3217
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3218
+ runtime: params.runtime,
3219
+ nodeId,
3220
+ method: request.method,
3221
+ path: request.path,
3222
+ body: request.body,
3223
+ timeoutMs: request.timeoutMs
3224
+ }),
3225
+ config: params.config,
3226
+ meetingUrl: params.meetingUrl,
3227
+ tab: params.tab
3228
+ });
3229
+ }
3230
+ async function openMeetWithBrowserProxy(params) {
3231
+ return await openMeetWithBrowserRequest({
3232
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3233
+ runtime: params.runtime,
3234
+ nodeId: params.nodeId,
3235
+ method: request.method,
3236
+ path: request.path,
3237
+ body: request.body,
3238
+ timeoutMs: request.timeoutMs
3239
+ }),
3240
+ config: params.config,
3241
+ mode: params.mode,
3242
+ meetingSessionId: params.meetingSessionId,
3243
+ url: params.url
3244
+ });
3245
+ }
3246
+ async function openMeetWithBrowserRequest(params) {
3247
+ if (!params.config.chrome.launch) return { launched: false };
3248
+ const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
3249
+ let targetId;
3250
+ let tab;
3251
+ let openUrl = params.url;
3252
+ let openedByPlugin = false;
3253
+ if (params.config.chrome.reuseExistingTab) {
3254
+ const matchingTabs = asBrowserTabs(await params.callBrowser({
3255
+ method: "GET",
3256
+ path: "/tabs",
3257
+ timeoutMs: Math.min(timeoutMs, 5e3)
3258
+ })).filter((entry) => isSameMeetUrlForReuse(entry.url, params.url));
3259
+ const requestedAuthUser = readMeetAuthUser(params.url);
3260
+ tab = matchingTabs.find((entry) => isEnglishMeetTab(entry.url) && (!requestedAuthUser || readMeetAuthUser(entry.url) === requestedAuthUser));
3261
+ if (!tab) {
3262
+ if (!new URL(params.url).searchParams.has("authuser")) openUrl = matchingTabs.find((entry) => entry.url)?.url ?? params.url;
3263
+ }
3264
+ targetId = tab?.targetId;
3265
+ if (tab && targetId) await params.callBrowser({
3266
+ method: "POST",
3267
+ path: "/tabs/focus",
3268
+ body: { targetId },
3269
+ timeoutMs: Math.min(timeoutMs, 5e3)
3270
+ });
3271
+ }
3272
+ if (!targetId) {
3273
+ tab = readBrowserTab(await params.callBrowser({
3274
+ method: "POST",
3275
+ path: "/tabs/open",
3276
+ body: { url: forceMeetEnglishUi(openUrl) },
3277
+ timeoutMs
3278
+ }));
3279
+ targetId = tab?.targetId;
3280
+ openedByPlugin = Boolean(targetId);
3281
+ }
3282
+ if (!targetId) return {
3283
+ launched: true,
3284
+ browser: {
3285
+ status: "browser-control",
3286
+ notes: ["Browser proxy opened Meet but did not return a targetId."],
3287
+ browserUrl: tab?.url,
3288
+ browserTitle: tab?.title
3289
+ }
3290
+ };
3291
+ const tabIdentity = {
3292
+ targetId,
3293
+ openedByPlugin
3294
+ };
3295
+ const permissionNotes = await grantMeetMediaPermissions({
3296
+ allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
3297
+ callBrowser: params.callBrowser,
3298
+ targetId,
3299
+ timeoutMs
2662
3300
  });
2663
3301
  const deadline = Date.now() + Math.max(0, params.config.chrome.waitForInCallMs);
2664
3302
  let browser = {
@@ -2677,6 +3315,7 @@ async function openMeetWithBrowserRequest(params) {
2677
3315
  targetId,
2678
3316
  fn: meetStatusScript({
2679
3317
  allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
3318
+ captionSessionId: params.meetingSessionId,
2680
3319
  captureCaptions: params.mode === "transcribe",
2681
3320
  guestName: params.config.chrome.guestName,
2682
3321
  autoJoin: params.config.chrome.autoJoin
@@ -2686,11 +3325,13 @@ async function openMeetWithBrowserRequest(params) {
2686
3325
  })) ?? browser, permissionNotes);
2687
3326
  if (browser?.inCall === true && (!isGoogleMeetTalkBackMode$2(params.mode) || browser.micMuted !== true)) return {
2688
3327
  launched: true,
2689
- browser
3328
+ browser,
3329
+ tab: tabIdentity
2690
3330
  };
2691
3331
  if (browser?.manualActionRequired === true) return {
2692
3332
  launched: true,
2693
- browser
3333
+ browser,
3334
+ tab: tabIdentity
2694
3335
  };
2695
3336
  } catch (error) {
2696
3337
  browser = {
@@ -2710,7 +3351,8 @@ async function openMeetWithBrowserRequest(params) {
2710
3351
  } while (Date.now() < deadline);
2711
3352
  return {
2712
3353
  launched: true,
2713
- browser
3354
+ browser,
3355
+ tab: tabIdentity
2714
3356
  };
2715
3357
  }
2716
3358
  function isRecoverableMeetTab(tab, url) {
@@ -2718,6 +3360,13 @@ function isRecoverableMeetTab(tab, url) {
2718
3360
  if (normalizeMeetUrlForReuse(tab.url)) return true;
2719
3361
  return (tab.url ?? "").startsWith("https://accounts.google.com/") && /sign in|google accounts|meet/i.test(tab.title ?? "");
2720
3362
  }
3363
+ function findRecoverableMeetTab(tabs, url) {
3364
+ const candidates = tabs.filter((tab) => isRecoverableMeetTab(tab, url));
3365
+ if (!url) return candidates[0];
3366
+ const requestedAuthUser = readMeetAuthUser(url);
3367
+ const accountCandidates = requestedAuthUser ? candidates.filter((tab) => readMeetAuthUser(tab.url) === requestedAuthUser) : candidates;
3368
+ return accountCandidates.find((tab) => isEnglishMeetTab(tab.url)) ?? accountCandidates[0];
3369
+ }
2721
3370
  async function inspectRecoverableMeetTab(params) {
2722
3371
  const allowMicrophone = params.mode !== "transcribe";
2723
3372
  await params.callBrowser({
@@ -2726,6 +3375,23 @@ async function inspectRecoverableMeetTab(params) {
2726
3375
  body: { targetId: params.targetId },
2727
3376
  timeoutMs: Math.min(params.timeoutMs, 5e3)
2728
3377
  });
3378
+ if (normalizeMeetUrlForReuse(params.tab.url) && !isEnglishMeetTab(params.tab.url)) {
3379
+ const manualActionMessage = "The existing Meet tab is not pinned to English. Open the meeting with ?hl=en, then retry recovery.";
3380
+ return {
3381
+ found: true,
3382
+ targetId: params.targetId,
3383
+ tab: params.tab,
3384
+ browser: {
3385
+ status: "browser-control",
3386
+ browserUrl: params.tab.url,
3387
+ browserTitle: params.tab.title,
3388
+ manualActionRequired: true,
3389
+ manualActionReason: "meet-locale-required",
3390
+ manualActionMessage
3391
+ },
3392
+ message: manualActionMessage
3393
+ };
3394
+ }
2729
3395
  const permissionNotes = params.readOnly ? [] : await grantMeetMediaPermissions({
2730
3396
  allowMicrophone,
2731
3397
  callBrowser: params.callBrowser,
@@ -2763,11 +3429,12 @@ async function inspectRecoverableMeetTab(params) {
2763
3429
  }
2764
3430
  async function recoverCurrentMeetTab(params) {
2765
3431
  const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
2766
- const tab = asBrowserTabs(await callLocalBrowserRequest({
3432
+ const callBrowser = await resolveLocalBrowserRequest(params.runtime);
3433
+ const tab = findRecoverableMeetTab(asBrowserTabs(await callBrowser({
2767
3434
  method: "GET",
2768
3435
  path: "/tabs",
2769
3436
  timeoutMs: Math.min(timeoutMs, 5e3)
2770
- })).find((entry) => isRecoverableMeetTab(entry, params.url));
3437
+ })), params.url);
2771
3438
  const targetId = tab?.targetId;
2772
3439
  if (!tab || !targetId) return {
2773
3440
  transport: "chrome",
@@ -2778,7 +3445,7 @@ async function recoverCurrentMeetTab(params) {
2778
3445
  return {
2779
3446
  transport: "chrome",
2780
3447
  ...await inspectRecoverableMeetTab({
2781
- callBrowser: callLocalBrowserRequest,
3448
+ callBrowser,
2782
3449
  config: params.config,
2783
3450
  mode: params.mode,
2784
3451
  readOnly: params.readOnly,
@@ -2794,13 +3461,13 @@ async function recoverCurrentMeetTabOnNode(params) {
2794
3461
  requestedNode: params.config.chromeNode.node
2795
3462
  });
2796
3463
  const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
2797
- const tab = asBrowserTabs(await callBrowserProxyOnNode({
3464
+ const tab = findRecoverableMeetTab(asBrowserTabs(await callBrowserProxyOnNode({
2798
3465
  runtime: params.runtime,
2799
3466
  nodeId,
2800
3467
  method: "GET",
2801
3468
  path: "/tabs",
2802
3469
  timeoutMs: Math.min(timeoutMs, 5e3)
2803
- })).find((entry) => isRecoverableMeetTab(entry, params.url));
3470
+ })), params.url);
2804
3471
  const targetId = tab?.targetId;
2805
3472
  if (!tab || !targetId) return {
2806
3473
  transport: "chrome-node",
@@ -2854,6 +3521,7 @@ async function launchChromeMeetOnNode(params) {
2854
3521
  nodeId,
2855
3522
  config: params.config,
2856
3523
  mode: params.mode,
3524
+ meetingSessionId: params.meetingSessionId,
2857
3525
  url: params.url
2858
3526
  });
2859
3527
  const result = parseNodeStartResult(await params.runtime.nodes.invoke({
@@ -2895,19 +3563,22 @@ async function launchChromeMeetOnNode(params) {
2895
3563
  nodeId,
2896
3564
  launched: browserControl.launched || result.launched === true,
2897
3565
  audioBridge: bridge,
2898
- browser: browserControl.browser ?? result.browser
3566
+ browser: browserControl.browser ?? result.browser,
3567
+ tab: browserControl.tab
2899
3568
  };
2900
3569
  }
2901
3570
  if (result.audioBridge?.type === "external-command") return {
2902
3571
  nodeId,
2903
3572
  launched: browserControl.launched || result.launched === true,
2904
3573
  audioBridge: { type: "external-command" },
2905
- browser: browserControl.browser ?? result.browser
3574
+ browser: browserControl.browser ?? result.browser,
3575
+ tab: browserControl.tab
2906
3576
  };
2907
3577
  return {
2908
3578
  nodeId,
2909
3579
  launched: browserControl.launched || result.launched === true,
2910
- browser: browserControl.browser ?? result.browser
3580
+ browser: browserControl.browser ?? result.browser,
3581
+ tab: browserControl.tab
2911
3582
  };
2912
3583
  }
2913
3584
  //#endregion
@@ -3075,28 +3746,33 @@ function nowIso() {
3075
3746
  function buildTwilioVoiceCallSessionKey(meetingSessionId) {
3076
3747
  return `voice:google-meet:${meetingSessionId}`;
3077
3748
  }
3078
- function normalizeMeetUrl(input) {
3079
- const raw = normalizeOptionalString(input);
3080
- if (!raw) throw new Error("url required");
3081
- let url;
3082
- try {
3083
- url = new URL(raw);
3084
- } catch {
3085
- throw new Error("url must be a valid Google Meet URL");
3086
- }
3087
- if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "meet.google.com") throw new Error("url must be an explicit https://meet.google.com/... URL");
3088
- if (!/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i.test(url.pathname)) throw new Error("url must include a Google Meet meeting code");
3089
- return url.toString();
3090
- }
3091
3749
  function resolveTransport(input, config) {
3092
3750
  return input ?? config.defaultTransport;
3093
3751
  }
3094
3752
  function resolveMode(input, config) {
3095
3753
  return input === "realtime" ? "agent" : input ?? config.defaultMode;
3096
3754
  }
3755
+ function resolveSessionAgentId(request, config) {
3756
+ return normalizeAgentId(request.agentId ?? config.realtime.agentId);
3757
+ }
3758
+ function withSessionAgentConfig(config, agentId) {
3759
+ return config.realtime.agentId === agentId ? config : {
3760
+ ...config,
3761
+ realtime: {
3762
+ ...config.realtime,
3763
+ agentId
3764
+ }
3765
+ };
3766
+ }
3097
3767
  function isGoogleMeetTalkBackMode$1(mode) {
3098
3768
  return mode === "agent" || mode === "bidi";
3099
3769
  }
3770
+ function isBrowserTransport(transport) {
3771
+ return transport === "chrome" || transport === "chrome-node";
3772
+ }
3773
+ function isReusableMeetSession(session, params) {
3774
+ return session.state === "active" && isSameMeetUrlForReuse(session.url, params.url) && session.transport === params.transport && session.mode === params.mode && session.agentId === params.agentId;
3775
+ }
3100
3776
  function hasRealtimeAudioOutputAdvanced(health, startOutputBytes) {
3101
3777
  return (health?.lastOutputBytes ?? 0) > startOutputBytes;
3102
3778
  }
@@ -3181,9 +3857,16 @@ async function commandExists(runtime, command) {
3181
3857
  }
3182
3858
  var GoogleMeetRuntime = class {
3183
3859
  #sessions = /* @__PURE__ */ new Map();
3860
+ #sessionLeaves = /* @__PURE__ */ new Map();
3861
+ #browserMeetingLocks = /* @__PURE__ */ new Map();
3862
+ #createdBrowserTabs = /* @__PURE__ */ new Map();
3184
3863
  #sessionStops = /* @__PURE__ */ new Map();
3185
3864
  #sessionSpeakers = /* @__PURE__ */ new Map();
3186
3865
  #sessionHealth = /* @__PURE__ */ new Map();
3866
+ #transcripts = /* @__PURE__ */ new Map();
3867
+ #transcriptCaptures = /* @__PURE__ */ new Map();
3868
+ #transcriptFinalizing = /* @__PURE__ */ new Set();
3869
+ #retiredTranscripts = /* @__PURE__ */ new Set();
3187
3870
  #voiceCallGateway;
3188
3871
  constructor(params) {
3189
3872
  this.params = params;
@@ -3210,6 +3893,28 @@ var GoogleMeetRuntime = class {
3210
3893
  session
3211
3894
  } : { found: false };
3212
3895
  }
3896
+ async transcript(sessionId, options = {}) {
3897
+ const session = this.#sessions.get(sessionId);
3898
+ if (!session) return { found: false };
3899
+ if (session.mode !== "transcribe") throw new Error("transcript is only available for transcribe-mode sessions");
3900
+ const sinceIndex = options.sinceIndex ?? 0;
3901
+ if (!Number.isSafeInteger(sinceIndex) || sinceIndex < 0) throw new Error("sinceIndex must be a non-negative safe integer");
3902
+ if (session.state === "active" && !this.#transcriptFinalizing.has(session.id)) await this.#captureTranscriptSnapshot(session);
3903
+ const snapshot = this.#transcripts.get(sessionId) ?? {
3904
+ droppedLines: 0,
3905
+ lines: []
3906
+ };
3907
+ const startIndex = Math.max(sinceIndex, snapshot.droppedLines);
3908
+ return {
3909
+ found: true,
3910
+ sessionId,
3911
+ startIndex,
3912
+ nextIndex: snapshot.droppedLines + snapshot.lines.length,
3913
+ droppedLines: snapshot.droppedLines,
3914
+ ...session.transcriptEvicted ? { evicted: true } : {},
3915
+ lines: snapshot.lines.slice(startIndex - snapshot.droppedLines)
3916
+ };
3917
+ }
3213
3918
  async setupStatus(options = {}) {
3214
3919
  const transport = resolveTransport(options.transport, this.params.config);
3215
3920
  const mode = resolveMode(options.mode, this.params.config);
@@ -3273,10 +3978,12 @@ var GoogleMeetRuntime = class {
3273
3978
  return status;
3274
3979
  }
3275
3980
  async createViaBrowser() {
3276
- return createMeetWithBrowserProxyOnNode({
3981
+ const result = await createMeetWithBrowserProxyOnNode({
3277
3982
  runtime: this.params.runtime,
3278
3983
  config: this.params.config
3279
3984
  });
3985
+ if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
3986
+ return result;
3280
3987
  }
3281
3988
  async recoverCurrentTab(request = {}) {
3282
3989
  const transport = resolveTransport(request.transport, this.params.config);
@@ -3288,6 +3995,7 @@ var GoogleMeetRuntime = class {
3288
3995
  url
3289
3996
  });
3290
3997
  return recoverCurrentMeetTab({
3998
+ runtime: this.params.runtime,
3291
3999
  config: this.params.config,
3292
4000
  url
3293
4001
  });
@@ -3296,7 +4004,51 @@ var GoogleMeetRuntime = class {
3296
4004
  const url = normalizeMeetUrl(request.url);
3297
4005
  const transport = resolveTransport(request.transport, this.params.config);
3298
4006
  const mode = resolveMode(request.mode, this.params.config);
3299
- let reusable = this.list().find((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport && session.mode === mode);
4007
+ const agentId = resolveSessionAgentId(request, this.params.config);
4008
+ if (!isBrowserTransport(transport)) return await this.#joinUnlocked(request, {
4009
+ url,
4010
+ transport,
4011
+ mode,
4012
+ agentId
4013
+ });
4014
+ return await this.#withBrowserMeetingLock(transport, url, async () => await this.#joinUnlocked(request, {
4015
+ url,
4016
+ transport,
4017
+ mode,
4018
+ agentId
4019
+ }));
4020
+ }
4021
+ async #joinUnlocked(request, resolved) {
4022
+ const { url, transport, mode, agentId } = resolved;
4023
+ const activeSessions = this.list().filter((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport);
4024
+ const retainedBrowserTabs = [];
4025
+ if (isBrowserTransport(transport) && isGoogleMeetTalkBackMode$1(mode)) for (const session of activeSessions) {
4026
+ if (!isGoogleMeetTalkBackMode$1(session.mode) || isReusableMeetSession(session, {
4027
+ url,
4028
+ transport,
4029
+ mode,
4030
+ agentId
4031
+ })) continue;
4032
+ const tab = this.params.config.chrome.reuseExistingTab ? session.chrome?.browserTab : void 0;
4033
+ const keepBrowserParticipant = Boolean(tab) || session.chrome?.launched === false;
4034
+ if (tab) retainedBrowserTabs.push({
4035
+ session,
4036
+ tab
4037
+ });
4038
+ try {
4039
+ if ((await this.#leaveUnlocked(session.id, keepBrowserParticipant ? { keepBrowserTab: true } : void 0)).browserLeft === false) throw new Error("Could not leave the previous Meet browser tab before reassignment.");
4040
+ } catch (error) {
4041
+ await this.#settleRetainedBrowserTabs(retainedBrowserTabs);
4042
+ throw error;
4043
+ }
4044
+ noteSession(session, "Ended before the same Meet tab was reassigned to another agent.");
4045
+ }
4046
+ let reusable = activeSessions.find((session) => isReusableMeetSession(session, {
4047
+ url,
4048
+ transport,
4049
+ mode,
4050
+ agentId
4051
+ }));
3300
4052
  if (reusable?.transport === "twilio") {
3301
4053
  await this.#refreshTwilioVoiceCallStatus(reusable);
3302
4054
  if (reusable.state !== "active") reusable = void 0;
@@ -3319,6 +4071,7 @@ var GoogleMeetRuntime = class {
3319
4071
  url,
3320
4072
  transport,
3321
4073
  mode,
4074
+ agentId,
3322
4075
  state: "active",
3323
4076
  createdAt,
3324
4077
  updatedAt: createdAt,
@@ -3335,40 +4088,60 @@ var GoogleMeetRuntime = class {
3335
4088
  };
3336
4089
  try {
3337
4090
  if (transport === "chrome" || transport === "chrome-node") {
3338
- const chromeConfig = request.agentId ? {
3339
- ...this.params.config,
3340
- realtime: {
3341
- ...this.params.config.realtime,
3342
- agentId: request.agentId
3343
- }
3344
- } : this.params.config;
3345
- const result = transport === "chrome-node" ? await launchChromeMeetOnNode({
3346
- runtime: this.params.runtime,
3347
- config: chromeConfig,
3348
- fullConfig: this.params.fullConfig,
3349
- meetingSessionId: session.id,
3350
- requesterSessionKey: request.requesterSessionKey,
3351
- mode,
3352
- url,
3353
- logger: this.params.logger
3354
- }) : await launchChromeMeet({
3355
- runtime: this.params.runtime,
3356
- config: chromeConfig,
3357
- fullConfig: this.params.fullConfig,
3358
- meetingSessionId: session.id,
3359
- requesterSessionKey: request.requesterSessionKey,
3360
- mode,
3361
- url,
3362
- logger: this.params.logger
4091
+ const chromeConfig = withSessionAgentConfig(this.params.config, agentId);
4092
+ let result;
4093
+ try {
4094
+ result = transport === "chrome-node" ? await launchChromeMeetOnNode({
4095
+ runtime: this.params.runtime,
4096
+ config: chromeConfig,
4097
+ fullConfig: this.params.fullConfig,
4098
+ meetingSessionId: session.id,
4099
+ requesterSessionKey: request.requesterSessionKey,
4100
+ mode,
4101
+ url,
4102
+ logger: this.params.logger
4103
+ }) : await launchChromeMeet({
4104
+ runtime: this.params.runtime,
4105
+ config: chromeConfig,
4106
+ fullConfig: this.params.fullConfig,
4107
+ meetingSessionId: session.id,
4108
+ requesterSessionKey: request.requesterSessionKey,
4109
+ mode,
4110
+ url,
4111
+ logger: this.params.logger
4112
+ });
4113
+ } catch (error) {
4114
+ await this.#settleRetainedBrowserTabs(retainedBrowserTabs);
4115
+ throw error;
4116
+ }
4117
+ const resultNodeId = "nodeId" in result ? result.nodeId : void 0;
4118
+ const browserTab = this.#inheritBrowserTabOwnership({
4119
+ transport,
4120
+ nodeId: resultNodeId,
4121
+ meetingUrl: url,
4122
+ tab: result.tab
3363
4123
  });
3364
4124
  session.chrome = {
3365
4125
  audioBackend: this.params.config.chrome.audioBackend,
3366
4126
  launched: result.launched,
3367
- nodeId: "nodeId" in result ? result.nodeId : void 0,
4127
+ nodeId: resultNodeId,
3368
4128
  browserProfile: this.params.config.chrome.browserProfile,
4129
+ browserTab,
3369
4130
  health: "browser" in result ? result.browser : void 0
3370
4131
  };
3371
4132
  this.#attachChromeAudioBridge(session, result.audioBridge);
4133
+ if (!await this.#settleRetainedBrowserTabs(retainedBrowserTabs, browserTab ? {
4134
+ transport,
4135
+ nodeId: resultNodeId,
4136
+ tab: browserTab
4137
+ } : void 0)) {
4138
+ try {
4139
+ await this.#leaveSession(session);
4140
+ } catch (error) {
4141
+ this.params.logger.warn(`[google-meet] replacement cleanup failed: ${formatErrorMessage(error)}`);
4142
+ }
4143
+ throw new Error("Could not leave the previous Meet browser tab before reassignment.");
4144
+ }
3372
4145
  session.notes.push(result.audioBridge ? transport === "chrome-node" ? "Chrome node transport joins as the signed-in Google profile on the selected node and routes realtime audio through the node bridge." : "Chrome transport joins as the signed-in Google profile and routes realtime audio through the configured bridge." : isGoogleMeetTalkBackMode$1(mode) ? "Chrome transport joins as the signed-in Google profile and expects BlackHole 2ch audio routing." : "Chrome transport joins as the signed-in Google profile without starting the realtime audio bridge.");
3373
4146
  this.#refreshSpeechReadiness(session);
3374
4147
  } else {
@@ -3379,6 +4152,7 @@ var GoogleMeetRuntime = class {
3379
4152
  dtmfSequence: request.dtmfSequence ?? this.params.config.twilio.defaultDtmfSequence
3380
4153
  });
3381
4154
  const dtmfSequence = request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
4155
+ const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? agentId : void 0;
3382
4156
  const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
3383
4157
  config: this.params.config,
3384
4158
  gateway: this.#voiceCallGateway,
@@ -3386,8 +4160,8 @@ var GoogleMeetRuntime = class {
3386
4160
  dtmfSequence,
3387
4161
  logger: this.params.logger,
3388
4162
  ...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
3389
- agentId: request.agentId,
3390
- sessionKey: request.agentId ? `agent:${request.agentId}:google-meet:${session.id}` : buildTwilioVoiceCallSessionKey(session.id),
4163
+ agentId: delegatedAgentId,
4164
+ sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : buildTwilioVoiceCallSessionKey(session.id),
3391
4165
  message: isGoogleMeetTalkBackMode$1(mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
3392
4166
  }) : void 0;
3393
4167
  delegatedTwilioSpoken = Boolean(voiceCallResult?.introSent);
@@ -3417,28 +4191,218 @@ var GoogleMeetRuntime = class {
3417
4191
  spoken: transport === "twilio" ? delegatedTwilioSpoken : isGoogleMeetTalkBackMode$1(mode) && speechInstructions ? await this.#speakWhenReady(session, speechInstructions) : false
3418
4192
  };
3419
4193
  }
3420
- async leave(sessionId) {
4194
+ async leave(sessionId, opts) {
4195
+ const session = this.#sessions.get(sessionId);
4196
+ if (!session) return { found: false };
4197
+ if (!isBrowserTransport(session.transport)) return await this.#leaveUnlocked(sessionId, opts);
4198
+ return await this.#withBrowserMeetingLock(session.transport, session.url, async () => await this.#leaveUnlocked(sessionId, opts));
4199
+ }
4200
+ async #leaveUnlocked(sessionId, opts) {
4201
+ const inFlight = this.#sessionLeaves.get(sessionId);
4202
+ if (inFlight) return await inFlight;
3421
4203
  const session = this.#sessions.get(sessionId);
3422
4204
  if (!session) return { found: false };
3423
- const stop = this.#sessionStops.get(sessionId);
3424
- if (stop) {
3425
- this.#sessionStops.delete(sessionId);
3426
- this.#sessionSpeakers.delete(sessionId);
3427
- this.#sessionHealth.delete(sessionId);
4205
+ if (session.state === "ended") return {
4206
+ found: true,
4207
+ session,
4208
+ ...session.browserLeft === void 0 ? {} : { browserLeft: session.browserLeft }
4209
+ };
4210
+ const leave = this.#leaveSession(session, opts);
4211
+ this.#sessionLeaves.set(sessionId, leave);
4212
+ try {
4213
+ return await leave;
4214
+ } finally {
4215
+ if (this.#sessionLeaves.get(sessionId) === leave) this.#sessionLeaves.delete(sessionId);
4216
+ }
4217
+ }
4218
+ async #withBrowserMeetingLock(transport, url, operation) {
4219
+ const key = `${transport}:${normalizeMeetUrlForReuse(url) ?? url}`;
4220
+ const previous = this.#browserMeetingLocks.get(key) ?? Promise.resolve();
4221
+ let release;
4222
+ const gate = new Promise((resolve) => {
4223
+ release = resolve;
4224
+ });
4225
+ const tail = previous.then(() => gate);
4226
+ this.#browserMeetingLocks.set(key, tail);
4227
+ await previous;
4228
+ try {
4229
+ return await operation();
4230
+ } finally {
4231
+ release?.();
4232
+ if (this.#browserMeetingLocks.get(key) === tail) this.#browserMeetingLocks.delete(key);
4233
+ }
4234
+ }
4235
+ async #leaveSession(session, opts) {
4236
+ if (session.mode === "transcribe") {
4237
+ this.#transcriptFinalizing.add(session.id);
4238
+ await this.#captureTranscriptSnapshot(session, { finalize: true }).catch((error) => {
4239
+ this.params.logger.debug?.(`[google-meet] final transcript snapshot ignored: ${formatErrorMessage(error)}`);
4240
+ });
4241
+ }
4242
+ session.state = "ended";
4243
+ session.updatedAt = nowIso();
4244
+ const stop = this.#sessionStops.get(session.id);
4245
+ this.#sessionStops.delete(session.id);
4246
+ this.#sessionSpeakers.delete(session.id);
4247
+ this.#sessionHealth.delete(session.id);
4248
+ let browserLeft;
4249
+ try {
4250
+ await stop?.();
4251
+ } finally {
3428
4252
  try {
3429
- await stop();
4253
+ if (!opts?.keepBrowserTab) browserLeft = await this.#releaseBrowserTab(session);
3430
4254
  } finally {
3431
- session.state = "ended";
3432
- session.updatedAt = nowIso();
4255
+ this.#retireTranscript(session.id);
4256
+ this.#transcriptFinalizing.delete(session.id);
3433
4257
  }
3434
4258
  }
3435
- session.state = "ended";
3436
- session.updatedAt = nowIso();
3437
4259
  return {
3438
4260
  found: true,
3439
- session
4261
+ session,
4262
+ ...browserLeft === void 0 ? {} : { browserLeft }
3440
4263
  };
3441
4264
  }
4265
+ async #captureTranscriptSnapshot(session, options = {}) {
4266
+ const capture = (this.#transcriptCaptures.get(session.id) ?? Promise.resolve()).catch(() => {}).then(async () => {
4267
+ if (!isBrowserTransport(session.transport) || session.mode !== "transcribe") return;
4268
+ const tab = session.chrome?.browserTab;
4269
+ if (!tab) return;
4270
+ const snapshot = session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
4271
+ runtime: this.params.runtime,
4272
+ nodeId: session.chrome?.nodeId,
4273
+ config: this.params.config,
4274
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
4275
+ meetingUrl: session.url,
4276
+ meetingSessionId: session.id,
4277
+ tab
4278
+ }) : await readChromeMeetTranscript({
4279
+ runtime: this.params.runtime,
4280
+ config: this.params.config,
4281
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
4282
+ meetingUrl: session.url,
4283
+ meetingSessionId: session.id,
4284
+ tab
4285
+ });
4286
+ this.#mergeTranscriptSnapshot(session.id, snapshot);
4287
+ });
4288
+ this.#transcriptCaptures.set(session.id, capture);
4289
+ try {
4290
+ await capture;
4291
+ } finally {
4292
+ if (this.#transcriptCaptures.get(session.id) === capture) this.#transcriptCaptures.delete(session.id);
4293
+ }
4294
+ }
4295
+ #mergeTranscriptSnapshot(sessionId, snapshot) {
4296
+ const pageNextIndex = snapshot.droppedLines + snapshot.lines.length;
4297
+ const retained = this.#transcripts.get(sessionId);
4298
+ if (!retained) {
4299
+ this.#transcripts.set(sessionId, {
4300
+ droppedLines: snapshot.droppedLines,
4301
+ lines: snapshot.lines,
4302
+ pageEpoch: snapshot.epoch,
4303
+ pageNextIndex
4304
+ });
4305
+ return;
4306
+ }
4307
+ if (retained.pageEpoch !== snapshot.epoch) {
4308
+ retained.droppedLines += snapshot.droppedLines;
4309
+ retained.lines.push(...snapshot.lines);
4310
+ retained.pageEpoch = snapshot.epoch;
4311
+ retained.pageNextIndex = pageNextIndex;
4312
+ } else if (pageNextIndex > retained.pageNextIndex) {
4313
+ const appendFrom = Math.max(retained.pageNextIndex, snapshot.droppedLines);
4314
+ retained.droppedLines += Math.max(0, snapshot.droppedLines - retained.pageNextIndex);
4315
+ retained.lines.push(...snapshot.lines.slice(appendFrom - snapshot.droppedLines));
4316
+ retained.pageNextIndex = pageNextIndex;
4317
+ }
4318
+ const excess = retained.lines.length - GOOGLE_MEET_TRANSCRIPT_MAX_LINES;
4319
+ if (excess > 0) {
4320
+ retained.lines.splice(0, excess);
4321
+ retained.droppedLines += excess;
4322
+ }
4323
+ }
4324
+ #retireTranscript(sessionId) {
4325
+ const snapshot = this.#transcripts.get(sessionId);
4326
+ if (snapshot) {
4327
+ this.#transcripts.delete(sessionId);
4328
+ this.#transcripts.set(sessionId, snapshot);
4329
+ this.#retiredTranscripts.delete(sessionId);
4330
+ this.#retiredTranscripts.add(sessionId);
4331
+ }
4332
+ const retainedIds = [...this.#retiredTranscripts].filter((id) => this.#transcripts.has(id)).toSorted((a, b) => (this.#sessions.get(a)?.updatedAt ?? "").localeCompare(this.#sessions.get(b)?.updatedAt ?? ""));
4333
+ for (const id of retainedIds.slice(0, -4)) {
4334
+ this.#transcripts.delete(id);
4335
+ this.#retiredTranscripts.delete(id);
4336
+ const session = this.#sessions.get(id);
4337
+ if (session) session.transcriptEvicted = true;
4338
+ }
4339
+ }
4340
+ #inheritBrowserTabOwnership(params) {
4341
+ if (!params.tab) return;
4342
+ const tab = params.tab;
4343
+ const createdTabKey = params.transport === "chrome-node" && params.nodeId ? `${params.nodeId}:${tab.targetId}` : void 0;
4344
+ const createdMeetingUrl = createdTabKey ? this.#createdBrowserTabs.get(createdTabKey) : void 0;
4345
+ const inheritedFromCreate = isSameMeetUrlForReuse(createdMeetingUrl, params.meetingUrl);
4346
+ if (createdMeetingUrl && createdTabKey) this.#createdBrowserTabs.delete(createdTabKey);
4347
+ const inheritedFromSession = [...this.#sessions.values()].some((session) => {
4348
+ const trackedTab = session.chrome?.browserTab;
4349
+ if (!trackedTab) return false;
4350
+ return session.transport === params.transport && isSameMeetUrlForReuse(session.url, params.meetingUrl) && session.chrome?.nodeId === params.nodeId && trackedTab.targetId === tab.targetId && trackedTab.openedByPlugin;
4351
+ });
4352
+ return inheritedFromCreate || inheritedFromSession ? {
4353
+ ...tab,
4354
+ openedByPlugin: true
4355
+ } : tab;
4356
+ }
4357
+ async #settleRetainedBrowserTabs(retained, adopted) {
4358
+ let settled = true;
4359
+ for (const { session, tab } of retained.splice(0)) {
4360
+ if (adopted?.transport === session.transport && adopted.nodeId === session.chrome?.nodeId && adopted.tab.targetId === tab.targetId) {
4361
+ if (session.chrome) session.chrome.browserTab = void 0;
4362
+ continue;
4363
+ }
4364
+ if (await this.#releaseBrowserTab(session) === false) settled = false;
4365
+ }
4366
+ return settled;
4367
+ }
4368
+ async #releaseBrowserTab(session) {
4369
+ let browserLeft;
4370
+ try {
4371
+ browserLeft = await this.#leaveBrowserMeetTab(session);
4372
+ } catch (error) {
4373
+ noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
4374
+ browserLeft = false;
4375
+ }
4376
+ session.browserLeft = browserLeft;
4377
+ if (session.chrome && browserLeft !== false) session.chrome.browserTab = void 0;
4378
+ return browserLeft;
4379
+ }
4380
+ async #leaveBrowserMeetTab(session) {
4381
+ if (!isBrowserTransport(session.transport)) return;
4382
+ const tab = session.chrome?.browserTab;
4383
+ if (!tab) {
4384
+ noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
4385
+ return false;
4386
+ }
4387
+ if (this.list().some((other) => other.id !== session.id && other.state === "active" && isBrowserTransport(other.transport) && other.chrome?.browserTab?.targetId === tab.targetId && other.chrome?.nodeId === session.chrome?.nodeId)) {
4388
+ noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
4389
+ return;
4390
+ }
4391
+ const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
4392
+ runtime: this.params.runtime,
4393
+ nodeId: session.chrome?.nodeId,
4394
+ config: this.params.config,
4395
+ meetingUrl: session.url,
4396
+ tab
4397
+ }) : await leaveChromeMeet({
4398
+ runtime: this.params.runtime,
4399
+ config: this.params.config,
4400
+ meetingUrl: session.url,
4401
+ tab
4402
+ });
4403
+ noteSession(session, result.note);
4404
+ return result.left;
4405
+ }
3442
4406
  async speak(sessionId, instructions) {
3443
4407
  const session = this.#sessions.get(sessionId);
3444
4408
  if (!session) return {
@@ -3519,9 +4483,16 @@ var GoogleMeetRuntime = class {
3519
4483
  const mode = requestedMode && isGoogleMeetTalkBackMode$1(requestedMode) ? requestedMode : isGoogleMeetTalkBackMode$1(this.params.config.defaultMode) ? this.params.config.defaultMode : "agent";
3520
4484
  const url = normalizeMeetUrl(request.url);
3521
4485
  const transport = resolveTransport(request.transport, this.params.config);
4486
+ const agentId = resolveSessionAgentId(request, this.params.config);
3522
4487
  const beforeSessions = this.list();
3523
4488
  const before = new Set(beforeSessions.map((session) => session.id));
3524
- const startOutputBytes = beforeSessions.find((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport && isGoogleMeetTalkBackMode$1(session.mode))?.chrome?.health?.lastOutputBytes ?? 0;
4489
+ const existingSession = beforeSessions.find((session) => isReusableMeetSession(session, {
4490
+ url,
4491
+ transport,
4492
+ mode,
4493
+ agentId
4494
+ }));
4495
+ const existingOutputBytes = existingSession?.chrome?.health?.lastOutputBytes ?? 0;
3525
4496
  const result = await this.join({
3526
4497
  ...request,
3527
4498
  transport,
@@ -3529,6 +4500,7 @@ var GoogleMeetRuntime = class {
3529
4500
  mode,
3530
4501
  message: request.message ?? "Say exactly: Google Meet speech test complete."
3531
4502
  });
4503
+ const startOutputBytes = existingSession?.id === result.session.id ? existingOutputBytes : 0;
3532
4504
  let health = result.session.chrome?.health;
3533
4505
  const shouldWaitForOutput = result.spoken === true && health?.manualActionRequired !== true && this.#sessionHealth.has(result.session.id);
3534
4506
  if (shouldWaitForOutput && !hasRealtimeAudioOutputAdvanced(health, startOutputBytes)) {
@@ -3564,9 +4536,16 @@ var GoogleMeetRuntime = class {
3564
4536
  const url = normalizeMeetUrl(request.url);
3565
4537
  const transport = resolveTransport(request.transport, this.params.config);
3566
4538
  if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
4539
+ const agentId = resolveSessionAgentId(request, this.params.config);
3567
4540
  const beforeSessions = this.list();
3568
4541
  const before = new Set(beforeSessions.map((session) => session.id));
3569
- const start = transcriptCheckpoint(beforeSessions.find((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport && session.mode === "transcribe")?.chrome?.health);
4542
+ const existingSession = beforeSessions.find((session) => isReusableMeetSession(session, {
4543
+ url,
4544
+ transport,
4545
+ mode: "transcribe",
4546
+ agentId
4547
+ }));
4548
+ const existingStart = transcriptCheckpoint(existingSession?.chrome?.health);
3570
4549
  const result = await this.join({
3571
4550
  ...request,
3572
4551
  transport,
@@ -3574,6 +4553,7 @@ var GoogleMeetRuntime = class {
3574
4553
  mode: "transcribe",
3575
4554
  message: void 0
3576
4555
  });
4556
+ const start = existingSession?.id === result.session.id ? existingStart : transcriptCheckpoint(void 0);
3577
4557
  let health = result.session.chrome?.health;
3578
4558
  const timeoutMs = resolveProbeTimeoutMs(request.timeoutMs, this.params.config.chrome.joinTimeoutMs);
3579
4559
  const shouldWait = health?.manualActionRequired !== true && isManagedChromeBrowserSession(result.session);
@@ -3667,6 +4647,7 @@ var GoogleMeetRuntime = class {
3667
4647
  readOnly: options.readOnly,
3668
4648
  url: session.url
3669
4649
  }) : await recoverCurrentMeetTab({
4650
+ runtime: this.params.runtime,
3670
4651
  config: this.params.config,
3671
4652
  mode: session.mode,
3672
4653
  readOnly: options.readOnly,
@@ -3700,12 +4681,13 @@ var GoogleMeetRuntime = class {
3700
4681
  if (!isGoogleMeetTalkBackMode$1(session.mode) || session.transport !== "chrome" || session.state !== "active" || !session.chrome || session.chrome.audioBridge) return;
3701
4682
  const health = session.chrome.health;
3702
4683
  if (health?.inCall !== true || health.micMuted === true || health.manualActionRequired === true) return;
4684
+ const sessionConfig = withSessionAgentConfig(this.params.config, session.agentId);
3703
4685
  const result = await launchChromeMeet({
3704
4686
  runtime: this.params.runtime,
3705
4687
  config: {
3706
- ...this.params.config,
4688
+ ...sessionConfig,
3707
4689
  chrome: {
3708
- ...this.params.config.chrome,
4690
+ ...sessionConfig.chrome,
3709
4691
  launch: false
3710
4692
  }
3711
4693
  },
@@ -3744,154 +4726,9 @@ var GoogleMeetRuntime = class {
3744
4726
  }
3745
4727
  };
3746
4728
  //#endregion
3747
- //#region extensions/google-meet/src/node-invoke-policy.ts
3748
- const GOOGLE_MEET_CHROME_NODE_COMMAND = "googlemeet.chrome";
3749
- const START_MODES = /* @__PURE__ */ new Set([
3750
- "agent",
3751
- "bidi",
3752
- "realtime",
3753
- "transcribe"
3754
- ]);
3755
- function asRecord$1(value) {
3756
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3757
- }
3758
- function readString(value) {
3759
- return typeof value === "string" && value.length > 0 ? value : void 0;
3760
- }
3761
- function readPositiveNumber(value) {
3762
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
3763
- }
3764
- function copyCommand(command) {
3765
- return command && command.length > 0 ? [...command] : void 0;
3766
- }
3767
- function denied(message, code = "GOOGLE_MEET_NODE_POLICY_DENIED") {
3768
- return {
3769
- ok: false,
3770
- code,
3771
- message
3772
- };
3773
- }
3774
- function approved(params) {
3775
- return {
3776
- approved: true,
3777
- params
3778
- };
3779
- }
3780
- function buildStartParams(params, config) {
3781
- let url;
3782
- try {
3783
- url = normalizeMeetUrl(params.url);
3784
- } catch (error) {
3785
- return {
3786
- approved: false,
3787
- result: denied(error instanceof Error ? error.message : "googlemeet.chrome start requires url")
3788
- };
3789
- }
3790
- const mode = readString(params.mode);
3791
- if (mode && !START_MODES.has(mode)) return {
3792
- approved: false,
3793
- result: denied(`googlemeet.chrome start mode is unsupported: ${mode}`)
3794
- };
3795
- const startParams = {
3796
- action: "start",
3797
- url,
3798
- launch: params.launch === false ? false : config.chrome.launch,
3799
- browserProfile: config.chrome.browserProfile,
3800
- joinTimeoutMs: config.chrome.joinTimeoutMs
3801
- };
3802
- if (mode) startParams.mode = mode;
3803
- const audioInputCommand = copyCommand(config.chrome.audioInputCommand);
3804
- if (audioInputCommand) startParams.audioInputCommand = audioInputCommand;
3805
- const audioOutputCommand = copyCommand(config.chrome.audioOutputCommand);
3806
- if (audioOutputCommand) startParams.audioOutputCommand = audioOutputCommand;
3807
- const audioBridgeCommand = copyCommand(config.chrome.audioBridgeCommand);
3808
- if (audioBridgeCommand) startParams.audioBridgeCommand = audioBridgeCommand;
3809
- const audioBridgeHealthCommand = copyCommand(config.chrome.audioBridgeHealthCommand);
3810
- if (audioBridgeHealthCommand) startParams.audioBridgeHealthCommand = audioBridgeHealthCommand;
3811
- return approved(startParams);
3812
- }
3813
- function buildForwardParams(params) {
3814
- const action = readString(params.action);
3815
- switch (action) {
3816
- case "setup": return { action };
3817
- case "status": {
3818
- const bridgeId = readString(params.bridgeId);
3819
- return bridgeId ? {
3820
- action,
3821
- bridgeId
3822
- } : { action };
3823
- }
3824
- case "list": {
3825
- const forwarded = { action };
3826
- const url = readString(params.url);
3827
- const mode = readString(params.mode);
3828
- if (url) forwarded.url = url;
3829
- if (mode) forwarded.mode = mode;
3830
- return forwarded;
3831
- }
3832
- case "stopByUrl": {
3833
- const forwarded = { action };
3834
- const url = readString(params.url);
3835
- const mode = readString(params.mode);
3836
- const exceptBridgeId = readString(params.exceptBridgeId);
3837
- if (url) forwarded.url = url;
3838
- if (mode) forwarded.mode = mode;
3839
- if (exceptBridgeId) forwarded.exceptBridgeId = exceptBridgeId;
3840
- return forwarded;
3841
- }
3842
- case "pullAudio": {
3843
- const forwarded = { action };
3844
- const bridgeId = readString(params.bridgeId);
3845
- const timeoutMs = readPositiveNumber(params.timeoutMs);
3846
- if (bridgeId) forwarded.bridgeId = bridgeId;
3847
- if (timeoutMs) forwarded.timeoutMs = timeoutMs;
3848
- return forwarded;
3849
- }
3850
- case "pushAudio": {
3851
- const forwarded = { action };
3852
- const bridgeId = readString(params.bridgeId);
3853
- const base64 = readString(params.base64);
3854
- if (bridgeId) forwarded.bridgeId = bridgeId;
3855
- if (base64) forwarded.base64 = base64;
3856
- return forwarded;
3857
- }
3858
- case "clearAudio":
3859
- case "stop": {
3860
- const bridgeId = readString(params.bridgeId);
3861
- return bridgeId ? {
3862
- action,
3863
- bridgeId
3864
- } : { action };
3865
- }
3866
- default: return null;
3867
- }
3868
- }
3869
- function createGoogleMeetChromeNodeInvokePolicy(config) {
3870
- return {
3871
- commands: [GOOGLE_MEET_CHROME_NODE_COMMAND],
3872
- dangerous: true,
3873
- async handle(ctx) {
3874
- if (ctx.command !== "googlemeet.chrome") return denied(`unsupported Google Meet node command: ${ctx.command}`);
3875
- const params = asRecord$1(ctx.params);
3876
- const action = readString(params.action);
3877
- let decision;
3878
- if (action === "start") decision = buildStartParams(params, config);
3879
- else {
3880
- const forwardParams = buildForwardParams(params);
3881
- decision = forwardParams ? approved(forwardParams) : {
3882
- approved: false,
3883
- result: denied("unsupported googlemeet.chrome action")
3884
- };
3885
- }
3886
- if (!decision.approved) return decision.result;
3887
- return await ctx.invokeNode({ params: decision.params });
3888
- }
3889
- };
3890
- }
3891
- //#endregion
3892
4729
  //#region extensions/google-meet/index.ts
3893
- const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-CkkaRlbI.js"));
3894
- const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-D364Yrym.js"));
4730
+ const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-CWcFI0ub.js"));
4731
+ const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-SodPSOFX.js"));
3895
4732
  const googleMeetConfigSchema = {
3896
4733
  parse(value) {
3897
4734
  return resolveGoogleMeetConfig(value);
@@ -4099,6 +4936,7 @@ const GoogleMeetToolSchema = Type.Object({
4099
4936
  "join",
4100
4937
  "create",
4101
4938
  "status",
4939
+ "transcript",
4102
4940
  "setup_status",
4103
4941
  "resolve_space",
4104
4942
  "preflight",
@@ -4150,6 +4988,10 @@ const GoogleMeetToolSchema = Type.Object({
4150
4988
  pin: Type.Optional(Type.String({ description: "Meet phone PIN for Twilio; # is appended if omitted" })),
4151
4989
  dtmfSequence: Type.Optional(Type.String({ description: "Explicit DTMF sequence for Twilio" })),
4152
4990
  sessionId: Type.Optional(Type.String({ description: "Meet session ID" })),
4991
+ sinceIndex: Type.Optional(Type.Integer({
4992
+ description: "For transcript, resume from the previous response's nextIndex.",
4993
+ minimum: 0
4994
+ })),
4153
4995
  message: Type.Optional(Type.String({ description: "Realtime instructions to speak now" })),
4154
4996
  timeoutMs: optionalPositiveIntegerSchema({ description: "Probe timeout in milliseconds" }),
4155
4997
  meeting: Type.Optional(Type.String({ description: "Meet URL, meeting code, or spaces/{id}" })),
@@ -4236,11 +5078,17 @@ function readGatewayErrorDetails(err) {
4236
5078
  }
4237
5079
  async function callGoogleMeetGatewayFromTool(params) {
4238
5080
  try {
4239
- if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, { timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config) });
5081
+ if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, {
5082
+ timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config),
5083
+ scopes: ["operator.admin"]
5084
+ });
4240
5085
  return await googleMeetToolDeps.callGatewayFromCli(googleMeetGatewayMethodForToolAction(params.action), {
4241
5086
  json: true,
4242
5087
  timeout: String(resolveGoogleMeetGatewayOperationTimeoutMs(params.config))
4243
- }, params.raw, { progress: false });
5088
+ }, params.raw, {
5089
+ progress: false,
5090
+ scopes: ["operator.admin"]
5091
+ });
4244
5092
  } catch (err) {
4245
5093
  const details = readGatewayErrorDetails(err);
4246
5094
  if (details && typeof details === "object") return details;
@@ -4263,7 +5111,7 @@ async function createAndJoinMeetFromParams(params) {
4263
5111
  return (await loadGoogleMeetCreateModule()).createAndJoinMeetFromParams(params);
4264
5112
  }
4265
5113
  async function resolveGoogleMeetTokenFromParams(config, raw) {
4266
- const { resolveGoogleMeetAccessToken } = await import("./oauth-75iPDzIn.js");
5114
+ const { resolveGoogleMeetAccessToken } = await import("./oauth-DOfMKlGp.js");
4267
5115
  return resolveGoogleMeetAccessToken({
4268
5116
  clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
4269
5117
  clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
@@ -4462,6 +5310,23 @@ var google_meet_default = definePluginEntry({
4462
5310
  sendError(respond, err);
4463
5311
  }
4464
5312
  });
5313
+ api.registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
5314
+ try {
5315
+ const sessionId = normalizeOptionalString(params?.sessionId);
5316
+ if (!sessionId) {
5317
+ sendError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
5318
+ return;
5319
+ }
5320
+ const sinceIndex = params?.sinceIndex;
5321
+ if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
5322
+ sendError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), ErrorCodes.INVALID_REQUEST);
5323
+ return;
5324
+ }
5325
+ respond(true, await (await ensureRuntime()).transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
5326
+ } catch (err) {
5327
+ sendError(respond, err);
5328
+ }
5329
+ });
4465
5330
  api.registerGatewayMethod("googlemeet.recoverCurrentTab", async ({ params, respond }) => {
4466
5331
  try {
4467
5332
  respond(true, await (await ensureRuntime()).recoverCurrentTab({
@@ -4613,13 +5478,15 @@ var google_meet_default = definePluginEntry({
4613
5478
  sendError(respond, err);
4614
5479
  }
4615
5480
  });
4616
- api.registerGatewayMethod("googlemeet.testListen", async ({ params, respond }) => {
5481
+ api.registerGatewayMethod("googlemeet.testListen", async ({ params, client, respond }) => {
4617
5482
  try {
5483
+ const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
4618
5484
  respond(true, await (await ensureRuntime()).testListen({
4619
- url: resolveMeetingInput(config, params?.url),
4620
- transport: normalizeTransport(params?.transport),
4621
- mode: normalizeMode(params?.mode),
4622
- timeoutMs: readPositiveIntegerParam(asParamRecord(params), "timeoutMs")
5485
+ url: resolveMeetingInput(config, trustedParams.url),
5486
+ transport: normalizeTransport(trustedParams.transport),
5487
+ mode: normalizeMode(trustedParams.mode),
5488
+ agentId: normalizeOptionalString(trustedParams.agentId),
5489
+ timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs")
4623
5490
  }));
4624
5491
  } catch (err) {
4625
5492
  sendError(respond, err);
@@ -4633,10 +5500,12 @@ var google_meet_default = definePluginEntry({
4633
5500
  async execute(_toolCallId, params) {
4634
5501
  const raw = asParamRecord(params);
4635
5502
  const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
4636
- const agentId = toolContext.agentId ? normalizeAgentId(toolContext.agentId) : void 0;
5503
+ const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
5504
+ const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
4637
5505
  try {
4638
- const useTrustedRuntime = agentId ? await api.runtime.gateway.isAvailable() : false;
4639
- if (agentId && agentId !== "main" && !useTrustedRuntime) throw new Error("Per-agent Google Meet routing requires a Gateway-hosted agent run.");
5506
+ const needsTrustedAgentRouting = Boolean(agentId && agentId !== "main");
5507
+ const useTrustedRuntime = needsTrustedAgentRouting ? await api.runtime.gateway.isAvailable() : false;
5508
+ if (needsTrustedAgentRouting && !useTrustedRuntime) throw new Error("Per-agent Google Meet routing requires a Gateway-hosted agent run.");
4640
5509
  const rawWithRequester = {
4641
5510
  ...raw,
4642
5511
  ...requesterSessionKey ? { requesterSessionKey } : {},
@@ -4668,13 +5537,19 @@ var google_meet_default = definePluginEntry({
4668
5537
  case "test_listen": return jsonResult(await callGoogleMeetGatewayFromTool({
4669
5538
  config,
4670
5539
  action: "test_listen",
4671
- raw
5540
+ raw: rawWithRequester,
5541
+ runtime: useTrustedRuntime ? api.runtime : void 0
4672
5542
  }));
4673
5543
  case "status": return jsonResult(await callGoogleMeetGatewayFromTool({
4674
5544
  config,
4675
5545
  action: "status",
4676
5546
  raw
4677
5547
  }));
5548
+ case "transcript": return jsonResult(await callGoogleMeetGatewayFromTool({
5549
+ config,
5550
+ action: "transcript",
5551
+ raw
5552
+ }));
4678
5553
  case "recover_current_tab": return jsonResult(await callGoogleMeetGatewayFromTool({
4679
5554
  config,
4680
5555
  action: "recover_current_tab",