From f85d438164eb954aecdf05b9e1c0102cf05346bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Jul 2026 18:52:04 -0700 Subject: [PATCH] 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 --- extensions/google-meet/index.test.ts | 372 ++++++++++++++++++ extensions/google-meet/index.ts | 19 +- extensions/google-meet/src/cli.test.ts | 7 + extensions/google-meet/src/cli.ts | 1 + extensions/google-meet/src/runtime.ts | 115 ++++-- .../google-meet/src/transports/types.ts | 2 + 6 files changed, 480 insertions(+), 36 deletions(-) diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index 6cc93144e950..111eaf798c36 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -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( { diff --git a/extensions/google-meet/index.ts b/extensions/google-meet/index.ts index eae546d63f83..e7f4e5d85ef6 100644 --- a/extensions/google-meet/index.ts +++ b/extensions/google-meet/index.ts @@ -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": { diff --git a/extensions/google-meet/src/cli.test.ts b/extensions/google-meet/src/cli.test.ts index cdc0a79107d9..1ec407e1394f 100644 --- a/extensions/google-meet/src/cli.test.ts +++ b/extensions/google-meet/src/cli.test.ts @@ -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", diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index 6a4c093f2135..92d6046128cd 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -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: "", diff --git a/extensions/google-meet/src/runtime.ts b/extensions/google-meet/src/runtime.ts index 2d714de5f213..3b68324f9cea 100644 --- a/extensions/google-meet/src/runtime.ts +++ b/extensions/google-meet/src/runtime.ts @@ -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, }, }, diff --git a/extensions/google-meet/src/transports/types.ts b/extensions/google-meet/src/transports/types.ts index 3f3e17156935..b659d1cda89a 100644 --- a/extensions/google-meet/src/transports/types.ts +++ b/extensions/google-meet/src/transports/types.ts @@ -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;