@openclaw/google-meet 2026.5.24-beta.2 → 2026.5.26-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.
@@ -1,4 +1,4 @@
1
- import { _ as googleApiError } from "./chrome-create-BTmb0Dnh.js";
1
+ import { _ as googleApiError } from "./chrome-create-B3zyjf5f.js";
2
2
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
3
3
  //#region extensions/google-meet/src/calendar.ts
4
4
  const GOOGLE_CALENDAR_API_BASE_URL = "https://www.googleapis.com/calendar/v3";
@@ -1,3 +1,4 @@
1
+ import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
1
2
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
2
3
  import { sleep } from "openclaw/plugin-sdk/runtime-env";
3
4
  //#region extensions/google-meet/src/google-api-errors.ts
@@ -458,7 +459,7 @@ function mergeAttendanceRows(rows, conferenceRecord, params) {
458
459
  });
459
460
  continue;
460
461
  }
461
- existing.participants = [...new Set([...existing.participants ?? [existing.participant], row.participant])];
462
+ existing.participants = uniqueStrings([...existing.participants ?? [existing.participant], row.participant]);
462
463
  existing.sessions.push(...row.sessions);
463
464
  existing.displayName ??= row.displayName;
464
465
  existing.user ??= row.user;
@@ -1,7 +1,7 @@
1
- import { d as createGoogleMeetSpace, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, m as fetchGoogleMeetAttendance, p as fetchGoogleMeetArtifacts, u as buildGoogleMeetPreflightReport } from "./chrome-create-BTmb0Dnh.js";
2
- import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-D2fOQKE2.js";
1
+ import { d as createGoogleMeetSpace, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, m as fetchGoogleMeetAttendance, p as fetchGoogleMeetArtifacts, u as buildGoogleMeetPreflightReport } from "./chrome-create-B3zyjf5f.js";
2
+ import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-IxWkFtfn.js";
3
3
  import { buildGoogleMeetAuthUrl, createGoogleMeetOAuthState, createGoogleMeetPkce, exchangeGoogleMeetAuthCode, resolveGoogleMeetAccessToken, waitForGoogleMeetAuthCode } from "./oauth-Byt1wS27.js";
4
- import { hasCreateSpaceConfigInput, resolveCreateSpaceConfig } from "./create-CaFygLla.js";
4
+ import { hasCreateSpaceConfigInput, resolveCreateSpaceConfig } from "./create-Q8KLuYnE.js";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
7
7
  import path from "node:path";
@@ -1,4 +1,4 @@
1
- import { d as createGoogleMeetSpace, t as createMeetWithBrowserProxyOnNode } from "./chrome-create-BTmb0Dnh.js";
1
+ import { d as createGoogleMeetSpace, t as createMeetWithBrowserProxyOnNode } from "./chrome-create-B3zyjf5f.js";
2
2
  import { resolveGoogleMeetAccessToken } from "./oauth-Byt1wS27.js";
3
3
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  //#region extensions/google-meet/src/create.ts
@@ -1,16 +1,11 @@
1
+ import { asNullableRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
1
2
  //#region extensions/google-meet/src/config-compat.ts
2
- function asRecord(value) {
3
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4
- }
5
- function normalizeProviderId(value) {
6
- return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : void 0;
7
- }
8
3
  function hasOwn(record, key) {
9
4
  return Object.prototype.hasOwnProperty.call(record, key);
10
5
  }
11
6
  function hasLegacyGoogleRealtimeProvider(value) {
12
- const realtime = asRecord(value);
13
- if (!realtime || normalizeProviderId(realtime.provider) !== "google") return false;
7
+ const realtime = asNullableRecord(value);
8
+ if (!realtime || normalizeOptionalLowercaseString(realtime.provider) !== "google") return false;
14
9
  return !hasOwn(realtime, "voiceProvider") || !hasOwn(realtime, "transcriptionProvider");
15
10
  }
16
11
  const legacyConfigRules = [{
@@ -25,18 +20,18 @@ const legacyConfigRules = [{
25
20
  match: hasLegacyGoogleRealtimeProvider
26
21
  }];
27
22
  function migrateGoogleMeetLegacyRealtimeProvider(config) {
28
- const rawRealtime = asRecord(asRecord(asRecord(config.plugins?.entries?.["google-meet"])?.config)?.realtime);
23
+ const rawRealtime = asNullableRecord(asNullableRecord(asNullableRecord(config.plugins?.entries?.["google-meet"])?.config)?.realtime);
29
24
  if (!rawRealtime || !hasLegacyGoogleRealtimeProvider(rawRealtime)) return null;
30
25
  const nextConfig = structuredClone(config);
31
- const nextPlugins = asRecord(nextConfig.plugins) ?? {};
26
+ const nextPlugins = asNullableRecord(nextConfig.plugins) ?? {};
32
27
  nextConfig.plugins = nextPlugins;
33
- const nextEntries = asRecord(nextPlugins.entries) ?? {};
28
+ const nextEntries = asNullableRecord(nextPlugins.entries) ?? {};
34
29
  nextPlugins.entries = nextEntries;
35
- const nextEntry = asRecord(nextEntries["google-meet"]) ?? {};
30
+ const nextEntry = asNullableRecord(nextEntries["google-meet"]) ?? {};
36
31
  nextEntries["google-meet"] = nextEntry;
37
- const nextPluginConfig = asRecord(nextEntry.config) ?? {};
32
+ const nextPluginConfig = asNullableRecord(nextEntry.config) ?? {};
38
33
  nextEntry.config = nextPluginConfig;
39
- const nextRealtime = asRecord(nextPluginConfig.realtime) ?? {};
34
+ const nextRealtime = asNullableRecord(nextPluginConfig.realtime) ?? {};
40
35
  nextPluginConfig.realtime = nextRealtime;
41
36
  nextRealtime.provider = "openai";
42
37
  if (!hasOwn(nextRealtime, "transcriptionProvider")) nextRealtime.transcriptionProvider = "openai";
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { a as isSameMeetUrlForReuse, c as resolveChromeNode, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, i as callBrowserProxyOnNode, l as resolveChromeNodeInfo, m as fetchGoogleMeetAttendance, n as isGoogleMeetBrowserManualActionError, o as normalizeMeetUrlForReuse, p as fetchGoogleMeetArtifacts, r as asBrowserTabs, s as readBrowserTab, t as createMeetWithBrowserProxyOnNode, u as buildGoogleMeetPreflightReport } from "./chrome-create-BTmb0Dnh.js";
2
- import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-D2fOQKE2.js";
1
+ import { a as isSameMeetUrlForReuse, c as resolveChromeNode, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, i as callBrowserProxyOnNode, l as resolveChromeNodeInfo, m as fetchGoogleMeetAttendance, n as isGoogleMeetBrowserManualActionError, o as normalizeMeetUrlForReuse, p as fetchGoogleMeetArtifacts, r as asBrowserTabs, s as readBrowserTab, t as createMeetWithBrowserProxyOnNode, u as buildGoogleMeetPreflightReport } from "./chrome-create-B3zyjf5f.js";
2
+ import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-IxWkFtfn.js";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { ErrorCodes, GatewayClient, callGatewayFromCli, errorShape, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
5
5
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
6
- import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { asRecord, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeOptionalTrimmedStringList, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { Type } from "typebox";
8
8
  import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
9
- import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentConsultWorkingResponse, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createRealtimeVoiceAgentTalkbackQueue, createRealtimeVoiceBridgeSession, createTalkSessionController, extendRealtimeVoiceOutputEchoSuppression, getRealtimeVoiceBridgeEventHealth, getRealtimeVoiceTranscriptHealth, isLikelyRealtimeVoiceAssistantEchoTranscript, mulawToPcm, recordRealtimeVoiceBridgeEvent, recordRealtimeVoiceTranscript, recordTalkObservabilityEvent, resamplePcm, resolveConfiguredRealtimeVoiceProvider, resolveRealtimeVoiceAgentConsultToolPolicy, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow } from "openclaw/plugin-sdk/realtime-voice";
9
+ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentConsultWorkingResponse, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createRealtimeVoiceAgentTalkbackQueue, createRealtimeVoiceBridgeSession, createRealtimeVoiceOutputActivityTracker, createTalkSessionController, extendRealtimeVoiceOutputEchoSuppression, getRealtimeVoiceBridgeEventHealth, getRealtimeVoiceTranscriptHealth, isLikelyRealtimeVoiceAssistantEchoTranscript, mulawToPcm, recordRealtimeVoiceBridgeEvent, recordRealtimeVoiceTranscript, recordTalkObservabilityEvent, resamplePcm, resolveConfiguredRealtimeVoiceProvider, resolveRealtimeVoiceAgentConsultToolPolicy, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow } from "openclaw/plugin-sdk/realtime-voice";
10
10
  import { spawn, spawnSync } from "node:child_process";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -154,9 +154,6 @@ const GOOGLE_MEET_ACCESS_TOKEN_KEYS = ["OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN", "GOO
154
154
  const GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT_KEYS = ["OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT", "GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT"];
155
155
  const GOOGLE_MEET_DEFAULT_MEETING_KEYS = ["OPENCLAW_GOOGLE_MEET_DEFAULT_MEETING", "GOOGLE_MEET_DEFAULT_MEETING"];
156
156
  const GOOGLE_MEET_PREVIEW_ACK_KEYS = ["OPENCLAW_GOOGLE_MEET_PREVIEW_ACK", "GOOGLE_MEET_PREVIEW_ACK"];
157
- function asRecord$3(value) {
158
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
159
- }
160
157
  function resolveBoolean(value, fallback) {
161
158
  return typeof value === "boolean" ? value : fallback;
162
159
  }
@@ -199,17 +196,15 @@ function readEnvNumber(env, keys) {
199
196
  return resolveOptionalNumber(readEnvString(env, keys));
200
197
  }
201
198
  function resolveStringArray(value) {
202
- if (!Array.isArray(value)) return;
203
- const normalized = value.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry));
204
- return normalized.length > 0 ? normalized : void 0;
199
+ return normalizeOptionalTrimmedStringList(value);
205
200
  }
206
201
  function resolveProvidersConfig(value) {
207
- const raw = asRecord$3(value);
202
+ const raw = asRecord(value);
208
203
  const providers = {};
209
204
  for (const [key, entry] of Object.entries(raw)) {
210
205
  const providerId = normalizeOptionalLowercaseString(key);
211
206
  if (!providerId) continue;
212
- providers[providerId] = asRecord$3(entry);
207
+ providers[providerId] = asRecord(entry);
213
208
  }
214
209
  return providers;
215
210
  }
@@ -255,23 +250,23 @@ function resolveGoogleMeetConfig(input) {
255
250
  return resolveGoogleMeetConfigWithEnv(input);
256
251
  }
257
252
  function resolveGoogleMeetConfigWithEnv(input, env = process.env) {
258
- const raw = asRecord$3(input);
259
- const defaults = asRecord$3(raw.defaults);
260
- const preview = asRecord$3(raw.preview);
261
- const chrome = asRecord$3(raw.chrome);
253
+ const raw = asRecord(input);
254
+ const defaults = asRecord(raw.defaults);
255
+ const preview = asRecord(raw.preview);
256
+ const chrome = asRecord(raw.chrome);
262
257
  const configuredAudioInputCommand = resolveStringArray(chrome.audioInputCommand);
263
258
  const configuredAudioOutputCommand = resolveStringArray(chrome.audioOutputCommand);
264
259
  const hasCustomAudioCommand = configuredAudioInputCommand !== void 0 || configuredAudioOutputCommand !== void 0;
265
260
  const audioFormat = resolveChromeAudioFormat(chrome.audioFormat) ?? (hasCustomAudioCommand ? "g711-ulaw-8khz" : DEFAULT_GOOGLE_MEET_CONFIG.chrome.audioFormat);
266
261
  const audioBufferBytes = resolveAudioBufferBytes(chrome.audioBufferBytes, DEFAULT_GOOGLE_MEET_CONFIG.chrome.audioBufferBytes);
267
- const chromeNode = asRecord$3(raw.chromeNode);
268
- const twilio = asRecord$3(raw.twilio);
269
- const voiceCall = asRecord$3(raw.voiceCall);
270
- const realtime = asRecord$3(raw.realtime);
262
+ const chromeNode = asRecord(raw.chromeNode);
263
+ const twilio = asRecord(raw.twilio);
264
+ const voiceCall = asRecord(raw.voiceCall);
265
+ const realtime = asRecord(raw.realtime);
271
266
  const realtimeProvider = normalizeOptionalString(realtime.provider);
272
267
  const resolvedRealtimeProvider = realtimeProvider ?? DEFAULT_GOOGLE_MEET_CONFIG.realtime.provider;
273
- const oauth = asRecord$3(raw.oauth);
274
- const auth = asRecord$3(raw.auth);
268
+ const oauth = asRecord(raw.oauth);
269
+ const auth = asRecord(raw.auth);
275
270
  return {
276
271
  enabled: resolveBoolean(raw.enabled, DEFAULT_GOOGLE_MEET_CONFIG.enabled),
277
272
  defaults: { meeting: normalizeOptionalString(defaults.meeting) ?? readEnvString(env, GOOGLE_MEET_DEFAULT_MEETING_KEYS) },
@@ -503,6 +498,16 @@ function extendGoogleMeetOutputEchoSuppression(params) {
503
498
  tailMs: GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS
504
499
  });
505
500
  }
501
+ function recordGoogleMeetOutputActivity(params) {
502
+ const suppression = extendGoogleMeetOutputEchoSuppression(params);
503
+ params.tracker.markPlaybackStarted();
504
+ params.tracker.markAudio({
505
+ audioMs: suppression.durationMs,
506
+ sourceAudioBytes: params.audio.byteLength,
507
+ sinkAudioBytes: params.audio.byteLength
508
+ });
509
+ return suppression;
510
+ }
506
511
  function resolveGoogleMeetRealtimeAudioFormat(config) {
507
512
  return config.chrome.audioFormat === "g711-ulaw-8khz" ? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ : REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ;
508
513
  }
@@ -667,7 +672,7 @@ async function startCommandAgentAudioBridge(params) {
667
672
  let lastInputAt;
668
673
  let lastOutputAt;
669
674
  let lastInputBytes = 0;
670
- let lastOutputBytes = 0;
675
+ const outputActivity = createRealtimeVoiceOutputActivityTracker();
671
676
  let suppressedInputBytes = 0;
672
677
  let lastSuppressedInputAt;
673
678
  let suppressInputUntil = 0;
@@ -765,7 +770,8 @@ async function startCommandAgentAudioBridge(params) {
765
770
  params.logger.debug?.(`[google-meet] audio output: ${String(chunk).trim()}`);
766
771
  });
767
772
  const writeOutputAudio = (audio) => {
768
- const suppression = extendGoogleMeetOutputEchoSuppression({
773
+ const suppression = recordGoogleMeetOutputActivity({
774
+ tracker: outputActivity,
769
775
  audio,
770
776
  audioFormat: params.config.chrome.audioFormat,
771
777
  nowMs: Date.now(),
@@ -775,7 +781,6 @@ async function startCommandAgentAudioBridge(params) {
775
781
  suppressInputUntil = suppression.suppressInputUntilMs;
776
782
  lastOutputPlayableUntilMs = suppression.lastOutputPlayableUntilMs;
777
783
  lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
778
- lastOutputBytes += audio.byteLength;
779
784
  emitTalkEvent({
780
785
  type: "output.audio.delta",
781
786
  turnId: ensureTalkTurn(),
@@ -941,12 +946,12 @@ async function startCommandAgentAudioBridge(params) {
941
946
  providerConnected: sttSession?.isConnected() ?? false,
942
947
  realtimeReady,
943
948
  audioInputActive: lastInputBytes > 0,
944
- audioOutputActive: lastOutputBytes > 0,
949
+ audioOutputActive: outputActivity.isActive(),
945
950
  lastInputAt,
946
951
  lastOutputAt,
947
952
  lastSuppressedInputAt,
948
953
  lastInputBytes,
949
- lastOutputBytes,
954
+ lastOutputBytes: outputActivity.snapshot().sinkAudioBytes,
950
955
  suppressedInputBytes,
951
956
  ...getGoogleMeetRealtimeTranscriptHealth(transcript),
952
957
  recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
@@ -976,18 +981,18 @@ async function startCommandRealtimeAudioBridge(params) {
976
981
  let lastInputAt;
977
982
  let lastOutputAt;
978
983
  let lastInputBytes = 0;
979
- let lastOutputBytes = 0;
984
+ const outputActivity = createRealtimeVoiceOutputActivityTracker();
980
985
  let lastClearAt;
981
986
  let clearCount = 0;
982
987
  let suppressedInputBytes = 0;
983
988
  let lastSuppressedInputAt;
984
989
  let suppressInputUntil = 0;
985
- let lastOutputAtMs = 0;
986
990
  let lastOutputPlayableUntilMs = 0;
987
991
  let bargeInInputProcess;
988
992
  let agentTalkback;
989
993
  const suppressInputForOutput = (audio) => {
990
- const suppression = extendGoogleMeetOutputEchoSuppression({
994
+ const suppression = recordGoogleMeetOutputActivity({
995
+ tracker: outputActivity,
991
996
  audio,
992
997
  audioFormat: params.config.chrome.audioFormat,
993
998
  nowMs: Date.now(),
@@ -1082,9 +1087,11 @@ async function startCommandRealtimeAudioBridge(params) {
1082
1087
  "pipe"
1083
1088
  ] });
1084
1089
  bargeInInputProcess.stdout?.on("data", (chunk) => {
1085
- if (stopped || lastOutputAtMs === 0) return;
1090
+ if (stopped || !outputActivity.isInterruptible()) return;
1086
1091
  const now = Date.now();
1087
- if (!(now <= Math.max(lastOutputPlayableUntilMs, suppressInputUntil)) && now - lastOutputAtMs > 1e3) return;
1092
+ const playbackActive = now <= Math.max(lastOutputPlayableUntilMs, suppressInputUntil);
1093
+ const lastOutputAudioAt = outputActivity.snapshot().lastAudioAt;
1094
+ if (!playbackActive && (lastOutputAudioAt === void 0 || now - lastOutputAudioAt > 1e3)) return;
1088
1095
  if (now - lastBargeInAt < params.config.chrome.bargeInCooldownMs) return;
1089
1096
  const stats = readPcm16Stats(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1090
1097
  if (stats.rms < params.config.chrome.bargeInRmsThreshold && stats.peak < params.config.chrome.bargeInPeakThreshold) return;
@@ -1209,9 +1216,7 @@ async function startCommandRealtimeAudioBridge(params) {
1209
1216
  turnId,
1210
1217
  payload: { byteLength: audio.byteLength }
1211
1218
  });
1212
- lastOutputAtMs = Date.now();
1213
1219
  lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
1214
- lastOutputBytes += audio.byteLength;
1215
1220
  suppressInputForOutput(audio);
1216
1221
  writeOutputAudio(audio);
1217
1222
  },
@@ -1366,12 +1371,12 @@ async function startCommandRealtimeAudioBridge(params) {
1366
1371
  providerConnected: bridge?.bridge.isConnected() ?? false,
1367
1372
  realtimeReady,
1368
1373
  audioInputActive: lastInputBytes > 0,
1369
- audioOutputActive: lastOutputBytes > 0,
1374
+ audioOutputActive: outputActivity.isActive(),
1370
1375
  lastInputAt,
1371
1376
  lastOutputAt,
1372
1377
  lastSuppressedInputAt,
1373
1378
  lastInputBytes,
1374
- lastOutputBytes,
1379
+ lastOutputBytes: outputActivity.snapshot().sinkAudioBytes,
1375
1380
  suppressedInputBytes,
1376
1381
  ...getGoogleMeetRealtimeTranscriptHealth(transcript),
1377
1382
  ...getGoogleMeetRealtimeEventHealth(realtimeEvents),
@@ -1385,10 +1390,10 @@ async function startCommandRealtimeAudioBridge(params) {
1385
1390
  }
1386
1391
  //#endregion
1387
1392
  //#region extensions/google-meet/src/realtime-node.ts
1388
- function asRecord$2(value) {
1393
+ function asRecord$1(value) {
1389
1394
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1390
1395
  }
1391
- function readString$1(value) {
1396
+ function readString(value) {
1392
1397
  return typeof value === "string" && value.trim() ? value : void 0;
1393
1398
  }
1394
1399
  function normalizeGoogleMeetTtsPromptText(text) {
@@ -1405,7 +1410,7 @@ async function startNodeAgentAudioBridge(params) {
1405
1410
  let lastInputAt;
1406
1411
  let lastOutputAt;
1407
1412
  let lastInputBytes = 0;
1408
- let lastOutputBytes = 0;
1413
+ const outputActivity = createRealtimeVoiceOutputActivityTracker();
1409
1414
  let suppressedInputBytes = 0;
1410
1415
  let lastSuppressedInputAt;
1411
1416
  let suppressInputUntil = 0;
@@ -1449,7 +1454,8 @@ async function startNodeAgentAudioBridge(params) {
1449
1454
  }
1450
1455
  };
1451
1456
  const pushOutputAudio = async (audio) => {
1452
- const suppression = extendGoogleMeetOutputEchoSuppression({
1457
+ const suppression = recordGoogleMeetOutputActivity({
1458
+ tracker: outputActivity,
1453
1459
  audio,
1454
1460
  audioFormat: params.config.chrome.audioFormat,
1455
1461
  nowMs: Date.now(),
@@ -1459,7 +1465,6 @@ async function startNodeAgentAudioBridge(params) {
1459
1465
  suppressInputUntil = suppression.suppressInputUntilMs;
1460
1466
  lastOutputPlayableUntilMs = suppression.lastOutputPlayableUntilMs;
1461
1467
  lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
1462
- lastOutputBytes += audio.byteLength;
1463
1468
  await params.runtime.nodes.invoke({
1464
1469
  nodeId: params.nodeId,
1465
1470
  command: "googlemeet.chrome",
@@ -1549,10 +1554,10 @@ async function startNodeAgentAudioBridge(params) {
1549
1554
  },
1550
1555
  timeoutMs: 2e3
1551
1556
  });
1552
- const result = asRecord$2(asRecord$2(raw).payload ?? raw);
1557
+ const result = asRecord$1(asRecord$1(raw).payload ?? raw);
1553
1558
  consecutiveInputErrors = 0;
1554
1559
  lastInputError = void 0;
1555
- const base64 = readString$1(result.base64);
1560
+ const base64 = readString(result.base64);
1556
1561
  if (base64) {
1557
1562
  const audio = Buffer.from(base64, "base64");
1558
1563
  if (Date.now() < suppressInputUntil) {
@@ -1587,12 +1592,12 @@ async function startNodeAgentAudioBridge(params) {
1587
1592
  providerConnected: sttSession?.isConnected() ?? false,
1588
1593
  realtimeReady,
1589
1594
  audioInputActive: lastInputBytes > 0,
1590
- audioOutputActive: lastOutputBytes > 0,
1595
+ audioOutputActive: outputActivity.isActive(),
1591
1596
  lastInputAt,
1592
1597
  lastOutputAt,
1593
1598
  lastSuppressedInputAt,
1594
1599
  lastInputBytes,
1595
- lastOutputBytes,
1600
+ lastOutputBytes: outputActivity.snapshot().sinkAudioBytes,
1596
1601
  suppressedInputBytes,
1597
1602
  ...getGoogleMeetRealtimeTranscriptHealth(transcript),
1598
1603
  consecutiveInputErrors,
@@ -1610,7 +1615,7 @@ async function startNodeRealtimeAudioBridge(params) {
1610
1615
  let lastOutputAt;
1611
1616
  let lastClearAt;
1612
1617
  let lastInputBytes = 0;
1613
- let lastOutputBytes = 0;
1618
+ const outputActivity = createRealtimeVoiceOutputActivityTracker();
1614
1619
  let suppressedInputBytes = 0;
1615
1620
  let lastSuppressedInputAt;
1616
1621
  let suppressInputUntil = 0;
@@ -1748,7 +1753,8 @@ async function startNodeRealtimeAudioBridge(params) {
1748
1753
  turnId,
1749
1754
  payload: { byteLength: audio.byteLength }
1750
1755
  });
1751
- const suppression = extendGoogleMeetOutputEchoSuppression({
1756
+ const suppression = recordGoogleMeetOutputActivity({
1757
+ tracker: outputActivity,
1752
1758
  audio,
1753
1759
  audioFormat: params.config.chrome.audioFormat,
1754
1760
  nowMs: Date.now(),
@@ -1758,7 +1764,6 @@ async function startNodeRealtimeAudioBridge(params) {
1758
1764
  suppressInputUntil = suppression.suppressInputUntilMs;
1759
1765
  lastOutputPlayableUntilMs = suppression.lastOutputPlayableUntilMs;
1760
1766
  lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
1761
- lastOutputBytes += audio.byteLength;
1762
1767
  params.runtime.nodes.invoke({
1763
1768
  nodeId: params.nodeId,
1764
1769
  command: "googlemeet.chrome",
@@ -1924,10 +1929,10 @@ async function startNodeRealtimeAudioBridge(params) {
1924
1929
  },
1925
1930
  timeoutMs: 2e3
1926
1931
  });
1927
- const result = asRecord$2(asRecord$2(raw).payload ?? raw);
1932
+ const result = asRecord$1(asRecord$1(raw).payload ?? raw);
1928
1933
  consecutiveInputErrors = 0;
1929
1934
  lastInputError = void 0;
1930
- const base64 = readString$1(result.base64);
1935
+ const base64 = readString(result.base64);
1931
1936
  if (base64) {
1932
1937
  const audio = Buffer.from(base64, "base64");
1933
1938
  if (Date.now() < suppressInputUntil) {
@@ -1969,13 +1974,13 @@ async function startNodeRealtimeAudioBridge(params) {
1969
1974
  providerConnected: bridge?.bridge.isConnected() ?? false,
1970
1975
  realtimeReady,
1971
1976
  audioInputActive: lastInputBytes > 0,
1972
- audioOutputActive: lastOutputBytes > 0,
1977
+ audioOutputActive: outputActivity.isActive(),
1973
1978
  lastInputAt,
1974
1979
  lastOutputAt,
1975
1980
  lastSuppressedInputAt,
1976
1981
  lastClearAt,
1977
1982
  lastInputBytes,
1978
- lastOutputBytes,
1983
+ lastOutputBytes: outputActivity.snapshot().sinkAudioBytes,
1979
1984
  suppressedInputBytes,
1980
1985
  ...getGoogleMeetRealtimeTranscriptHealth(transcript),
1981
1986
  ...getGoogleMeetRealtimeEventHealth(realtimeEvents),
@@ -2134,7 +2139,7 @@ function mergeBrowserNotes(browser, notes) {
2134
2139
  if (!browser || notes.length === 0) return browser;
2135
2140
  return {
2136
2141
  ...browser,
2137
- notes: [...new Set([...browser.notes ?? [], ...notes])]
2142
+ notes: uniqueStrings([...browser.notes ?? [], ...notes])
2138
2143
  };
2139
2144
  }
2140
2145
  function parsePermissionGrantNotes(result) {
@@ -2728,12 +2733,6 @@ async function launchChromeMeetOnNode(params) {
2728
2733
  //#endregion
2729
2734
  //#region extensions/google-meet/src/node-host.ts
2730
2735
  const sessions = /* @__PURE__ */ new Map();
2731
- function asRecord$1(value) {
2732
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2733
- }
2734
- function readString(value) {
2735
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2736
- }
2737
2736
  function readStringArray(value) {
2738
2737
  if (!Array.isArray(value)) return;
2739
2738
  const result = value.filter((entry) => typeof entry === "string" && entry.length > 0);
@@ -2854,7 +2853,7 @@ function terminateChild(child) {
2854
2853
  }, 2e3).unref?.();
2855
2854
  }
2856
2855
  async function pullAudio(params) {
2857
- const bridgeId = readString(params.bridgeId);
2856
+ const bridgeId = normalizeOptionalString(params.bridgeId);
2858
2857
  if (!bridgeId) throw new Error("bridgeId required");
2859
2858
  const session = sessions.get(bridgeId);
2860
2859
  if (!session) throw new Error(`unknown bridgeId: ${bridgeId}`);
@@ -2870,8 +2869,8 @@ async function pullAudio(params) {
2870
2869
  };
2871
2870
  }
2872
2871
  function pushAudio(params) {
2873
- const bridgeId = readString(params.bridgeId);
2874
- const base64 = readString(params.base64);
2872
+ const bridgeId = normalizeOptionalString(params.bridgeId);
2873
+ const base64 = normalizeOptionalString(params.base64);
2875
2874
  if (!bridgeId || !base64) throw new Error("bridgeId and base64 required");
2876
2875
  const session = sessions.get(bridgeId);
2877
2876
  if (!session || session.closed) throw new Error(`bridge is not open: ${bridgeId}`);
@@ -2890,7 +2889,7 @@ function pushAudio(params) {
2890
2889
  };
2891
2890
  }
2892
2891
  function clearAudio(params) {
2893
- const bridgeId = readString(params.bridgeId);
2892
+ const bridgeId = normalizeOptionalString(params.bridgeId);
2894
2893
  if (!bridgeId) throw new Error("bridgeId required");
2895
2894
  const session = sessions.get(bridgeId);
2896
2895
  if (!session || session.closed) throw new Error(`bridge is not open: ${bridgeId}`);
@@ -2908,10 +2907,10 @@ function clearAudio(params) {
2908
2907
  };
2909
2908
  }
2910
2909
  function startChrome(params) {
2911
- const url = readString(params.url);
2910
+ const url = normalizeOptionalString(params.url);
2912
2911
  if (!url) throw new Error("url required");
2913
2912
  const timeoutMs = readNumber(params.joinTimeoutMs, 3e4);
2914
- const mode = readString(params.mode);
2913
+ const mode = normalizeOptionalString(params.mode);
2915
2914
  let bridgeId;
2916
2915
  let audioBridge;
2917
2916
  if (mode === "agent" || mode === "bidi" || mode === "realtime") {
@@ -2943,7 +2942,7 @@ function startChrome(params) {
2943
2942
  "-a",
2944
2943
  "Google Chrome"
2945
2944
  ];
2946
- const browserProfile = readString(params.browserProfile);
2945
+ const browserProfile = normalizeOptionalString(params.browserProfile);
2947
2946
  if (browserProfile) argv.push("--args", `--profile-directory=${browserProfile}`);
2948
2947
  argv.push(url);
2949
2948
  const result = runCommandWithTimeout(argv, timeoutMs);
@@ -2967,7 +2966,7 @@ function startChrome(params) {
2967
2966
  };
2968
2967
  }
2969
2968
  function bridgeStatus(params) {
2970
- const bridgeId = readString(params.bridgeId);
2969
+ const bridgeId = normalizeOptionalString(params.bridgeId);
2971
2970
  const session = bridgeId ? sessions.get(bridgeId) : void 0;
2972
2971
  return { bridge: session ? {
2973
2972
  bridgeId,
@@ -3010,15 +3009,15 @@ function summarizeSession(session) {
3010
3009
  };
3011
3010
  }
3012
3011
  function listSessions(params) {
3013
- const urlKey = normalizeMeetKey(readString(params.url));
3014
- const mode = readString(params.mode);
3012
+ const urlKey = normalizeMeetKey(normalizeOptionalString(params.url));
3013
+ const mode = normalizeOptionalString(params.mode);
3015
3014
  return { bridges: [...sessions.values()].filter((session) => !session.closed).filter((session) => !urlKey || normalizeMeetKey(session.url) === urlKey).filter((session) => !mode || session.mode === mode).map(summarizeSession) };
3016
3015
  }
3017
3016
  function stopSessionsByUrl(params) {
3018
- const urlKey = normalizeMeetKey(readString(params.url));
3017
+ const urlKey = normalizeMeetKey(normalizeOptionalString(params.url));
3019
3018
  if (!urlKey) throw new Error("url required");
3020
- const mode = readString(params.mode);
3021
- const exceptBridgeId = readString(params.exceptBridgeId);
3019
+ const mode = normalizeOptionalString(params.mode);
3020
+ const exceptBridgeId = normalizeOptionalString(params.exceptBridgeId);
3022
3021
  let stopped = 0;
3023
3022
  for (const [bridgeId, session] of sessions) {
3024
3023
  if (exceptBridgeId && bridgeId === exceptBridgeId) continue;
@@ -3035,7 +3034,7 @@ function stopSessionsByUrl(params) {
3035
3034
  };
3036
3035
  }
3037
3036
  function stopChrome(params) {
3038
- const bridgeId = readString(params.bridgeId);
3037
+ const bridgeId = normalizeOptionalString(params.bridgeId);
3039
3038
  if (!bridgeId) return {
3040
3039
  ok: true,
3041
3040
  stopped: false
@@ -3059,8 +3058,8 @@ async function handleGoogleMeetNodeHostCommand(paramsJSON) {
3059
3058
  } catch {
3060
3059
  throw new Error("Google Meet node host received malformed params JSON.");
3061
3060
  }
3062
- const params = asRecord$1(raw);
3063
- const action = readString(params.action);
3061
+ const params = asRecord(raw);
3062
+ const action = normalizeOptionalString(params.action);
3064
3063
  let result;
3065
3064
  switch (action) {
3066
3065
  case "setup":
@@ -3110,11 +3109,11 @@ function isProviderUnreachableWebhookUrl(webhookUrl) {
3110
3109
  }
3111
3110
  }
3112
3111
  function getVoiceCallWebhookExposureCheck(voiceCallConfig) {
3113
- const publicUrl = normalizeOptionalString$1(voiceCallConfig.publicUrl);
3112
+ const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
3114
3113
  const tunnel = asRecord(voiceCallConfig.tunnel);
3115
3114
  const tailscale = asRecord(voiceCallConfig.tailscale);
3116
- const tunnelProvider = normalizeOptionalString$1(tunnel.provider);
3117
- const tailscaleMode = normalizeOptionalString$1(tailscale.mode);
3115
+ const tunnelProvider = normalizeOptionalString(tunnel.provider);
3116
+ const tailscaleMode = normalizeOptionalString(tailscale.mode);
3118
3117
  if (publicUrl) {
3119
3118
  const ok = !isProviderUnreachableWebhookUrl(publicUrl);
3120
3119
  return {
@@ -3215,10 +3214,10 @@ function getGoogleMeetSetupStatus(config, options) {
3215
3214
  ok: voiceCallAllowed && voiceCallEnabled,
3216
3215
  message: voiceCallAllowed && voiceCallEnabled ? "Twilio transport can delegate dialing to the voice-call plugin" : "Enable plugins.entries.voice-call and include voice-call in plugins.allow for Twilio dialing"
3217
3216
  });
3218
- if ((normalizeOptionalString$1(voiceCallConfig.provider) ?? "twilio") === "twilio") {
3219
- const accountSid = normalizeOptionalString$1(voiceCallTwilioConfig.accountSid);
3220
- const authToken = normalizeOptionalString$1(voiceCallTwilioConfig.authToken);
3221
- const fromNumber = normalizeOptionalString$1(voiceCallConfig.fromNumber);
3217
+ if ((normalizeOptionalString(voiceCallConfig.provider) ?? "twilio") === "twilio") {
3218
+ const accountSid = normalizeOptionalString(voiceCallTwilioConfig.accountSid);
3219
+ const authToken = normalizeOptionalString(voiceCallTwilioConfig.authToken);
3220
+ const fromNumber = normalizeOptionalString(voiceCallConfig.fromNumber);
3222
3221
  const twilioReady = Boolean((accountSid || env.TWILIO_ACCOUNT_SID) && (authToken || env.TWILIO_AUTH_TOKEN) && (fromNumber || env.TWILIO_FROM_NUMBER));
3223
3222
  checks.push({
3224
3223
  id: "twilio-voice-call-credentials",
@@ -3240,12 +3239,6 @@ function addGoogleMeetSetupCheck(status, check) {
3240
3239
  checks
3241
3240
  };
3242
3241
  }
3243
- function asRecord(value) {
3244
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
3245
- }
3246
- function normalizeOptionalString$1(value) {
3247
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
3248
- }
3249
3242
  //#endregion
3250
3243
  //#region extensions/google-meet/src/transports/twilio.ts
3251
3244
  const DTMF_PATTERN = /^[0-9*#wWpP,]+$/;
@@ -3505,12 +3498,11 @@ function evaluateSpeechReadiness(session) {
3505
3498
  };
3506
3499
  }
3507
3500
  function collectChromeAudioCommands(config) {
3508
- const commands = config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
3501
+ return uniqueStrings((config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
3509
3502
  config.chrome.audioInputCommand?.[0],
3510
3503
  config.chrome.audioOutputCommand?.[0],
3511
3504
  config.chrome.bargeInInputCommand?.[0]
3512
- ];
3513
- return [...new Set(commands.filter((value) => Boolean(value?.trim())))];
3505
+ ]).filter((value) => Boolean(value?.trim())));
3514
3506
  }
3515
3507
  async function commandExists(runtime, command) {
3516
3508
  return (await runtime.system.runCommandWithTimeout([
@@ -4448,10 +4440,10 @@ async function callGoogleMeetGatewayFromTool(params) {
4448
4440
  }
4449
4441
  }
4450
4442
  async function createMeetFromParams(params) {
4451
- return (await import("./create-CaFygLla.js")).createMeetFromParams(params);
4443
+ return (await import("./create-Q8KLuYnE.js")).createMeetFromParams(params);
4452
4444
  }
4453
4445
  async function createAndJoinMeetFromParams(params) {
4454
- return (await import("./create-CaFygLla.js")).createAndJoinMeetFromParams(params);
4446
+ return (await import("./create-Q8KLuYnE.js")).createAndJoinMeetFromParams(params);
4455
4447
  }
4456
4448
  async function resolveGoogleMeetTokenFromParams(config, raw) {
4457
4449
  const { resolveGoogleMeetAccessToken } = await import("./oauth-Byt1wS27.js");
@@ -4543,7 +4535,7 @@ async function exportGoogleMeetBundleFromParams(config, raw) {
4543
4535
  lateAfterMinutes: resolved.lateAfterMinutes,
4544
4536
  earlyBeforeMinutes: resolved.earlyBeforeMinutes
4545
4537
  })]);
4546
- const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-BoWxP6Ej.js");
4538
+ const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-CS9eUUSs.js");
4547
4539
  const calendarId = normalizeOptionalString(raw.calendarId);
4548
4540
  const request = {
4549
4541
  ...resolved.meeting ? { meeting: resolved.meeting } : {},
@@ -4959,7 +4951,7 @@ var google_meet_default = definePluginEntry({
4959
4951
  handle: handleGoogleMeetNodeHostCommand
4960
4952
  });
4961
4953
  api.registerCli(async ({ program }) => {
4962
- const { registerGoogleMeetCli } = await import("./cli-BoWxP6Ej.js");
4954
+ const { registerGoogleMeetCli } = await import("./cli-CS9eUUSs.js");
4963
4955
  registerGoogleMeetCli({
4964
4956
  program,
4965
4957
  config,
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@openclaw/google-meet",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.26-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/google-meet",
9
- "version": "2026.5.24-beta.2",
9
+ "version": "2026.5.26-beta.1",
10
10
  "dependencies": {
11
11
  "commander": "14.0.3",
12
12
  "typebox": "1.1.38"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.5.24-beta.2"
15
+ "openclaw": ">=2026.5.26-beta.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/google-meet",
3
- "version": "2026.5.24-beta.2",
3
+ "version": "2026.5.26-beta.1",
4
4
  "description": "OpenClaw Google Meet participant plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@
12
12
  "typebox": "1.1.38"
13
13
  },
14
14
  "peerDependencies": {
15
- "openclaw": ">=2026.5.24-beta.2"
15
+ "openclaw": ">=2026.5.26-beta.1"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "openclaw": {
@@ -29,10 +29,10 @@
29
29
  "minHostVersion": ">=2026.4.20"
30
30
  },
31
31
  "compat": {
32
- "pluginApi": ">=2026.5.24-beta.2"
32
+ "pluginApi": ">=2026.5.26-beta.1"
33
33
  },
34
34
  "build": {
35
- "openclawVersion": "2026.5.24-beta.2"
35
+ "openclawVersion": "2026.5.26-beta.1"
36
36
  },
37
37
  "release": {
38
38
  "publishToClawHub": true,