fix(google-meet): keep Meet sessions on the agent that joined them (#102331)

* fix(google-meet): preserve routed agent across session reuse

* chore: defer google meet release note to release closeout

---------

Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
Peter Steinberger
2026-07-08 18:52:04 -07:00
committed by GitHub
parent 6630138598
commit f85d438164
6 changed files with 480 additions and 36 deletions
+372
View File
@@ -46,6 +46,7 @@ import {
noopLogger,
setupGoogleMeetPlugin,
} from "./src/test-support/plugin-harness.js";
import * as chromeTransport from "./src/transports/chrome.js";
import { testing as chromeTransportTesting } from "./src/transports/chrome.js";
import {
buildMeetDtmfSequence,
@@ -1489,6 +1490,70 @@ describe("google-meet plugin", () => {
);
});
it("keeps Twilio calls on the configured realtime agent when no tool override exists", async () => {
const { tools } = setup({
defaultTransport: "twilio",
realtime: { agentId: "support" },
});
const tool = tools[0] as {
execute: (id: string, params: unknown) => Promise<{ details: { session: { id: string } } }>;
};
const result = await tool.execute("id", {
action: "join",
url: "https://meet.google.com/abc-defg-hij",
dialInNumber: "+15551234567",
});
expect(voiceCallMocks.joinMeetViaVoiceCallGateway).toHaveBeenCalledWith(
expect.objectContaining({
agentId: "support",
sessionKey: `agent:support:google-meet:${result.details.session.id}`,
}),
);
});
it("keeps test-listen probes on the agent that invoked the tool", async () => {
const { tools, gatewayRequest } = setup(
{ defaultTransport: "chrome-node" },
{
gatewayAvailable: true,
toolContext: { agentId: "Support", sessionKey: "agent:support:main" },
browserActResult: {
inCall: true,
captioning: true,
transcriptLines: 1,
lastCaptionText: "hello from the meeting",
title: "Meet call",
url: "https://meet.google.com/abc-defg-hij",
},
},
);
const tool = tools[0] as {
execute: (
id: string,
params: unknown,
) => Promise<{ details: { session: { agentId: string }; listenVerified: boolean } }>;
};
const result = await tool.execute("id", {
action: "test_listen",
url: "https://meet.google.com/abc-defg-hij",
timeoutMs: "100",
});
expect(gatewayRequest).toHaveBeenCalledWith(
"googlemeet.testListen",
expect.objectContaining({
agentId: "support",
requesterSessionKey: "agent:support:main",
}),
{ timeoutMs: 60_000 },
);
expect(result.details.session.agentId).toBe("support");
expect(result.details.listenVerified).toBe(true);
});
it("fails closed for standalone non-default agent routing", async () => {
const { tools } = setup(
{},
@@ -3788,6 +3853,7 @@ describe("google-meet plugin", () => {
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "agent",
agentId: "main",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
@@ -3836,6 +3902,7 @@ describe("google-meet plugin", () => {
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "bidi",
agentId: "main",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
@@ -3868,6 +3935,104 @@ describe("google-meet plugin", () => {
expect(joinArgs.mode).toBe("bidi");
});
it("resets test speech output baselines when another agent owns the old session", async () => {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const oldSession: GoogleMeetSession = {
id: "meet_old",
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "agent",
agentId: "support",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
participantIdentity: "signed-in Google Chrome profile",
realtime: {
enabled: true,
strategy: "agent",
transcriptionProvider: "openai",
toolPolicy: "safe-read-only",
},
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { audioOutputActive: true, lastOutputBytes: 100 },
},
notes: [],
};
const newSession: GoogleMeetSession = {
...oldSession,
id: "meet_new",
agentId: "main",
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { audioOutputActive: true, lastOutputBytes: 1 },
},
};
vi.spyOn(runtime, "list").mockReturnValue([oldSession]);
vi.spyOn(runtime, "join").mockResolvedValue({ session: newSession, spoken: true });
const result = await runtime.testSpeech({
url: "https://meet.google.com/abc-defg-hij",
agentId: "main",
message: "Say exactly: hello.",
});
expect(result.speechOutputVerified).toBe(true);
expect(result.speechOutputTimedOut).toBe(false);
});
it("keeps the pre-join test speech baseline when reusing the same session", async () => {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const session: GoogleMeetSession = {
id: "meet_1",
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "agent",
agentId: "main",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
participantIdentity: "signed-in Google Chrome profile",
realtime: {
enabled: true,
strategy: "agent",
transcriptionProvider: "openai",
toolPolicy: "safe-read-only",
},
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { audioOutputActive: true, lastOutputBytes: 10 },
},
notes: [],
};
vi.spyOn(runtime, "list").mockReturnValue([session]);
vi.spyOn(runtime, "join").mockImplementation(async () => {
session.chrome!.health!.lastOutputBytes = 11;
return { session, spoken: true };
});
const result = await runtime.testSpeech({
url: "https://meet.google.com/abc-defg-hij",
message: "Say exactly: hello.",
});
expect(result.speechOutputVerified).toBe(true);
expect(result.speechOutputTimedOut).toBe(false);
});
it("rejects observe-only mode for test speech", async () => {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({}),
@@ -3907,6 +4072,213 @@ describe("google-meet plugin", () => {
).rejects.toThrow("test_listen supports chrome or chrome-node");
});
it("resets test listen transcript baselines when another agent owns the old session", async () => {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const oldSession: GoogleMeetSession = {
id: "meet_old",
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "transcribe",
agentId: "support",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
participantIdentity: "signed-in Google Chrome profile",
realtime: {
enabled: false,
strategy: "agent",
toolPolicy: "safe-read-only",
},
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { transcriptLines: 10, lastCaptionText: "old caption" },
},
notes: [],
};
const newSession: GoogleMeetSession = {
...oldSession,
id: "meet_new",
agentId: "main",
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { transcriptLines: 1, lastCaptionText: "fresh caption" },
},
};
vi.spyOn(runtime, "list").mockReturnValue([oldSession]);
vi.spyOn(runtime, "join").mockResolvedValue({ session: newSession, spoken: false });
const result = await runtime.testListen({
url: "https://meet.google.com/abc-defg-hij",
agentId: "main",
});
expect(result.listenVerified).toBe(true);
expect(result.listenTimedOut).toBe(false);
});
it("keeps the pre-join test listen baseline when reusing the same session", async () => {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const session: GoogleMeetSession = {
id: "meet_1",
url: "https://meet.google.com/abc-defg-hij",
transport: "chrome",
mode: "transcribe",
agentId: "main",
state: "active",
createdAt: "2026-04-27T00:00:00.000Z",
updatedAt: "2026-04-27T00:00:00.000Z",
participantIdentity: "signed-in Google Chrome profile",
realtime: {
enabled: false,
strategy: "agent",
toolPolicy: "safe-read-only",
},
chrome: {
audioBackend: "blackhole-2ch",
launched: true,
health: { transcriptLines: 1, lastCaptionText: "old caption" },
},
notes: [],
};
vi.spyOn(runtime, "list").mockReturnValue([session]);
vi.spyOn(runtime, "join").mockImplementation(async () => {
session.chrome!.health = { transcriptLines: 2, lastCaptionText: "fresh caption" };
return { session, spoken: false };
});
const result = await runtime.testListen({
url: "https://meet.google.com/abc-defg-hij",
});
expect(result.listenVerified).toBe(true);
expect(result.listenTimedOut).toBe(false);
});
it("stops the old Chrome bridge before reassigning a Meet tab to another agent", async () => {
const stop = vi.fn(async () => {});
const launchChromeMeet = vi
.spyOn(chromeTransport, "launchChromeMeet")
.mockResolvedValueOnce({
launched: true,
browser: { inCall: true, micMuted: false },
audioBridge: {
type: "command-pair",
providerId: "openai",
inputCommand: ["capture-meet"],
outputCommand: ["play-meet"],
speak: vi.fn(),
getHealth: vi.fn(() => ({})),
stop,
},
})
.mockResolvedValueOnce({
launched: true,
browser: { inCall: true, micMuted: false },
});
try {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({
defaultTransport: "chrome",
defaultMode: "agent",
realtime: { introMessage: "" },
}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const first = await runtime.join({
url: "https://meet.google.com/abc-defg-hij",
agentId: "support",
});
const second = await runtime.join({
url: "https://meet.google.com/abc-defg-hij",
agentId: "main",
});
expect(first.session.agentId).toBe("support");
expect(second.session.agentId).toBe("main");
expect(first.session.state).toBe("ended");
expect(first.session.notes).toContain(
"Ended before the same Meet tab was reassigned to another agent.",
);
expect(second.session.id).not.toBe(first.session.id);
expect(stop).toHaveBeenCalledTimes(1);
expect(launchChromeMeet).toHaveBeenCalledTimes(2);
} finally {
launchChromeMeet.mockRestore();
}
});
it("reuses the stored session agent when a later Chrome bridge starts", async () => {
const launchChromeMeet = vi
.spyOn(chromeTransport, "launchChromeMeet")
.mockResolvedValueOnce({
launched: true,
browser: { inCall: false, micMuted: false },
})
.mockResolvedValueOnce({
launched: false,
audioBridge: { type: "external-command" },
});
const recoverCurrentMeetTab = vi
.spyOn(chromeTransport, "recoverCurrentMeetTab")
.mockResolvedValue({
transport: "chrome",
found: true,
message: "Existing Meet tab is in-call.",
browser: { inCall: true, micMuted: false },
});
try {
const runtime = new GoogleMeetRuntime({
config: resolveGoogleMeetConfig({
defaultTransport: "chrome",
defaultMode: "agent",
realtime: { introMessage: "" },
}),
fullConfig: {} as never,
runtime: {} as never,
logger: noopLogger,
});
const joined = await runtime.join({
url: "https://meet.google.com/abc-defg-hij",
agentId: "support",
});
await runtime.speak(joined.session.id, "Say exactly: hello.");
const delayedBridgeParams = requireRecord(
mockCallArg(launchChromeMeet, 0, 1),
"delayed bridge launch params",
);
const delayedBridgeConfig = requireRecord(
delayedBridgeParams.config,
"delayed bridge config",
);
const delayedRealtime = requireRecord(
delayedBridgeConfig.realtime,
"delayed bridge realtime config",
);
expect(joined.session.agentId).toBe("support");
expect(delayedRealtime.agentId).toBe("support");
} finally {
recoverCurrentMeetTab.mockRestore();
launchChromeMeet.mockRestore();
}
});
it("reports manual action when the browser profile needs Google login", async () => {
const { tools } = setup(
{
+13 -6
View File
@@ -1023,14 +1023,16 @@ export default definePluginEntry({
api.registerGatewayMethod(
"googlemeet.testListen",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
async ({ params, client, respond }: GatewayRequestHandlerOptions) => {
try {
const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
const rt = await ensureRuntime();
const result = await rt.testListen({
url: resolveMeetingInput(config, params?.url),
transport: normalizeTransport(params?.transport),
mode: normalizeMode(params?.mode),
timeoutMs: readPositiveIntegerParam(asParamRecord(params), "timeoutMs"),
url: resolveMeetingInput(config, trustedParams.url),
transport: normalizeTransport(trustedParams.transport),
mode: normalizeMode(trustedParams.mode),
agentId: normalizeOptionalString(trustedParams.agentId),
timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs"),
});
respond(true, result);
} catch (err) {
@@ -1094,7 +1096,12 @@ export default definePluginEntry({
}
case "test_listen": {
return json(
await callGoogleMeetGatewayFromTool({ config, action: "test_listen", raw }),
await callGoogleMeetGatewayFromTool({
config,
action: "test_listen",
raw: rawWithRequester,
runtime: useTrustedRuntime ? api.runtime : undefined,
}),
);
}
case "status": {
+7
View File
@@ -735,6 +735,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "twilio",
mode: "agent",
agentId: "main",
participantIdentity: "Twilio PSTN participant",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -766,6 +767,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "chrome-node",
mode: "agent",
agentId: "main",
participantIdentity: "signed-in Google Chrome profile on a paired node",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -809,6 +811,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "chrome-node",
mode: "realtime",
agentId: "main",
participantIdentity: "signed-in Google Chrome profile on a paired node",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -882,6 +885,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "chrome",
mode: "bidi",
agentId: "main",
participantIdentity: "signed-in Google Chrome profile",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -944,6 +948,7 @@ describe("google-meet CLI", () => {
state: "active" as const,
transport: "chrome-node" as const,
mode: "transcribe" as const,
agentId: "main",
participantIdentity: "signed-in Google Chrome profile on a paired node",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -1093,6 +1098,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "chrome-node",
mode: "agent",
agentId: "main",
participantIdentity: "signed-in Google Chrome profile on a paired node",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
@@ -1149,6 +1155,7 @@ describe("google-meet CLI", () => {
state: "active",
transport: "twilio",
mode: "agent",
agentId: "main",
participantIdentity: "Twilio phone participant",
createdAt: "2026-04-25T00:00:00.000Z",
updatedAt: "2026-04-25T00:00:01.000Z",
+1
View File
@@ -613,6 +613,7 @@ function writeRecoverCurrentTabResult(
url: result.browser.browserUrl ?? result.tab?.url ?? "unknown",
transport: result.transport,
mode: "transcribe",
agentId: "main",
state: "active",
createdAt: "",
updatedAt: "",
+85 -30
View File
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { sleep } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
@@ -83,10 +84,45 @@ function resolveMode(input: GoogleMeetModeInput | undefined, config: GoogleMeetC
return input === "realtime" ? "agent" : (input ?? config.defaultMode);
}
function resolveSessionAgentId(request: GoogleMeetJoinRequest, config: GoogleMeetConfig): string {
return normalizeAgentId(request.agentId ?? config.realtime.agentId);
}
function withSessionAgentConfig(config: GoogleMeetConfig, agentId: string): GoogleMeetConfig {
return config.realtime.agentId === agentId
? config
: {
...config,
realtime: { ...config.realtime, agentId },
};
}
function isGoogleMeetTalkBackMode(mode: GoogleMeetMode): boolean {
return mode === "agent" || mode === "bidi";
}
function isBrowserTransport(transport: GoogleMeetTransport): boolean {
return transport === "chrome" || transport === "chrome-node";
}
function isReusableMeetSession(
session: GoogleMeetSession,
params: {
url: string;
transport: GoogleMeetTransport;
mode: GoogleMeetMode;
agentId: string;
},
): boolean {
return (
session.state === "active" &&
isSameMeetUrlForReuse(session.url, params.url) &&
session.transport === params.transport &&
session.mode === params.mode &&
session.agentId === params.agentId
);
}
function hasRealtimeAudioOutputAdvanced(
health: GoogleMeetChromeHealth | undefined,
startOutputBytes: number,
@@ -378,12 +414,26 @@ export class GoogleMeetRuntime {
const url = normalizeMeetUrl(request.url);
const transport = resolveTransport(request.transport, this.params.config);
const mode = resolveMode(request.mode, this.params.config);
let reusable = this.list().find(
const agentId = resolveSessionAgentId(request, this.params.config);
const activeSessions = this.list().filter(
(session) =>
session.state === "active" &&
isSameMeetUrlForReuse(session.url, url) &&
session.transport === transport &&
session.mode === mode,
session.transport === transport,
);
if (isBrowserTransport(transport) && isGoogleMeetTalkBackMode(mode)) {
for (const session of activeSessions) {
if (session.agentId === agentId || !isGoogleMeetTalkBackMode(session.mode)) {
continue;
}
// A reused browser tab can only host one live talk-back bridge safely.
// End the previous owner before the tab is reused for another agent.
await this.leave(session.id);
noteSession(session, "Ended before the same Meet tab was reassigned to another agent.");
}
}
let reusable = activeSessions.find((session) =>
isReusableMeetSession(session, { url, transport, mode, agentId }),
);
if (reusable?.transport === "twilio") {
await this.#refreshTwilioVoiceCallStatus(reusable);
@@ -410,6 +460,7 @@ export class GoogleMeetRuntime {
url,
transport,
mode,
agentId,
state: "active",
createdAt,
updatedAt: createdAt,
@@ -439,14 +490,9 @@ export class GoogleMeetRuntime {
try {
if (transport === "chrome" || transport === "chrome-node") {
// Freeze the invoking agent into the bridge config so every later consult
// stays on the same workspace even if plugin defaults change mid-call.
const chromeConfig = request.agentId
? {
...this.params.config,
realtime: { ...this.params.config.realtime, agentId: request.agentId },
}
: this.params.config;
// Session ownership must outlive the original request so later bridge
// startup and reuse stay on the same workspace and tool policy.
const chromeConfig = withSessionAgentConfig(this.params.config, agentId);
const result =
transport === "chrome-node"
? await launchChromeMeetOnNode({
@@ -504,6 +550,11 @@ export class GoogleMeetRuntime {
request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence
? rawDtmfSequence
: prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
const hasExplicitDelegatedAgent = Boolean(
normalizeOptionalString(request.agentId) ||
normalizeOptionalString(this.params.config.realtime.agentId),
);
const delegatedAgentId = hasExplicitDelegatedAgent ? agentId : undefined;
const voiceCallResult = this.params.config.voiceCall.enabled
? await joinMeetViaVoiceCallGateway({
config: this.params.config,
@@ -514,9 +565,9 @@ export class GoogleMeetRuntime {
...(request.requesterSessionKey
? { requesterSessionKey: request.requesterSessionKey }
: {}),
agentId: request.agentId,
sessionKey: request.agentId
? `agent:${request.agentId}:google-meet:${session.id}`
agentId: delegatedAgentId,
sessionKey: delegatedAgentId
? `agent:${delegatedAgentId}:google-meet:${session.id}`
: buildTwilioVoiceCallSessionKey(session.id),
message: isGoogleMeetTalkBackMode(mode)
? (request.message ??
@@ -701,16 +752,13 @@ export class GoogleMeetRuntime {
: "agent";
const url = normalizeMeetUrl(request.url);
const transport = resolveTransport(request.transport, this.params.config);
const agentId = resolveSessionAgentId(request, this.params.config);
const beforeSessions = this.list();
const before = new Set(beforeSessions.map((session) => session.id));
const existingSession = beforeSessions.find(
(session) =>
session.state === "active" &&
isSameMeetUrlForReuse(session.url, url) &&
session.transport === transport &&
isGoogleMeetTalkBackMode(session.mode),
const existingSession = beforeSessions.find((session) =>
isReusableMeetSession(session, { url, transport, mode, agentId }),
);
const startOutputBytes = existingSession?.chrome?.health?.lastOutputBytes ?? 0;
const existingOutputBytes = existingSession?.chrome?.health?.lastOutputBytes ?? 0;
const result = await this.join({
...request,
transport,
@@ -718,6 +766,8 @@ export class GoogleMeetRuntime {
mode,
message: request.message ?? "Say exactly: Google Meet speech test complete.",
});
const startOutputBytes =
existingSession?.id === result.session.id ? existingOutputBytes : 0;
let health = result.session.chrome?.health;
const shouldWaitForOutput =
result.spoken === true &&
@@ -781,16 +831,18 @@ export class GoogleMeetRuntime {
if (transport === "twilio") {
throw new Error("test_listen supports chrome or chrome-node transports");
}
const agentId = resolveSessionAgentId(request, this.params.config);
const beforeSessions = this.list();
const before = new Set(beforeSessions.map((session) => session.id));
const existingSession = beforeSessions.find(
(session) =>
session.state === "active" &&
isSameMeetUrlForReuse(session.url, url) &&
session.transport === transport &&
session.mode === "transcribe",
const existingSession = beforeSessions.find((session) =>
isReusableMeetSession(session, {
url,
transport,
mode: "transcribe",
agentId,
}),
);
const start = transcriptCheckpoint(existingSession?.chrome?.health);
const existingStart = transcriptCheckpoint(existingSession?.chrome?.health);
const result = await this.join({
...request,
transport,
@@ -798,6 +850,8 @@ export class GoogleMeetRuntime {
mode: "transcribe",
message: undefined,
});
const start =
existingSession?.id === result.session.id ? existingStart : transcriptCheckpoint(undefined);
let health = result.session.chrome?.health;
const timeoutMs = resolveProbeTimeoutMs(
request.timeoutMs,
@@ -973,12 +1027,13 @@ export class GoogleMeetRuntime {
) {
return;
}
const sessionConfig = withSessionAgentConfig(this.params.config, session.agentId);
const result = await launchChromeMeet({
runtime: this.params.runtime,
config: {
...this.params.config,
...sessionConfig,
chrome: {
...this.params.config.chrome,
...sessionConfig.chrome,
launch: false,
},
},
@@ -110,6 +110,8 @@ export type GoogleMeetSession = {
url: string;
transport: GoogleMeetTransport;
mode: GoogleMeetMode;
/** Canonical agent owner for every later consult and bridge restart. */
agentId: string;
state: GoogleMeetSessionState;
createdAt: string;
updatedAt: string;