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

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 +1235 -359
  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,23 @@ 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
2360
  const chromeTransportDeps = { callGatewayFromCli };
2361
+ const GOOGLE_MEET_CAPTION_SETTLE_MS = 1e3;
2178
2362
  function isGoogleMeetTalkBackMode$2(mode) {
2179
2363
  return mode === "agent" || mode === "bidi";
2180
2364
  }
2365
+ function readMeetAuthUser(url) {
2366
+ if (!url) return;
2367
+ try {
2368
+ return new URL(url).searchParams.get("authuser") ?? void 0;
2369
+ } catch {
2370
+ return;
2371
+ }
2372
+ }
2181
2373
  async function assertBlackHole2chAvailable(params) {
2182
2374
  if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
2183
2375
  const result = await params.runtime.system.runCommandWithTimeout([GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"], { timeoutMs: params.timeoutMs });
@@ -2250,9 +2442,10 @@ async function launchChromeMeet(params) {
2250
2442
  audioBridge: await startRealtimeAudioBridge()
2251
2443
  };
2252
2444
  const result = await openMeetWithBrowserRequest({
2253
- callBrowser: callLocalBrowserRequest,
2445
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
2254
2446
  config: params.config,
2255
2447
  mode: params.mode,
2448
+ meetingSessionId: params.meetingSessionId,
2256
2449
  url: params.url
2257
2450
  });
2258
2451
  const audioBridge = isGoogleMeetTalkBackMode$2(params.mode) && result.browser?.inCall === true && result.browser.micMuted !== true && result.browser.manualActionRequired !== true ? await startRealtimeAudioBridge() : void 0;
@@ -2310,6 +2503,18 @@ async function callLocalBrowserRequest(params) {
2310
2503
  timeoutMs: params.timeoutMs
2311
2504
  }, { progress: false });
2312
2505
  }
2506
+ async function resolveLocalBrowserRequest(runtime) {
2507
+ if (!await runtime.gateway.isAvailable()) return callLocalBrowserRequest;
2508
+ return async (params) => await runtime.gateway.request("browser.request", {
2509
+ method: params.method,
2510
+ path: params.path,
2511
+ body: params.body,
2512
+ timeoutMs: params.timeoutMs
2513
+ }, {
2514
+ timeoutMs: resolveBrowserGatewayTimeoutMs(params.timeoutMs),
2515
+ scopes: ["operator.admin"]
2516
+ });
2517
+ }
2313
2518
  function resolveBrowserGatewayTimeoutMs(timeoutMs) {
2314
2519
  return addTimerTimeoutGraceMs(timeoutMs) ?? 1;
2315
2520
  }
@@ -2350,6 +2555,7 @@ function meetStatusScript(params) {
2350
2555
  return `async () => {
2351
2556
  const text = (node) => (node?.innerText || node?.textContent || "").trim();
2352
2557
  const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
2558
+ const captionSessionId = ${JSON.stringify(params.captionSessionId)};
2353
2559
  const captureCaptions = ${JSON.stringify(params.captureCaptions)};
2354
2560
  const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
2355
2561
  const buttons = [...document.querySelectorAll('button')];
@@ -2405,8 +2611,9 @@ function meetStatusScript(params) {
2405
2611
  mic.click();
2406
2612
  notes.push("Muted Meet microphone for observe-only mode.");
2407
2613
  }
2614
+ const joinElsewhere = findButton(/join here too/i);
2408
2615
  const join = !readOnly && ${JSON.stringify(params.autoJoin)}
2409
- ? findButton(/join now|ask to join|join here too/i)
2616
+ ? findButton(/join now|ask to join/i)
2410
2617
  : null;
2411
2618
  if (join) join.click();
2412
2619
  const microphoneChoice = findButton(/\\buse microphone\\b/i);
@@ -2480,42 +2687,127 @@ function meetStatusScript(params) {
2480
2687
  if (!captureCaptions) return undefined;
2481
2688
  const w = window;
2482
2689
  if (!inCall && !w.__openclawMeetCaptions) return undefined;
2483
- if (!w.__openclawMeetCaptions) {
2690
+ // A reused tab starts a fresh logical transcript for each OpenClaw session.
2691
+ // Status refreshes omit the id, so they preserve the active page-owned buffer.
2692
+ if (!w.__openclawMeetCaptions || (captionSessionId && w.__openclawMeetCaptions.sessionId !== captionSessionId)) {
2693
+ if (w.__openclawMeetCaptions?.settleTimer !== undefined) {
2694
+ clearTimeout(w.__openclawMeetCaptions.settleTimer);
2695
+ }
2696
+ w.__openclawMeetCaptions?.observer?.disconnect?.();
2484
2697
  w.__openclawMeetCaptions = {
2698
+ sessionId: captionSessionId,
2699
+ // Epochs cross document lifetimes in the runtime transcript cursor.
2700
+ // Strong UUIDs keep a reloaded page distinct from its prior buffer.
2701
+ epoch: crypto.randomUUID(),
2485
2702
  enabledAttempted: false,
2486
2703
  observerInstalled: false,
2704
+ observer: undefined,
2705
+ droppedLines: 0,
2487
2706
  lines: [],
2488
- seen: {}
2707
+ settleTimer: undefined,
2708
+ visible: []
2489
2709
  };
2490
2710
  }
2491
2711
  return w.__openclawMeetCaptions;
2492
2712
  })();
2493
- const recordCaption = (speaker, captionText) => {
2713
+ const normalizeCaption = (speaker, captionText) => {
2494
2714
  if (!captionState) return;
2495
2715
  const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
2496
2716
  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);
2717
+ if (!clean || clean.length < 2) return undefined;
2718
+ if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return undefined;
2719
+ return { speaker: cleanSpeaker || undefined, text: clean };
2720
+ };
2721
+ const commitLines = (state, entries) => {
2722
+ state.lines.push(...entries.map((entry) => ({
2723
+ at: entry.at,
2724
+ speaker: entry.speaker,
2725
+ text: entry.text
2726
+ })));
2727
+ const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
2728
+ if (excess > 0) {
2729
+ state.lines.splice(0, excess);
2730
+ state.droppedLines = (state.droppedLines || 0) + excess;
2731
+ }
2505
2732
  };
2506
2733
  const scrapeCaptions = () => {
2507
2734
  if (!captionState) return;
2508
2735
  const regions = [...document.querySelectorAll(captionSelector)];
2736
+ const rows = [];
2509
2737
  for (const region of regions) {
2510
2738
  const raw = text(region);
2511
2739
  if (!raw) continue;
2512
2740
  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);
2741
+ const row = pieces.length >= 2
2742
+ ? normalizeCaption(pieces[0], pieces.slice(1).join(" "))
2743
+ : normalizeCaption("", pieces[0] || raw);
2744
+ if (row) rows.push({ ...row, node: region });
2745
+ }
2746
+ if (rows.length === 0) {
2747
+ // Meet briefly removes caption rows while rerendering. Keep them mutable
2748
+ // for one settle window so a DOM gap cannot fabricate a repeated line.
2749
+ if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
2750
+ const pendingState = captionState;
2751
+ pendingState.settleTimer = setTimeout(() => {
2752
+ if (window.__openclawMeetCaptions !== pendingState) return;
2753
+ commitLines(pendingState, pendingState.visible);
2754
+ pendingState.visible = [];
2755
+ pendingState.settleTimer = undefined;
2756
+ }, ${GOOGLE_MEET_CAPTION_SETTLE_MS});
2517
2757
  }
2758
+ return;
2518
2759
  }
2760
+ if (captionState.settleTimer !== undefined) {
2761
+ clearTimeout(captionState.settleTimer);
2762
+ captionState.settleTimer = undefined;
2763
+ }
2764
+ const previous = Array.isArray(captionState.visible) ? captionState.visible : [];
2765
+ const unmatchedPrevious = [...previous];
2766
+ const nextVisible = [];
2767
+ const now = Date.now();
2768
+ for (let index = 0; index < rows.length; index += 1) {
2769
+ const row = rows[index];
2770
+ const priorIndex = unmatchedPrevious.findIndex((candidate) => {
2771
+ const sameTextLifecycle =
2772
+ candidate.text === row.text ||
2773
+ row.text.startsWith(candidate.text) ||
2774
+ candidate.text.startsWith(row.text);
2775
+ const sameDomLifecycle =
2776
+ candidate.node === row.node || now - candidate.seenAt <= ${GOOGLE_MEET_CAPTION_SETTLE_MS};
2777
+ return candidate.speaker === row.speaker && sameTextLifecycle && sameDomLifecycle;
2778
+ });
2779
+ const prior = priorIndex >= 0 ? unmatchedPrevious.splice(priorIndex, 1)[0] : undefined;
2780
+ const sameSpeaker = Boolean(prior) && prior.speaker === row.speaker;
2781
+ if (sameSpeaker && prior.text === row.text) {
2782
+ prior.node = row.node;
2783
+ prior.seenAt = now;
2784
+ nextVisible.push(prior);
2785
+ continue;
2786
+ }
2787
+ if (sameSpeaker && row.text.startsWith(prior.text)) {
2788
+ prior.text = row.text;
2789
+ prior.node = row.node;
2790
+ prior.seenAt = now;
2791
+ nextVisible.push(prior);
2792
+ continue;
2793
+ }
2794
+ if (sameSpeaker && prior.text.startsWith(row.text)) {
2795
+ prior.node = row.node;
2796
+ prior.seenAt = now;
2797
+ nextVisible.push(prior);
2798
+ continue;
2799
+ }
2800
+ const entry = {
2801
+ at: new Date().toISOString(),
2802
+ node: row.node,
2803
+ seenAt: now,
2804
+ speaker: row.speaker,
2805
+ text: row.text
2806
+ };
2807
+ nextVisible.push(entry);
2808
+ }
2809
+ commitLines(captionState, unmatchedPrevious);
2810
+ captionState.visible = nextVisible;
2519
2811
  };
2520
2812
  if (captionState) {
2521
2813
  if (!readOnly && inCall && !captionState.enabledAttempted) {
@@ -2534,7 +2826,8 @@ function meetStatusScript(params) {
2534
2826
  }
2535
2827
  if (inCall && !captionState.observerInstalled) {
2536
2828
  captionState.observerInstalled = true;
2537
- new MutationObserver(scrapeCaptions).observe(document.body, {
2829
+ captionState.observer = new MutationObserver(scrapeCaptions);
2830
+ captionState.observer.observe(document.body, {
2538
2831
  childList: true,
2539
2832
  subtree: true,
2540
2833
  characterData: true
@@ -2544,17 +2837,19 @@ function meetStatusScript(params) {
2544
2837
  if (inCall) {
2545
2838
  scrapeCaptions();
2546
2839
  }
2547
- const lines = Array.isArray(captionState.lines) ? captionState.lines : [];
2840
+ const committedLines = Array.isArray(captionState.lines) ? captionState.lines : [];
2841
+ const visibleLines = Array.isArray(captionState.visible) ? captionState.visible : [];
2842
+ const lines = [...committedLines, ...visibleLines];
2548
2843
  const last = lines[lines.length - 1];
2549
2844
  captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
2550
- transcriptLines = lines.length;
2845
+ transcriptLines = (captionState.droppedLines || 0) + lines.length;
2551
2846
  lastCaptionAt = last?.at;
2552
2847
  lastCaptionSpeaker = last?.speaker;
2553
2848
  lastCaptionText = last?.text;
2554
2849
  recentTranscript = lines.slice(-5);
2555
2850
  }
2556
2851
  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)
2852
+ 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
2853
  ? 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
2854
  : undefined;
2560
2855
  let manualActionReason;
@@ -2562,6 +2857,9 @@ function meetStatusScript(params) {
2562
2857
  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
2858
  manualActionReason = "google-login-required";
2564
2859
  manualActionMessage = "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.";
2860
+ } else if (!inCall && joinElsewhere) {
2861
+ manualActionReason = "meet-session-conflict";
2862
+ manualActionMessage = "Meet is already active in another tab or device. Leave that session or reuse an English-pinned tab before retrying.";
2565
2863
  } 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
2864
  manualActionReason = "meet-admission-required";
2567
2865
  manualActionMessage = "Admit the OpenClaw browser participant in Google Meet, then retry speech.";
@@ -2602,63 +2900,404 @@ function meetStatusScript(params) {
2602
2900
  });
2603
2901
  }`;
2604
2902
  }
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
- });
2903
+ function meetTranscriptScript(meetingUrl, meetingSessionId, finalize) {
2904
+ const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
2905
+ return `() => {
2906
+ const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
2907
+ const expectedSessionId = ${JSON.stringify(meetingSessionId)};
2908
+ let currentMeetingUrl;
2909
+ try {
2910
+ const currentUrl = new URL(location.href);
2911
+ currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
2912
+ } catch {
2913
+ return JSON.stringify({ urlMatched: false });
2914
+ }
2915
+ if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
2916
+ return JSON.stringify({ urlMatched: false });
2917
+ }
2918
+ const state = window.__openclawMeetCaptions;
2919
+ if (state?.sessionId && state.sessionId !== expectedSessionId) {
2920
+ return JSON.stringify({ urlMatched: true, sessionMatched: false });
2921
+ }
2922
+ if (${JSON.stringify(finalize)} && Array.isArray(state?.visible) && state.visible.length > 0) {
2923
+ if (state.settleTimer !== undefined) clearTimeout(state.settleTimer);
2924
+ state.settleTimer = undefined;
2925
+ state.lines = Array.isArray(state.lines) ? state.lines : [];
2926
+ state.lines.push(...state.visible.map((entry) => ({
2927
+ at: entry.at,
2928
+ speaker: entry.speaker,
2929
+ text: entry.text
2930
+ })));
2931
+ state.visible = [];
2932
+ const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
2933
+ if (excess > 0) {
2934
+ state.lines.splice(0, excess);
2935
+ state.droppedLines = (state.droppedLines || 0) + excess;
2936
+ }
2937
+ }
2938
+ const lines = Array.isArray(state?.lines) ? state.lines : [];
2939
+ return JSON.stringify({
2940
+ urlMatched: true,
2941
+ sessionMatched: true,
2942
+ epoch: typeof state?.epoch === "string" ? state.epoch : undefined,
2943
+ droppedLines: Number.isFinite(state?.droppedLines) ? Math.max(0, Math.trunc(state.droppedLines)) : 0,
2944
+ lines: lines.map((line) => ({
2945
+ at: typeof line?.at === "string" ? line.at : undefined,
2946
+ speaker: typeof line?.speaker === "string" ? line.speaker : undefined,
2947
+ text: typeof line?.text === "string" ? line.text : ""
2948
+ })).filter((line) => line.text)
2949
+ });
2950
+ }`;
2619
2951
  }
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;
2952
+ function parseMeetTranscriptSnapshot(result) {
2953
+ const raw = (result && typeof result === "object" ? result : {}).result;
2954
+ if (typeof raw !== "string" || !raw.trim()) return {
2955
+ droppedLines: 0,
2956
+ lines: []
2957
+ };
2958
+ let parsed;
2959
+ try {
2960
+ parsed = JSON.parse(raw);
2961
+ } catch {
2962
+ throw new Error("Google Meet transcript JSON is malformed.");
2647
2963
  }
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
- }
2964
+ if (!parsed || typeof parsed !== "object") throw new Error("Google Meet transcript payload is invalid.");
2965
+ const payload = parsed;
2966
+ const droppedLines = typeof payload.droppedLines === "number" && Number.isSafeInteger(payload.droppedLines) ? Math.max(0, payload.droppedLines) : 0;
2967
+ const lines = Array.isArray(payload.lines) ? payload.lines.flatMap((value) => {
2968
+ if (!value || typeof value !== "object") return [];
2969
+ const line = value;
2970
+ if (typeof line.text !== "string" || !line.text.trim()) return [];
2971
+ return [{
2972
+ ...typeof line.at === "string" ? { at: line.at } : {},
2973
+ ...typeof line.speaker === "string" ? { speaker: line.speaker } : {},
2974
+ text: line.text
2975
+ }];
2976
+ }) : [];
2977
+ return {
2978
+ droppedLines,
2979
+ ...typeof payload.epoch === "string" ? { epoch: payload.epoch } : {},
2980
+ lines,
2981
+ ...typeof payload.urlMatched === "boolean" ? { urlMatched: payload.urlMatched } : {},
2982
+ ...typeof payload.sessionMatched === "boolean" ? { sessionMatched: payload.sessionMatched } : {}
2656
2983
  };
2657
- const permissionNotes = await grantMeetMediaPermissions({
2658
- allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
2659
- callBrowser: params.callBrowser,
2660
- targetId,
2661
- timeoutMs
2984
+ }
2985
+ async function readMeetTranscriptWithBrowserRequest(params) {
2986
+ const snapshot = parseMeetTranscriptSnapshot(await params.callBrowser({
2987
+ method: "POST",
2988
+ path: "/act",
2989
+ body: {
2990
+ kind: "evaluate",
2991
+ targetId: params.tab.targetId,
2992
+ fn: meetTranscriptScript(params.meetingUrl, params.meetingSessionId, params.finalize)
2993
+ },
2994
+ timeoutMs: params.timeoutMs
2995
+ }));
2996
+ if (snapshot.urlMatched === false) throw new Error("The tracked Meet tab no longer shows this session's meeting URL.");
2997
+ if (snapshot.sessionMatched === false) throw new Error("The tracked Meet tab now belongs to another OpenClaw meeting session.");
2998
+ return {
2999
+ droppedLines: snapshot.droppedLines,
3000
+ ...snapshot.epoch ? { epoch: snapshot.epoch } : {},
3001
+ lines: snapshot.lines
3002
+ };
3003
+ }
3004
+ function meetLeaveScript(meetingUrl) {
3005
+ const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
3006
+ return `() => {
3007
+ const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
3008
+ let currentMeetingUrl;
3009
+ try {
3010
+ const currentUrl = new URL(location.href);
3011
+ currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
3012
+ } catch {
3013
+ return JSON.stringify({ departed: false });
3014
+ }
3015
+ if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
3016
+ return JSON.stringify({ departed: true, urlMatched: false });
3017
+ }
3018
+ const text = (node) => (node?.innerText || node?.textContent || "").trim();
3019
+ // Locale-independent fallback: Meet renders the leave control as a Material
3020
+ // Symbols icon whose ligature text is "call_end" in every UI language, so a
3021
+ // localized aria-label (e.g. "Anruf verlassen") still resolves to the button.
3022
+ const hasLeaveIcon = (button) => {
3023
+ const icon = button.querySelector ? button.querySelector("i") : null;
3024
+ return icon ? (icon.textContent || "").trim() === "call_end" : false;
3025
+ };
3026
+ const buttons = [...document.querySelectorAll('button')];
3027
+ const label = (button) => [
3028
+ button.getAttribute("aria-label"),
3029
+ button.getAttribute("data-tooltip"),
3030
+ text(button),
3031
+ ]
3032
+ .filter(Boolean)
3033
+ .join(" ");
3034
+ const postCall = buttons.some((button) => /\\b(rejoin|return to home screen)\\b/i.test(label(button)));
3035
+ if (postCall) {
3036
+ return JSON.stringify({ departed: true, urlMatched: true });
3037
+ }
3038
+ // Managed join tabs are reused only after the English-tab gate or opened
3039
+ // through the English-UI helper, so follow-up labels are pinned to English.
3040
+ const confirmation = buttons.find((button) => {
3041
+ return !button.disabled && /\\bleave meeting\\b/i.test(label(button));
3042
+ });
3043
+ if (confirmation) {
3044
+ confirmation.click();
3045
+ return JSON.stringify({ departed: false, leaveAction: "confirm", urlMatched: true });
3046
+ }
3047
+ const leave = buttons.find((button) => {
3048
+ if (button.disabled) return false;
3049
+ return /leave call/i.test(label(button)) || hasLeaveIcon(button);
3050
+ });
3051
+ if (leave) {
3052
+ leave.click();
3053
+ return JSON.stringify({ departed: false, leaveAction: "leave", urlMatched: true });
3054
+ }
3055
+ return JSON.stringify({ departed: false, urlMatched: true });
3056
+ }`;
3057
+ }
3058
+ function parseMeetLeaveResult(result) {
3059
+ const raw = (result && typeof result === "object" ? result : {}).result;
3060
+ if (typeof raw !== "string" || !raw.trim()) return { departed: false };
3061
+ try {
3062
+ const parsed = JSON.parse(raw);
3063
+ const leaveAction = parsed.leaveAction === "leave" || parsed.leaveAction === "confirm" ? parsed.leaveAction : void 0;
3064
+ return {
3065
+ departed: parsed.departed === true,
3066
+ ...leaveAction ? { leaveAction } : {},
3067
+ ...typeof parsed.urlMatched === "boolean" ? { urlMatched: parsed.urlMatched } : {}
3068
+ };
3069
+ } catch {
3070
+ return { departed: false };
3071
+ }
3072
+ }
3073
+ async function leaveMeetInPage(params) {
3074
+ const deadline = Date.now() + params.timeoutMs;
3075
+ let clickedLeave = false;
3076
+ let clickedConfirmation = false;
3077
+ do {
3078
+ const step = parseMeetLeaveResult(await params.callBrowser({
3079
+ method: "POST",
3080
+ path: "/act",
3081
+ body: {
3082
+ kind: "evaluate",
3083
+ targetId: params.targetId,
3084
+ fn: meetLeaveScript(params.meetingUrl)
3085
+ },
3086
+ timeoutMs: params.timeoutMs
3087
+ }));
3088
+ clickedLeave ||= step.leaveAction === "leave";
3089
+ clickedConfirmation ||= step.leaveAction === "confirm";
3090
+ if (step.departed || step.urlMatched !== true) return {
3091
+ departed: step.departed,
3092
+ clickedLeave,
3093
+ clickedConfirmation,
3094
+ urlMatched: step.urlMatched
3095
+ };
3096
+ if (!step.leaveAction && !clickedLeave) return {
3097
+ departed: false,
3098
+ clickedLeave,
3099
+ clickedConfirmation,
3100
+ urlMatched: true
3101
+ };
3102
+ if (!step.leaveAction) await new Promise((resolve) => {
3103
+ setTimeout(resolve, 100);
3104
+ });
3105
+ } while (Date.now() < deadline);
3106
+ return {
3107
+ departed: false,
3108
+ clickedLeave,
3109
+ clickedConfirmation,
3110
+ urlMatched: true
3111
+ };
3112
+ }
3113
+ async function leaveMeetWithBrowserRequest(params) {
3114
+ if (!params.config.chrome.launch) return {
3115
+ left: false,
3116
+ note: "Browser leave skipped because chrome.launch is disabled."
3117
+ };
3118
+ const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 5e3);
3119
+ const { targetId, openedByPlugin } = params.tab;
3120
+ try {
3121
+ if (!asBrowserTabs(await params.callBrowser({
3122
+ method: "GET",
3123
+ path: "/tabs",
3124
+ timeoutMs
3125
+ })).find((entry) => entry.targetId === targetId)) return {
3126
+ left: true,
3127
+ note: "Meet tab is already closed."
3128
+ };
3129
+ let leaveResult;
3130
+ try {
3131
+ leaveResult = await leaveMeetInPage({
3132
+ callBrowser: params.callBrowser,
3133
+ meetingUrl: params.meetingUrl,
3134
+ targetId,
3135
+ timeoutMs
3136
+ });
3137
+ } catch (error) {
3138
+ return {
3139
+ left: false,
3140
+ note: `Browser control could not verify the Meet tab before leaving: ${error instanceof Error ? error.message : String(error)}`
3141
+ };
3142
+ }
3143
+ if (leaveResult.urlMatched === false) return {
3144
+ left: true,
3145
+ note: "Meet tab moved away from this session; left its current page untouched."
3146
+ };
3147
+ if (leaveResult.urlMatched !== true) return {
3148
+ left: false,
3149
+ note: "Browser control could not verify that the tracked tab still showed this meeting."
3150
+ };
3151
+ const { clickedLeave, departed } = leaveResult;
3152
+ if (!openedByPlugin) return {
3153
+ left: departed,
3154
+ 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."
3155
+ };
3156
+ await params.callBrowser({
3157
+ method: "DELETE",
3158
+ path: `/tabs/${targetId}`,
3159
+ timeoutMs
3160
+ });
3161
+ return {
3162
+ left: true,
3163
+ 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)."
3164
+ };
3165
+ } catch (error) {
3166
+ return {
3167
+ left: false,
3168
+ note: `Browser control could not leave the Meet tab: ${error instanceof Error ? error.message : String(error)}`
3169
+ };
3170
+ }
3171
+ }
3172
+ async function leaveChromeMeet(params) {
3173
+ return await leaveMeetWithBrowserRequest({
3174
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
3175
+ config: params.config,
3176
+ meetingUrl: params.meetingUrl,
3177
+ tab: params.tab
3178
+ });
3179
+ }
3180
+ async function readChromeMeetTranscript(params) {
3181
+ return await readMeetTranscriptWithBrowserRequest({
3182
+ callBrowser: await resolveLocalBrowserRequest(params.runtime),
3183
+ finalize: params.finalize === true,
3184
+ meetingUrl: params.meetingUrl,
3185
+ meetingSessionId: params.meetingSessionId,
3186
+ tab: params.tab,
3187
+ timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
3188
+ });
3189
+ }
3190
+ async function readChromeMeetTranscriptOnNode(params) {
3191
+ const nodeId = params.nodeId ?? await resolveChromeNode({
3192
+ runtime: params.runtime,
3193
+ requestedNode: params.config.chromeNode.node
3194
+ });
3195
+ const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
3196
+ return await readMeetTranscriptWithBrowserRequest({
3197
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3198
+ runtime: params.runtime,
3199
+ nodeId,
3200
+ method: request.method,
3201
+ path: request.path,
3202
+ body: request.body,
3203
+ timeoutMs: request.timeoutMs
3204
+ }),
3205
+ finalize: params.finalize === true,
3206
+ meetingUrl: params.meetingUrl,
3207
+ meetingSessionId: params.meetingSessionId,
3208
+ tab: params.tab,
3209
+ timeoutMs
3210
+ });
3211
+ }
3212
+ async function leaveChromeMeetOnNode(params) {
3213
+ const nodeId = params.nodeId ?? await resolveChromeNode({
3214
+ runtime: params.runtime,
3215
+ requestedNode: params.config.chromeNode.node
3216
+ });
3217
+ return await leaveMeetWithBrowserRequest({
3218
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3219
+ runtime: params.runtime,
3220
+ nodeId,
3221
+ method: request.method,
3222
+ path: request.path,
3223
+ body: request.body,
3224
+ timeoutMs: request.timeoutMs
3225
+ }),
3226
+ config: params.config,
3227
+ meetingUrl: params.meetingUrl,
3228
+ tab: params.tab
3229
+ });
3230
+ }
3231
+ async function openMeetWithBrowserProxy(params) {
3232
+ return await openMeetWithBrowserRequest({
3233
+ callBrowser: async (request) => await callBrowserProxyOnNode({
3234
+ runtime: params.runtime,
3235
+ nodeId: params.nodeId,
3236
+ method: request.method,
3237
+ path: request.path,
3238
+ body: request.body,
3239
+ timeoutMs: request.timeoutMs
3240
+ }),
3241
+ config: params.config,
3242
+ mode: params.mode,
3243
+ meetingSessionId: params.meetingSessionId,
3244
+ url: params.url
3245
+ });
3246
+ }
3247
+ async function openMeetWithBrowserRequest(params) {
3248
+ if (!params.config.chrome.launch) return { launched: false };
3249
+ const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
3250
+ let targetId;
3251
+ let tab;
3252
+ let openUrl = params.url;
3253
+ let openedByPlugin = false;
3254
+ if (params.config.chrome.reuseExistingTab) {
3255
+ const matchingTabs = asBrowserTabs(await params.callBrowser({
3256
+ method: "GET",
3257
+ path: "/tabs",
3258
+ timeoutMs: Math.min(timeoutMs, 5e3)
3259
+ })).filter((entry) => isSameMeetUrlForReuse(entry.url, params.url));
3260
+ const requestedAuthUser = readMeetAuthUser(params.url);
3261
+ tab = matchingTabs.find((entry) => isEnglishMeetTab(entry.url) && (!requestedAuthUser || readMeetAuthUser(entry.url) === requestedAuthUser));
3262
+ if (!tab) {
3263
+ if (!new URL(params.url).searchParams.has("authuser")) openUrl = matchingTabs.find((entry) => entry.url)?.url ?? params.url;
3264
+ }
3265
+ targetId = tab?.targetId;
3266
+ if (tab && targetId) await params.callBrowser({
3267
+ method: "POST",
3268
+ path: "/tabs/focus",
3269
+ body: { targetId },
3270
+ timeoutMs: Math.min(timeoutMs, 5e3)
3271
+ });
3272
+ }
3273
+ if (!targetId) {
3274
+ tab = readBrowserTab(await params.callBrowser({
3275
+ method: "POST",
3276
+ path: "/tabs/open",
3277
+ body: { url: forceMeetEnglishUi(openUrl) },
3278
+ timeoutMs
3279
+ }));
3280
+ targetId = tab?.targetId;
3281
+ openedByPlugin = Boolean(targetId);
3282
+ }
3283
+ if (!targetId) return {
3284
+ launched: true,
3285
+ browser: {
3286
+ status: "browser-control",
3287
+ notes: ["Browser proxy opened Meet but did not return a targetId."],
3288
+ browserUrl: tab?.url,
3289
+ browserTitle: tab?.title
3290
+ }
3291
+ };
3292
+ const tabIdentity = {
3293
+ targetId,
3294
+ openedByPlugin
3295
+ };
3296
+ const permissionNotes = await grantMeetMediaPermissions({
3297
+ allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
3298
+ callBrowser: params.callBrowser,
3299
+ targetId,
3300
+ timeoutMs
2662
3301
  });
2663
3302
  const deadline = Date.now() + Math.max(0, params.config.chrome.waitForInCallMs);
2664
3303
  let browser = {
@@ -2677,6 +3316,7 @@ async function openMeetWithBrowserRequest(params) {
2677
3316
  targetId,
2678
3317
  fn: meetStatusScript({
2679
3318
  allowMicrophone: isGoogleMeetTalkBackMode$2(params.mode),
3319
+ captionSessionId: params.meetingSessionId,
2680
3320
  captureCaptions: params.mode === "transcribe",
2681
3321
  guestName: params.config.chrome.guestName,
2682
3322
  autoJoin: params.config.chrome.autoJoin
@@ -2686,11 +3326,13 @@ async function openMeetWithBrowserRequest(params) {
2686
3326
  })) ?? browser, permissionNotes);
2687
3327
  if (browser?.inCall === true && (!isGoogleMeetTalkBackMode$2(params.mode) || browser.micMuted !== true)) return {
2688
3328
  launched: true,
2689
- browser
3329
+ browser,
3330
+ tab: tabIdentity
2690
3331
  };
2691
3332
  if (browser?.manualActionRequired === true) return {
2692
3333
  launched: true,
2693
- browser
3334
+ browser,
3335
+ tab: tabIdentity
2694
3336
  };
2695
3337
  } catch (error) {
2696
3338
  browser = {
@@ -2710,7 +3352,8 @@ async function openMeetWithBrowserRequest(params) {
2710
3352
  } while (Date.now() < deadline);
2711
3353
  return {
2712
3354
  launched: true,
2713
- browser
3355
+ browser,
3356
+ tab: tabIdentity
2714
3357
  };
2715
3358
  }
2716
3359
  function isRecoverableMeetTab(tab, url) {
@@ -2718,6 +3361,13 @@ function isRecoverableMeetTab(tab, url) {
2718
3361
  if (normalizeMeetUrlForReuse(tab.url)) return true;
2719
3362
  return (tab.url ?? "").startsWith("https://accounts.google.com/") && /sign in|google accounts|meet/i.test(tab.title ?? "");
2720
3363
  }
3364
+ function findRecoverableMeetTab(tabs, url) {
3365
+ const candidates = tabs.filter((tab) => isRecoverableMeetTab(tab, url));
3366
+ if (!url) return candidates[0];
3367
+ const requestedAuthUser = readMeetAuthUser(url);
3368
+ const accountCandidates = requestedAuthUser ? candidates.filter((tab) => readMeetAuthUser(tab.url) === requestedAuthUser) : candidates;
3369
+ return accountCandidates.find((tab) => isEnglishMeetTab(tab.url)) ?? accountCandidates[0];
3370
+ }
2721
3371
  async function inspectRecoverableMeetTab(params) {
2722
3372
  const allowMicrophone = params.mode !== "transcribe";
2723
3373
  await params.callBrowser({
@@ -2726,6 +3376,23 @@ async function inspectRecoverableMeetTab(params) {
2726
3376
  body: { targetId: params.targetId },
2727
3377
  timeoutMs: Math.min(params.timeoutMs, 5e3)
2728
3378
  });
3379
+ if (normalizeMeetUrlForReuse(params.tab.url) && !isEnglishMeetTab(params.tab.url)) {
3380
+ const manualActionMessage = "The existing Meet tab is not pinned to English. Open the meeting with ?hl=en, then retry recovery.";
3381
+ return {
3382
+ found: true,
3383
+ targetId: params.targetId,
3384
+ tab: params.tab,
3385
+ browser: {
3386
+ status: "browser-control",
3387
+ browserUrl: params.tab.url,
3388
+ browserTitle: params.tab.title,
3389
+ manualActionRequired: true,
3390
+ manualActionReason: "meet-locale-required",
3391
+ manualActionMessage
3392
+ },
3393
+ message: manualActionMessage
3394
+ };
3395
+ }
2729
3396
  const permissionNotes = params.readOnly ? [] : await grantMeetMediaPermissions({
2730
3397
  allowMicrophone,
2731
3398
  callBrowser: params.callBrowser,
@@ -2763,11 +3430,12 @@ async function inspectRecoverableMeetTab(params) {
2763
3430
  }
2764
3431
  async function recoverCurrentMeetTab(params) {
2765
3432
  const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
2766
- const tab = asBrowserTabs(await callLocalBrowserRequest({
3433
+ const callBrowser = await resolveLocalBrowserRequest(params.runtime);
3434
+ const tab = findRecoverableMeetTab(asBrowserTabs(await callBrowser({
2767
3435
  method: "GET",
2768
3436
  path: "/tabs",
2769
3437
  timeoutMs: Math.min(timeoutMs, 5e3)
2770
- })).find((entry) => isRecoverableMeetTab(entry, params.url));
3438
+ })), params.url);
2771
3439
  const targetId = tab?.targetId;
2772
3440
  if (!tab || !targetId) return {
2773
3441
  transport: "chrome",
@@ -2778,7 +3446,7 @@ async function recoverCurrentMeetTab(params) {
2778
3446
  return {
2779
3447
  transport: "chrome",
2780
3448
  ...await inspectRecoverableMeetTab({
2781
- callBrowser: callLocalBrowserRequest,
3449
+ callBrowser,
2782
3450
  config: params.config,
2783
3451
  mode: params.mode,
2784
3452
  readOnly: params.readOnly,
@@ -2794,13 +3462,13 @@ async function recoverCurrentMeetTabOnNode(params) {
2794
3462
  requestedNode: params.config.chromeNode.node
2795
3463
  });
2796
3464
  const timeoutMs = Math.max(1e3, params.config.chrome.joinTimeoutMs);
2797
- const tab = asBrowserTabs(await callBrowserProxyOnNode({
3465
+ const tab = findRecoverableMeetTab(asBrowserTabs(await callBrowserProxyOnNode({
2798
3466
  runtime: params.runtime,
2799
3467
  nodeId,
2800
3468
  method: "GET",
2801
3469
  path: "/tabs",
2802
3470
  timeoutMs: Math.min(timeoutMs, 5e3)
2803
- })).find((entry) => isRecoverableMeetTab(entry, params.url));
3471
+ })), params.url);
2804
3472
  const targetId = tab?.targetId;
2805
3473
  if (!tab || !targetId) return {
2806
3474
  transport: "chrome-node",
@@ -2854,6 +3522,7 @@ async function launchChromeMeetOnNode(params) {
2854
3522
  nodeId,
2855
3523
  config: params.config,
2856
3524
  mode: params.mode,
3525
+ meetingSessionId: params.meetingSessionId,
2857
3526
  url: params.url
2858
3527
  });
2859
3528
  const result = parseNodeStartResult(await params.runtime.nodes.invoke({
@@ -2895,19 +3564,22 @@ async function launchChromeMeetOnNode(params) {
2895
3564
  nodeId,
2896
3565
  launched: browserControl.launched || result.launched === true,
2897
3566
  audioBridge: bridge,
2898
- browser: browserControl.browser ?? result.browser
3567
+ browser: browserControl.browser ?? result.browser,
3568
+ tab: browserControl.tab
2899
3569
  };
2900
3570
  }
2901
3571
  if (result.audioBridge?.type === "external-command") return {
2902
3572
  nodeId,
2903
3573
  launched: browserControl.launched || result.launched === true,
2904
3574
  audioBridge: { type: "external-command" },
2905
- browser: browserControl.browser ?? result.browser
3575
+ browser: browserControl.browser ?? result.browser,
3576
+ tab: browserControl.tab
2906
3577
  };
2907
3578
  return {
2908
3579
  nodeId,
2909
3580
  launched: browserControl.launched || result.launched === true,
2910
- browser: browserControl.browser ?? result.browser
3581
+ browser: browserControl.browser ?? result.browser,
3582
+ tab: browserControl.tab
2911
3583
  };
2912
3584
  }
2913
3585
  //#endregion
@@ -3075,28 +3747,33 @@ function nowIso() {
3075
3747
  function buildTwilioVoiceCallSessionKey(meetingSessionId) {
3076
3748
  return `voice:google-meet:${meetingSessionId}`;
3077
3749
  }
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
3750
  function resolveTransport(input, config) {
3092
3751
  return input ?? config.defaultTransport;
3093
3752
  }
3094
3753
  function resolveMode(input, config) {
3095
3754
  return input === "realtime" ? "agent" : input ?? config.defaultMode;
3096
3755
  }
3756
+ function resolveSessionAgentId(request, config) {
3757
+ return normalizeAgentId(request.agentId ?? config.realtime.agentId);
3758
+ }
3759
+ function withSessionAgentConfig(config, agentId) {
3760
+ return config.realtime.agentId === agentId ? config : {
3761
+ ...config,
3762
+ realtime: {
3763
+ ...config.realtime,
3764
+ agentId
3765
+ }
3766
+ };
3767
+ }
3097
3768
  function isGoogleMeetTalkBackMode$1(mode) {
3098
3769
  return mode === "agent" || mode === "bidi";
3099
3770
  }
3771
+ function isBrowserTransport(transport) {
3772
+ return transport === "chrome" || transport === "chrome-node";
3773
+ }
3774
+ function isReusableMeetSession(session, params) {
3775
+ return session.state === "active" && isSameMeetUrlForReuse(session.url, params.url) && session.transport === params.transport && session.mode === params.mode && session.agentId === params.agentId;
3776
+ }
3100
3777
  function hasRealtimeAudioOutputAdvanced(health, startOutputBytes) {
3101
3778
  return (health?.lastOutputBytes ?? 0) > startOutputBytes;
3102
3779
  }
@@ -3181,9 +3858,16 @@ async function commandExists(runtime, command) {
3181
3858
  }
3182
3859
  var GoogleMeetRuntime = class {
3183
3860
  #sessions = /* @__PURE__ */ new Map();
3861
+ #sessionLeaves = /* @__PURE__ */ new Map();
3862
+ #browserMeetingLocks = /* @__PURE__ */ new Map();
3863
+ #createdBrowserTabs = /* @__PURE__ */ new Map();
3184
3864
  #sessionStops = /* @__PURE__ */ new Map();
3185
3865
  #sessionSpeakers = /* @__PURE__ */ new Map();
3186
3866
  #sessionHealth = /* @__PURE__ */ new Map();
3867
+ #transcripts = /* @__PURE__ */ new Map();
3868
+ #transcriptCaptures = /* @__PURE__ */ new Map();
3869
+ #transcriptFinalizing = /* @__PURE__ */ new Set();
3870
+ #retiredTranscripts = /* @__PURE__ */ new Set();
3187
3871
  #voiceCallGateway;
3188
3872
  constructor(params) {
3189
3873
  this.params = params;
@@ -3210,6 +3894,28 @@ var GoogleMeetRuntime = class {
3210
3894
  session
3211
3895
  } : { found: false };
3212
3896
  }
3897
+ async transcript(sessionId, options = {}) {
3898
+ const session = this.#sessions.get(sessionId);
3899
+ if (!session) return { found: false };
3900
+ if (session.mode !== "transcribe") throw new Error("transcript is only available for transcribe-mode sessions");
3901
+ const sinceIndex = options.sinceIndex ?? 0;
3902
+ if (!Number.isSafeInteger(sinceIndex) || sinceIndex < 0) throw new Error("sinceIndex must be a non-negative safe integer");
3903
+ if (session.state === "active" && !this.#transcriptFinalizing.has(session.id)) await this.#captureTranscriptSnapshot(session);
3904
+ const snapshot = this.#transcripts.get(sessionId) ?? {
3905
+ droppedLines: 0,
3906
+ lines: []
3907
+ };
3908
+ const startIndex = Math.max(sinceIndex, snapshot.droppedLines);
3909
+ return {
3910
+ found: true,
3911
+ sessionId,
3912
+ startIndex,
3913
+ nextIndex: snapshot.droppedLines + snapshot.lines.length,
3914
+ droppedLines: snapshot.droppedLines,
3915
+ ...session.transcriptEvicted ? { evicted: true } : {},
3916
+ lines: snapshot.lines.slice(startIndex - snapshot.droppedLines)
3917
+ };
3918
+ }
3213
3919
  async setupStatus(options = {}) {
3214
3920
  const transport = resolveTransport(options.transport, this.params.config);
3215
3921
  const mode = resolveMode(options.mode, this.params.config);
@@ -3273,10 +3979,12 @@ var GoogleMeetRuntime = class {
3273
3979
  return status;
3274
3980
  }
3275
3981
  async createViaBrowser() {
3276
- return createMeetWithBrowserProxyOnNode({
3982
+ const result = await createMeetWithBrowserProxyOnNode({
3277
3983
  runtime: this.params.runtime,
3278
3984
  config: this.params.config
3279
3985
  });
3986
+ if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
3987
+ return result;
3280
3988
  }
3281
3989
  async recoverCurrentTab(request = {}) {
3282
3990
  const transport = resolveTransport(request.transport, this.params.config);
@@ -3288,6 +3996,7 @@ var GoogleMeetRuntime = class {
3288
3996
  url
3289
3997
  });
3290
3998
  return recoverCurrentMeetTab({
3999
+ runtime: this.params.runtime,
3291
4000
  config: this.params.config,
3292
4001
  url
3293
4002
  });
@@ -3296,7 +4005,51 @@ var GoogleMeetRuntime = class {
3296
4005
  const url = normalizeMeetUrl(request.url);
3297
4006
  const transport = resolveTransport(request.transport, this.params.config);
3298
4007
  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);
4008
+ const agentId = resolveSessionAgentId(request, this.params.config);
4009
+ if (!isBrowserTransport(transport)) return await this.#joinUnlocked(request, {
4010
+ url,
4011
+ transport,
4012
+ mode,
4013
+ agentId
4014
+ });
4015
+ return await this.#withBrowserMeetingLock(transport, url, async () => await this.#joinUnlocked(request, {
4016
+ url,
4017
+ transport,
4018
+ mode,
4019
+ agentId
4020
+ }));
4021
+ }
4022
+ async #joinUnlocked(request, resolved) {
4023
+ const { url, transport, mode, agentId } = resolved;
4024
+ const activeSessions = this.list().filter((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport);
4025
+ const retainedBrowserTabs = [];
4026
+ if (isBrowserTransport(transport) && isGoogleMeetTalkBackMode$1(mode)) for (const session of activeSessions) {
4027
+ if (!isGoogleMeetTalkBackMode$1(session.mode) || isReusableMeetSession(session, {
4028
+ url,
4029
+ transport,
4030
+ mode,
4031
+ agentId
4032
+ })) continue;
4033
+ const tab = this.params.config.chrome.reuseExistingTab ? session.chrome?.browserTab : void 0;
4034
+ const keepBrowserParticipant = Boolean(tab) || session.chrome?.launched === false;
4035
+ if (tab) retainedBrowserTabs.push({
4036
+ session,
4037
+ tab
4038
+ });
4039
+ try {
4040
+ 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.");
4041
+ } catch (error) {
4042
+ await this.#settleRetainedBrowserTabs(retainedBrowserTabs);
4043
+ throw error;
4044
+ }
4045
+ noteSession(session, "Ended before the same Meet tab was reassigned to another agent.");
4046
+ }
4047
+ let reusable = activeSessions.find((session) => isReusableMeetSession(session, {
4048
+ url,
4049
+ transport,
4050
+ mode,
4051
+ agentId
4052
+ }));
3300
4053
  if (reusable?.transport === "twilio") {
3301
4054
  await this.#refreshTwilioVoiceCallStatus(reusable);
3302
4055
  if (reusable.state !== "active") reusable = void 0;
@@ -3319,6 +4072,7 @@ var GoogleMeetRuntime = class {
3319
4072
  url,
3320
4073
  transport,
3321
4074
  mode,
4075
+ agentId,
3322
4076
  state: "active",
3323
4077
  createdAt,
3324
4078
  updatedAt: createdAt,
@@ -3335,40 +4089,60 @@ var GoogleMeetRuntime = class {
3335
4089
  };
3336
4090
  try {
3337
4091
  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
4092
+ const chromeConfig = withSessionAgentConfig(this.params.config, agentId);
4093
+ let result;
4094
+ try {
4095
+ result = transport === "chrome-node" ? await launchChromeMeetOnNode({
4096
+ runtime: this.params.runtime,
4097
+ config: chromeConfig,
4098
+ fullConfig: this.params.fullConfig,
4099
+ meetingSessionId: session.id,
4100
+ requesterSessionKey: request.requesterSessionKey,
4101
+ mode,
4102
+ url,
4103
+ logger: this.params.logger
4104
+ }) : await launchChromeMeet({
4105
+ runtime: this.params.runtime,
4106
+ config: chromeConfig,
4107
+ fullConfig: this.params.fullConfig,
4108
+ meetingSessionId: session.id,
4109
+ requesterSessionKey: request.requesterSessionKey,
4110
+ mode,
4111
+ url,
4112
+ logger: this.params.logger
4113
+ });
4114
+ } catch (error) {
4115
+ await this.#settleRetainedBrowserTabs(retainedBrowserTabs);
4116
+ throw error;
4117
+ }
4118
+ const resultNodeId = "nodeId" in result ? result.nodeId : void 0;
4119
+ const browserTab = this.#inheritBrowserTabOwnership({
4120
+ transport,
4121
+ nodeId: resultNodeId,
4122
+ meetingUrl: url,
4123
+ tab: result.tab
3363
4124
  });
3364
4125
  session.chrome = {
3365
4126
  audioBackend: this.params.config.chrome.audioBackend,
3366
4127
  launched: result.launched,
3367
- nodeId: "nodeId" in result ? result.nodeId : void 0,
4128
+ nodeId: resultNodeId,
3368
4129
  browserProfile: this.params.config.chrome.browserProfile,
4130
+ browserTab,
3369
4131
  health: "browser" in result ? result.browser : void 0
3370
4132
  };
3371
4133
  this.#attachChromeAudioBridge(session, result.audioBridge);
4134
+ if (!await this.#settleRetainedBrowserTabs(retainedBrowserTabs, browserTab ? {
4135
+ transport,
4136
+ nodeId: resultNodeId,
4137
+ tab: browserTab
4138
+ } : void 0)) {
4139
+ try {
4140
+ await this.#leaveSession(session);
4141
+ } catch (error) {
4142
+ this.params.logger.warn(`[google-meet] replacement cleanup failed: ${formatErrorMessage(error)}`);
4143
+ }
4144
+ throw new Error("Could not leave the previous Meet browser tab before reassignment.");
4145
+ }
3372
4146
  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
4147
  this.#refreshSpeechReadiness(session);
3374
4148
  } else {
@@ -3379,6 +4153,7 @@ var GoogleMeetRuntime = class {
3379
4153
  dtmfSequence: request.dtmfSequence ?? this.params.config.twilio.defaultDtmfSequence
3380
4154
  });
3381
4155
  const dtmfSequence = request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
4156
+ const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? agentId : void 0;
3382
4157
  const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
3383
4158
  config: this.params.config,
3384
4159
  gateway: this.#voiceCallGateway,
@@ -3386,8 +4161,8 @@ var GoogleMeetRuntime = class {
3386
4161
  dtmfSequence,
3387
4162
  logger: this.params.logger,
3388
4163
  ...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
3389
- agentId: request.agentId,
3390
- sessionKey: request.agentId ? `agent:${request.agentId}:google-meet:${session.id}` : buildTwilioVoiceCallSessionKey(session.id),
4164
+ agentId: delegatedAgentId,
4165
+ sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : buildTwilioVoiceCallSessionKey(session.id),
3391
4166
  message: isGoogleMeetTalkBackMode$1(mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
3392
4167
  }) : void 0;
3393
4168
  delegatedTwilioSpoken = Boolean(voiceCallResult?.introSent);
@@ -3417,28 +4192,218 @@ var GoogleMeetRuntime = class {
3417
4192
  spoken: transport === "twilio" ? delegatedTwilioSpoken : isGoogleMeetTalkBackMode$1(mode) && speechInstructions ? await this.#speakWhenReady(session, speechInstructions) : false
3418
4193
  };
3419
4194
  }
3420
- async leave(sessionId) {
4195
+ async leave(sessionId, opts) {
4196
+ const session = this.#sessions.get(sessionId);
4197
+ if (!session) return { found: false };
4198
+ if (!isBrowserTransport(session.transport)) return await this.#leaveUnlocked(sessionId, opts);
4199
+ return await this.#withBrowserMeetingLock(session.transport, session.url, async () => await this.#leaveUnlocked(sessionId, opts));
4200
+ }
4201
+ async #leaveUnlocked(sessionId, opts) {
4202
+ const inFlight = this.#sessionLeaves.get(sessionId);
4203
+ if (inFlight) return await inFlight;
3421
4204
  const session = this.#sessions.get(sessionId);
3422
4205
  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);
4206
+ if (session.state === "ended") return {
4207
+ found: true,
4208
+ session,
4209
+ ...session.browserLeft === void 0 ? {} : { browserLeft: session.browserLeft }
4210
+ };
4211
+ const leave = this.#leaveSession(session, opts);
4212
+ this.#sessionLeaves.set(sessionId, leave);
4213
+ try {
4214
+ return await leave;
4215
+ } finally {
4216
+ if (this.#sessionLeaves.get(sessionId) === leave) this.#sessionLeaves.delete(sessionId);
4217
+ }
4218
+ }
4219
+ async #withBrowserMeetingLock(transport, url, operation) {
4220
+ const key = `${transport}:${normalizeMeetUrlForReuse(url) ?? url}`;
4221
+ const previous = this.#browserMeetingLocks.get(key) ?? Promise.resolve();
4222
+ let release;
4223
+ const gate = new Promise((resolve) => {
4224
+ release = resolve;
4225
+ });
4226
+ const tail = previous.then(() => gate);
4227
+ this.#browserMeetingLocks.set(key, tail);
4228
+ await previous;
4229
+ try {
4230
+ return await operation();
4231
+ } finally {
4232
+ release?.();
4233
+ if (this.#browserMeetingLocks.get(key) === tail) this.#browserMeetingLocks.delete(key);
4234
+ }
4235
+ }
4236
+ async #leaveSession(session, opts) {
4237
+ if (session.mode === "transcribe") {
4238
+ this.#transcriptFinalizing.add(session.id);
4239
+ await this.#captureTranscriptSnapshot(session, { finalize: true }).catch((error) => {
4240
+ this.params.logger.debug?.(`[google-meet] final transcript snapshot ignored: ${formatErrorMessage(error)}`);
4241
+ });
4242
+ }
4243
+ session.state = "ended";
4244
+ session.updatedAt = nowIso();
4245
+ const stop = this.#sessionStops.get(session.id);
4246
+ this.#sessionStops.delete(session.id);
4247
+ this.#sessionSpeakers.delete(session.id);
4248
+ this.#sessionHealth.delete(session.id);
4249
+ let browserLeft;
4250
+ try {
4251
+ await stop?.();
4252
+ } finally {
3428
4253
  try {
3429
- await stop();
4254
+ if (!opts?.keepBrowserTab) browserLeft = await this.#releaseBrowserTab(session);
3430
4255
  } finally {
3431
- session.state = "ended";
3432
- session.updatedAt = nowIso();
4256
+ this.#retireTranscript(session.id);
4257
+ this.#transcriptFinalizing.delete(session.id);
3433
4258
  }
3434
4259
  }
3435
- session.state = "ended";
3436
- session.updatedAt = nowIso();
3437
4260
  return {
3438
4261
  found: true,
3439
- session
4262
+ session,
4263
+ ...browserLeft === void 0 ? {} : { browserLeft }
3440
4264
  };
3441
4265
  }
4266
+ async #captureTranscriptSnapshot(session, options = {}) {
4267
+ const capture = (this.#transcriptCaptures.get(session.id) ?? Promise.resolve()).catch(() => {}).then(async () => {
4268
+ if (!isBrowserTransport(session.transport) || session.mode !== "transcribe") return;
4269
+ const tab = session.chrome?.browserTab;
4270
+ if (!tab) return;
4271
+ const snapshot = session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
4272
+ runtime: this.params.runtime,
4273
+ nodeId: session.chrome?.nodeId,
4274
+ config: this.params.config,
4275
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
4276
+ meetingUrl: session.url,
4277
+ meetingSessionId: session.id,
4278
+ tab
4279
+ }) : await readChromeMeetTranscript({
4280
+ runtime: this.params.runtime,
4281
+ config: this.params.config,
4282
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
4283
+ meetingUrl: session.url,
4284
+ meetingSessionId: session.id,
4285
+ tab
4286
+ });
4287
+ this.#mergeTranscriptSnapshot(session.id, snapshot);
4288
+ });
4289
+ this.#transcriptCaptures.set(session.id, capture);
4290
+ try {
4291
+ await capture;
4292
+ } finally {
4293
+ if (this.#transcriptCaptures.get(session.id) === capture) this.#transcriptCaptures.delete(session.id);
4294
+ }
4295
+ }
4296
+ #mergeTranscriptSnapshot(sessionId, snapshot) {
4297
+ const pageNextIndex = snapshot.droppedLines + snapshot.lines.length;
4298
+ const retained = this.#transcripts.get(sessionId);
4299
+ if (!retained) {
4300
+ this.#transcripts.set(sessionId, {
4301
+ droppedLines: snapshot.droppedLines,
4302
+ lines: snapshot.lines,
4303
+ pageEpoch: snapshot.epoch,
4304
+ pageNextIndex
4305
+ });
4306
+ return;
4307
+ }
4308
+ if (retained.pageEpoch !== snapshot.epoch) {
4309
+ retained.droppedLines += snapshot.droppedLines;
4310
+ retained.lines.push(...snapshot.lines);
4311
+ retained.pageEpoch = snapshot.epoch;
4312
+ retained.pageNextIndex = pageNextIndex;
4313
+ } else if (pageNextIndex > retained.pageNextIndex) {
4314
+ const appendFrom = Math.max(retained.pageNextIndex, snapshot.droppedLines);
4315
+ retained.droppedLines += Math.max(0, snapshot.droppedLines - retained.pageNextIndex);
4316
+ retained.lines.push(...snapshot.lines.slice(appendFrom - snapshot.droppedLines));
4317
+ retained.pageNextIndex = pageNextIndex;
4318
+ }
4319
+ const excess = retained.lines.length - GOOGLE_MEET_TRANSCRIPT_MAX_LINES;
4320
+ if (excess > 0) {
4321
+ retained.lines.splice(0, excess);
4322
+ retained.droppedLines += excess;
4323
+ }
4324
+ }
4325
+ #retireTranscript(sessionId) {
4326
+ const snapshot = this.#transcripts.get(sessionId);
4327
+ if (snapshot) {
4328
+ this.#transcripts.delete(sessionId);
4329
+ this.#transcripts.set(sessionId, snapshot);
4330
+ this.#retiredTranscripts.delete(sessionId);
4331
+ this.#retiredTranscripts.add(sessionId);
4332
+ }
4333
+ const retainedIds = [...this.#retiredTranscripts].filter((id) => this.#transcripts.has(id)).toSorted((a, b) => (this.#sessions.get(a)?.updatedAt ?? "").localeCompare(this.#sessions.get(b)?.updatedAt ?? ""));
4334
+ for (const id of retainedIds.slice(0, -4)) {
4335
+ this.#transcripts.delete(id);
4336
+ this.#retiredTranscripts.delete(id);
4337
+ const session = this.#sessions.get(id);
4338
+ if (session) session.transcriptEvicted = true;
4339
+ }
4340
+ }
4341
+ #inheritBrowserTabOwnership(params) {
4342
+ if (!params.tab) return;
4343
+ const tab = params.tab;
4344
+ const createdTabKey = params.transport === "chrome-node" && params.nodeId ? `${params.nodeId}:${tab.targetId}` : void 0;
4345
+ const createdMeetingUrl = createdTabKey ? this.#createdBrowserTabs.get(createdTabKey) : void 0;
4346
+ const inheritedFromCreate = isSameMeetUrlForReuse(createdMeetingUrl, params.meetingUrl);
4347
+ if (createdMeetingUrl && createdTabKey) this.#createdBrowserTabs.delete(createdTabKey);
4348
+ const inheritedFromSession = [...this.#sessions.values()].some((session) => {
4349
+ const trackedTab = session.chrome?.browserTab;
4350
+ if (!trackedTab) return false;
4351
+ return session.transport === params.transport && isSameMeetUrlForReuse(session.url, params.meetingUrl) && session.chrome?.nodeId === params.nodeId && trackedTab.targetId === tab.targetId && trackedTab.openedByPlugin;
4352
+ });
4353
+ return inheritedFromCreate || inheritedFromSession ? {
4354
+ ...tab,
4355
+ openedByPlugin: true
4356
+ } : tab;
4357
+ }
4358
+ async #settleRetainedBrowserTabs(retained, adopted) {
4359
+ let settled = true;
4360
+ for (const { session, tab } of retained.splice(0)) {
4361
+ if (adopted?.transport === session.transport && adopted.nodeId === session.chrome?.nodeId && adopted.tab.targetId === tab.targetId) {
4362
+ if (session.chrome) session.chrome.browserTab = void 0;
4363
+ continue;
4364
+ }
4365
+ if (await this.#releaseBrowserTab(session) === false) settled = false;
4366
+ }
4367
+ return settled;
4368
+ }
4369
+ async #releaseBrowserTab(session) {
4370
+ let browserLeft;
4371
+ try {
4372
+ browserLeft = await this.#leaveBrowserMeetTab(session);
4373
+ } catch (error) {
4374
+ noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
4375
+ browserLeft = false;
4376
+ }
4377
+ session.browserLeft = browserLeft;
4378
+ if (session.chrome && browserLeft !== false) session.chrome.browserTab = void 0;
4379
+ return browserLeft;
4380
+ }
4381
+ async #leaveBrowserMeetTab(session) {
4382
+ if (!isBrowserTransport(session.transport)) return;
4383
+ const tab = session.chrome?.browserTab;
4384
+ if (!tab) {
4385
+ noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
4386
+ return false;
4387
+ }
4388
+ 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)) {
4389
+ noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
4390
+ return;
4391
+ }
4392
+ const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
4393
+ runtime: this.params.runtime,
4394
+ nodeId: session.chrome?.nodeId,
4395
+ config: this.params.config,
4396
+ meetingUrl: session.url,
4397
+ tab
4398
+ }) : await leaveChromeMeet({
4399
+ runtime: this.params.runtime,
4400
+ config: this.params.config,
4401
+ meetingUrl: session.url,
4402
+ tab
4403
+ });
4404
+ noteSession(session, result.note);
4405
+ return result.left;
4406
+ }
3442
4407
  async speak(sessionId, instructions) {
3443
4408
  const session = this.#sessions.get(sessionId);
3444
4409
  if (!session) return {
@@ -3519,9 +4484,16 @@ var GoogleMeetRuntime = class {
3519
4484
  const mode = requestedMode && isGoogleMeetTalkBackMode$1(requestedMode) ? requestedMode : isGoogleMeetTalkBackMode$1(this.params.config.defaultMode) ? this.params.config.defaultMode : "agent";
3520
4485
  const url = normalizeMeetUrl(request.url);
3521
4486
  const transport = resolveTransport(request.transport, this.params.config);
4487
+ const agentId = resolveSessionAgentId(request, this.params.config);
3522
4488
  const beforeSessions = this.list();
3523
4489
  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;
4490
+ const existingSession = beforeSessions.find((session) => isReusableMeetSession(session, {
4491
+ url,
4492
+ transport,
4493
+ mode,
4494
+ agentId
4495
+ }));
4496
+ const existingOutputBytes = existingSession?.chrome?.health?.lastOutputBytes ?? 0;
3525
4497
  const result = await this.join({
3526
4498
  ...request,
3527
4499
  transport,
@@ -3529,6 +4501,7 @@ var GoogleMeetRuntime = class {
3529
4501
  mode,
3530
4502
  message: request.message ?? "Say exactly: Google Meet speech test complete."
3531
4503
  });
4504
+ const startOutputBytes = existingSession?.id === result.session.id ? existingOutputBytes : 0;
3532
4505
  let health = result.session.chrome?.health;
3533
4506
  const shouldWaitForOutput = result.spoken === true && health?.manualActionRequired !== true && this.#sessionHealth.has(result.session.id);
3534
4507
  if (shouldWaitForOutput && !hasRealtimeAudioOutputAdvanced(health, startOutputBytes)) {
@@ -3564,9 +4537,16 @@ var GoogleMeetRuntime = class {
3564
4537
  const url = normalizeMeetUrl(request.url);
3565
4538
  const transport = resolveTransport(request.transport, this.params.config);
3566
4539
  if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
4540
+ const agentId = resolveSessionAgentId(request, this.params.config);
3567
4541
  const beforeSessions = this.list();
3568
4542
  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);
4543
+ const existingSession = beforeSessions.find((session) => isReusableMeetSession(session, {
4544
+ url,
4545
+ transport,
4546
+ mode: "transcribe",
4547
+ agentId
4548
+ }));
4549
+ const existingStart = transcriptCheckpoint(existingSession?.chrome?.health);
3570
4550
  const result = await this.join({
3571
4551
  ...request,
3572
4552
  transport,
@@ -3574,6 +4554,7 @@ var GoogleMeetRuntime = class {
3574
4554
  mode: "transcribe",
3575
4555
  message: void 0
3576
4556
  });
4557
+ const start = existingSession?.id === result.session.id ? existingStart : transcriptCheckpoint(void 0);
3577
4558
  let health = result.session.chrome?.health;
3578
4559
  const timeoutMs = resolveProbeTimeoutMs(request.timeoutMs, this.params.config.chrome.joinTimeoutMs);
3579
4560
  const shouldWait = health?.manualActionRequired !== true && isManagedChromeBrowserSession(result.session);
@@ -3667,6 +4648,7 @@ var GoogleMeetRuntime = class {
3667
4648
  readOnly: options.readOnly,
3668
4649
  url: session.url
3669
4650
  }) : await recoverCurrentMeetTab({
4651
+ runtime: this.params.runtime,
3670
4652
  config: this.params.config,
3671
4653
  mode: session.mode,
3672
4654
  readOnly: options.readOnly,
@@ -3700,12 +4682,13 @@ var GoogleMeetRuntime = class {
3700
4682
  if (!isGoogleMeetTalkBackMode$1(session.mode) || session.transport !== "chrome" || session.state !== "active" || !session.chrome || session.chrome.audioBridge) return;
3701
4683
  const health = session.chrome.health;
3702
4684
  if (health?.inCall !== true || health.micMuted === true || health.manualActionRequired === true) return;
4685
+ const sessionConfig = withSessionAgentConfig(this.params.config, session.agentId);
3703
4686
  const result = await launchChromeMeet({
3704
4687
  runtime: this.params.runtime,
3705
4688
  config: {
3706
- ...this.params.config,
4689
+ ...sessionConfig,
3707
4690
  chrome: {
3708
- ...this.params.config.chrome,
4691
+ ...sessionConfig.chrome,
3709
4692
  launch: false
3710
4693
  }
3711
4694
  },
@@ -3744,154 +4727,9 @@ var GoogleMeetRuntime = class {
3744
4727
  }
3745
4728
  };
3746
4729
  //#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
4730
  //#region extensions/google-meet/index.ts
3893
- const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-CkkaRlbI.js"));
3894
- const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-D364Yrym.js"));
4731
+ const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-CWcFI0ub.js"));
4732
+ const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-SodPSOFX.js"));
3895
4733
  const googleMeetConfigSchema = {
3896
4734
  parse(value) {
3897
4735
  return resolveGoogleMeetConfig(value);
@@ -4099,6 +4937,7 @@ const GoogleMeetToolSchema = Type.Object({
4099
4937
  "join",
4100
4938
  "create",
4101
4939
  "status",
4940
+ "transcript",
4102
4941
  "setup_status",
4103
4942
  "resolve_space",
4104
4943
  "preflight",
@@ -4150,6 +4989,10 @@ const GoogleMeetToolSchema = Type.Object({
4150
4989
  pin: Type.Optional(Type.String({ description: "Meet phone PIN for Twilio; # is appended if omitted" })),
4151
4990
  dtmfSequence: Type.Optional(Type.String({ description: "Explicit DTMF sequence for Twilio" })),
4152
4991
  sessionId: Type.Optional(Type.String({ description: "Meet session ID" })),
4992
+ sinceIndex: Type.Optional(Type.Integer({
4993
+ description: "For transcript, resume from the previous response's nextIndex.",
4994
+ minimum: 0
4995
+ })),
4153
4996
  message: Type.Optional(Type.String({ description: "Realtime instructions to speak now" })),
4154
4997
  timeoutMs: optionalPositiveIntegerSchema({ description: "Probe timeout in milliseconds" }),
4155
4998
  meeting: Type.Optional(Type.String({ description: "Meet URL, meeting code, or spaces/{id}" })),
@@ -4236,11 +5079,17 @@ function readGatewayErrorDetails(err) {
4236
5079
  }
4237
5080
  async function callGoogleMeetGatewayFromTool(params) {
4238
5081
  try {
4239
- if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, { timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config) });
5082
+ if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, {
5083
+ timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config),
5084
+ scopes: ["operator.admin"]
5085
+ });
4240
5086
  return await googleMeetToolDeps.callGatewayFromCli(googleMeetGatewayMethodForToolAction(params.action), {
4241
5087
  json: true,
4242
5088
  timeout: String(resolveGoogleMeetGatewayOperationTimeoutMs(params.config))
4243
- }, params.raw, { progress: false });
5089
+ }, params.raw, {
5090
+ progress: false,
5091
+ scopes: ["operator.admin"]
5092
+ });
4244
5093
  } catch (err) {
4245
5094
  const details = readGatewayErrorDetails(err);
4246
5095
  if (details && typeof details === "object") return details;
@@ -4263,7 +5112,7 @@ async function createAndJoinMeetFromParams(params) {
4263
5112
  return (await loadGoogleMeetCreateModule()).createAndJoinMeetFromParams(params);
4264
5113
  }
4265
5114
  async function resolveGoogleMeetTokenFromParams(config, raw) {
4266
- const { resolveGoogleMeetAccessToken } = await import("./oauth-75iPDzIn.js");
5115
+ const { resolveGoogleMeetAccessToken } = await import("./oauth-DOfMKlGp.js");
4267
5116
  return resolveGoogleMeetAccessToken({
4268
5117
  clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
4269
5118
  clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
@@ -4462,6 +5311,23 @@ var google_meet_default = definePluginEntry({
4462
5311
  sendError(respond, err);
4463
5312
  }
4464
5313
  });
5314
+ api.registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
5315
+ try {
5316
+ const sessionId = normalizeOptionalString(params?.sessionId);
5317
+ if (!sessionId) {
5318
+ sendError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
5319
+ return;
5320
+ }
5321
+ const sinceIndex = params?.sinceIndex;
5322
+ if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
5323
+ sendError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), ErrorCodes.INVALID_REQUEST);
5324
+ return;
5325
+ }
5326
+ respond(true, await (await ensureRuntime()).transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
5327
+ } catch (err) {
5328
+ sendError(respond, err);
5329
+ }
5330
+ });
4465
5331
  api.registerGatewayMethod("googlemeet.recoverCurrentTab", async ({ params, respond }) => {
4466
5332
  try {
4467
5333
  respond(true, await (await ensureRuntime()).recoverCurrentTab({
@@ -4613,13 +5479,15 @@ var google_meet_default = definePluginEntry({
4613
5479
  sendError(respond, err);
4614
5480
  }
4615
5481
  });
4616
- api.registerGatewayMethod("googlemeet.testListen", async ({ params, respond }) => {
5482
+ api.registerGatewayMethod("googlemeet.testListen", async ({ params, client, respond }) => {
4617
5483
  try {
5484
+ const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
4618
5485
  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")
5486
+ url: resolveMeetingInput(config, trustedParams.url),
5487
+ transport: normalizeTransport(trustedParams.transport),
5488
+ mode: normalizeMode(trustedParams.mode),
5489
+ agentId: normalizeOptionalString(trustedParams.agentId),
5490
+ timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs")
4623
5491
  }));
4624
5492
  } catch (err) {
4625
5493
  sendError(respond, err);
@@ -4633,10 +5501,12 @@ var google_meet_default = definePluginEntry({
4633
5501
  async execute(_toolCallId, params) {
4634
5502
  const raw = asParamRecord(params);
4635
5503
  const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
4636
- const agentId = toolContext.agentId ? normalizeAgentId(toolContext.agentId) : void 0;
5504
+ const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
5505
+ const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
4637
5506
  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.");
5507
+ const needsTrustedAgentRouting = Boolean(agentId && agentId !== "main");
5508
+ const useTrustedRuntime = needsTrustedAgentRouting ? await api.runtime.gateway.isAvailable() : false;
5509
+ if (needsTrustedAgentRouting && !useTrustedRuntime) throw new Error("Per-agent Google Meet routing requires a Gateway-hosted agent run.");
4640
5510
  const rawWithRequester = {
4641
5511
  ...raw,
4642
5512
  ...requesterSessionKey ? { requesterSessionKey } : {},
@@ -4668,13 +5538,19 @@ var google_meet_default = definePluginEntry({
4668
5538
  case "test_listen": return jsonResult(await callGoogleMeetGatewayFromTool({
4669
5539
  config,
4670
5540
  action: "test_listen",
4671
- raw
5541
+ raw: rawWithRequester,
5542
+ runtime: useTrustedRuntime ? api.runtime : void 0
4672
5543
  }));
4673
5544
  case "status": return jsonResult(await callGoogleMeetGatewayFromTool({
4674
5545
  config,
4675
5546
  action: "status",
4676
5547
  raw
4677
5548
  }));
5549
+ case "transcript": return jsonResult(await callGoogleMeetGatewayFromTool({
5550
+ config,
5551
+ action: "transcript",
5552
+ raw
5553
+ }));
4678
5554
  case "recover_current_tab": return jsonResult(await callGoogleMeetGatewayFromTool({
4679
5555
  config,
4680
5556
  action: "recover_current_tab",