mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
committed by
GitHub
parent
6630138598
commit
f85d438164
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user