UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

7,281 lines 283 kB
import { C as parseStrictNonNegativeInteger, P as resolvePositiveTimerTimeoutMs, a as addTimerTimeoutGraceMs, f as asSafeIntegerInRange, u as asPositiveFiniteNumber } from "../number-coercion-CLj0HTDM.js";
import { a as asOptionalRecord, o as asRecord } from "../record-coerce-DItp3I4t.js";
import { c as normalizeOptionalLowercaseString, h as readNonEmptyStringPreservingWhitespace, l as normalizeOptionalString, m as readNonBlankString } from "../string-coerce-CIXf7egm.js";
import { s as normalizeOptionalTrimmedStringList, y as uniqueStrings } from "../string-normalization-DsCfAx8q.js";
import { r as truncateUtf16Safe } from "../utf16-slice-D_ngcYKd.js";
import { r as createLazyRuntimeModule } from "../lazy-runtime-CgCh8H_K.js";
import { At as boolean, Et as array, Rn as string, Tn as object, Xn as union, Zn as unknown, fn as looseObject, wn as number } from "../schemas-zxit8y5H.js";
import "../utils-P__uGsPB.js";
import { t as sleep } from "../sleep-D7nua6TP.js";
import { n as normalizeAgentId } from "../agent-id-CeT3w4ap.js";
import { y as resolveDefaultAgentId } from "../agent-scope-config-DcbEhP0R.js";
import { O as parseAgentSessionKey } from "../session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "../errors-Db3Ymjlb.js";
import { n as callGatewayFromCli } from "../gateway-rpc-C9fDQ0vT.js";
import { n as truncateUtf8Suffix } from "../utf8-truncate-CCgJrv8A.js";
import { t as ErrorCodes } from "../gateway-error-details-w0nAGBBp.js";
import { d as errorShape } from "../error-codes-Bo8q2D1o.js";
import { t as canonicalizeBase64 } from "../base64-Vw7DZYSc.js";
import { t as jsonResult } from "../tool-results-BCM3fdVS.js";
import { d as readPositiveIntegerParam, l as readNonNegativeIntegerParam } from "../common-Bm6UTDDA.js";
import { t as onDecodedOutput } from "../decoded-output-DGGKfGi4.js";
import { t as resolveTranscriptsConfig } from "../config-HG7W2c5J.js";
import { n as getRealtimeTranscriptionProvider, r as listRealtimeTranscriptionProviders } from "../provider-registry-BcVf6icc.js";
import { A as REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, D as resolveRealtimeVoiceAgentConsultTools, E as resolveRealtimeVoiceAgentConsultToolPolicy, O as resolveRealtimeVoiceAgentConsultToolsAllow, k as REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, x as buildRealtimeVoiceAgentConsultWorkingResponse } from "../agent-run-control-shared-zVaKVJuy.js";
import { S as consultRealtimeVoiceAgent, d as resolveConfiguredRealtimeVoiceProvider, t as createRealtimeVoiceSessionHarness } from "../realtime-session-harness-boRKVRPl.js";
import { a as resamplePcm, r as mulawToPcm, t as convertPcmToMulaw8k } from "../audio-codec-DFkaYkvm.js";
import { n as createSpeechThresholdGate, r as readPcm16AudioStats } from "../audio-energy-B7WMUoUD.js";
import { t as MeetingSessionTranscriptStore } from "../session-transcript-store-D1UrWVu5.js";
import { t as createMeetingTranscriptSourceProvider } from "../transcripts-bridge-BhkdwJIv.js";
import { spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { Readable } from "node:stream";
//#region src/meeting-bot/realtime-audio-format.ts
function resolveMeetingRealtimeAudioFormat(audioFormat) {
	return audioFormat === "g711-ulaw-8khz" ? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ : REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ;
}
function convertMeetingBridgeAudioForStt(audio, audioFormat) {
	if (audioFormat === "g711-ulaw-8khz") return audio;
	return convertPcmToMulaw8k(audio, 24e3);
}
function convertMeetingTtsAudioForBridge(audio, sampleRate, audioFormat, outputFormat, platformName = "meeting platform") {
	const sourceFormat = sourceTelephonyTtsFormat(outputFormat, platformName);
	if (audioFormat === "g711-ulaw-8khz" && sourceFormat === "mulaw" && sampleRate === 8e3) return audio;
	const pcm = decodeMeetingTelephonyTtsAudio(audio, sourceFormat);
	return audioFormat === "g711-ulaw-8khz" ? convertPcmToMulaw8k(pcm, sampleRate) : resamplePcm(pcm, sampleRate, 24e3);
}
function sourceTelephonyTtsFormat(outputFormat, platformName) {
	const normalized = outputFormat?.trim().toLowerCase().replaceAll("_", "-") ?? "";
	if (!normalized || normalized === "pcm" || normalized.startsWith("pcm-") || normalized.includes("pcm16") || normalized.includes("16bit-mono-pcm")) return "pcm";
	if (normalized === "mulaw" || normalized === "ulaw" || normalized.includes("mu-law") || normalized.includes("mulaw") || normalized.includes("ulaw")) return "mulaw";
	if (normalized === "alaw" || normalized.includes("a-law") || normalized.includes("alaw")) return "alaw";
	throw new Error(`Unsupported telephony TTS output format for ${platformName}: ${outputFormat}`);
}
function decodeMeetingTelephonyTtsAudio(audio, sourceFormat) {
	switch (sourceFormat) {
		case "pcm": return audio;
		case "mulaw": return mulawToPcm(audio);
		case "alaw": return alawToPcm(audio);
	}
	throw new Error("Unsupported telephony TTS output format for meeting platform");
}
function alawToPcm(alaw) {
	const pcm = Buffer.alloc(alaw.length * 2);
	for (let index = 0; index < alaw.length; index += 1) pcm.writeInt16LE(alawByteToLinear(alaw[index] ?? 0), index * 2);
	return pcm;
}
function alawByteToLinear(value) {
	const aLaw = value ^ 85;
	const sign = aLaw & 128;
	const exponent = (aLaw & 112) >> 4;
	const mantissa = aLaw & 15;
	const sample = exponent === 0 ? (mantissa << 4) + 8 : (mantissa << 4) + 264 << exponent - 1;
	return sign ? sample : -sample;
}
//#endregion
//#region src/meeting-bot/realtime-engine-support.ts
const MEETING_REALTIME_CANCELLATION_RACE_DETAIL = "Cancellation failed: no active response found";
function meetingOutputBytesPerMs(audioFormat) {
	return audioFormat === "g711-ulaw-8khz" ? 8 : 48;
}
function resolveMeetingRealtimeProvider(params) {
	const providerId = params.config.realtime.voiceProvider ?? params.config.realtime.provider;
	return resolveConfiguredRealtimeVoiceProvider({
		configuredProviderId: providerId,
		providerConfigs: params.config.realtime.providers,
		cfg: params.fullConfig,
		agentId: params.config.realtime.agentId,
		providers: params.providers,
		defaultModel: params.config.realtime.model,
		noRegisteredProviderMessage: "No configured realtime voice provider registered"
	});
}
function resolveMeetingRealtimeTranscriptionProvider(params) {
	const providers = params.providers ?? listRealtimeTranscriptionProviders(params.fullConfig);
	if (providers.length === 0) throw new Error("No configured realtime transcription provider registered");
	const providerId = params.config.realtime.transcriptionProvider ?? params.config.realtime.provider;
	const provider = (providerId ? params.providers?.find((entry) => entry.id === providerId || entry.aliases?.includes(providerId)) ?? getRealtimeTranscriptionProvider(providerId, params.fullConfig) : void 0) ?? providers[0];
	if (!provider) throw new Error("No configured realtime transcription provider registered");
	const rawConfig = providerId ? params.config.realtime.providers[providerId] ?? params.config.realtime.providers[provider.id] ?? {} : params.config.realtime.providers[provider.id] ?? {};
	const providerConfig = provider.resolveConfig ? provider.resolveConfig({
		cfg: params.fullConfig,
		rawConfig
	}) : rawConfig;
	if (!provider.isConfigured({
		cfg: params.fullConfig,
		providerConfig
	})) throw new Error(`Realtime transcription provider "${provider.id}" is not configured`);
	return {
		provider,
		providerConfig
	};
}
function buildMeetingSpeakExactUserMessage(text) {
	return ["Speak this exact OpenClaw answer to the meeting, without adding, removing, or rephrasing words.", `Answer: ${JSON.stringify(text)}`].join("\n");
}
function formatLogValue(value) {
	return (value ? truncateUtf16Safe(value.replace(/\s+/g, "_"), 180) : void 0) || "unknown";
}
function resolveProviderModelForLog(params) {
	return normalizeOptionalString(params.providerConfig.model) ?? normalizeOptionalString(params.providerConfig.modelId) ?? normalizeOptionalString(params.fallbackModel) ?? normalizeOptionalString(params.provider.defaultModel) ?? "provider-default";
}
function formatMeetingRealtimeVoiceModelLog(params) {
	return [
		`${params.logScope} realtime voice bridge starting: strategy=${formatLogValue(params.strategy)}`,
		`provider=${formatLogValue(params.provider.id)}`,
		`model=${formatLogValue(resolveProviderModelForLog({
			provider: params.provider,
			providerConfig: params.providerConfig,
			fallbackModel: params.fallbackModel
		}))}`,
		`audioFormat=${formatLogValue(params.audioFormat)}`
	].join(" ");
}
function formatMeetingAgentAudioModelLog(params) {
	return [
		`${params.logScope} agent audio bridge starting: transcriptionProvider=${formatLogValue(params.provider.id)}`,
		`transcriptionModel=${formatLogValue(resolveProviderModelForLog({
			provider: params.provider,
			providerConfig: params.providerConfig
		}))}`,
		"tts=telephony",
		`audioFormat=${formatLogValue(params.audioFormat)}`
	].join(" ");
}
function formatMeetingAgentTtsResultLog(logScope, prefix, result) {
	return [
		`${logScope} ${prefix} TTS: provider=${formatLogValue(result.provider)}`,
		`model=${formatLogValue(result.providerModel)}`,
		`voice=${formatLogValue(result.providerVoice)}`,
		`outputFormat=${formatLogValue(result.outputFormat)}`,
		`sampleRate=${result.sampleRate ?? "unknown"}`,
		...result.fallbackFrom ? [`fallbackFrom=${formatLogValue(result.fallbackFrom)}`] : []
	].join(" ");
}
function formatMeetingTranscriptSummaryLog(logScope, prefix, text) {
	return `${logScope} ${prefix}: chars=${text.length}`;
}
function normalizeMeetingTtsPromptText(text) {
	const trimmed = text?.trim();
	if (!trimmed) return;
	const sayExactly = trimmed.match(/^say exactly:\s*(?<text>.+)$/is)?.groups?.text?.trim();
	if (sayExactly) return sayExactly.replace(/^["']|["']$/g, "").trim() || trimmed;
	return trimmed;
}
function createMeetingRealtimeLifecycleHandlers(params) {
	const onEvent = (event) => {
		if (event.direction === "server" && event.type === "session.created") params.setContinuityResetActive(false);
		if (event.direction === "client" && event.type === "session.continuity.reset") {
			if (params.getContinuityResetActive()) return;
			params.setContinuityResetActive(true);
			params.setRealtimeReady(false);
			params.outputOwner.reset();
			params.setOutputGenerationActive(false);
			params.resetToolContinuity(event.type);
			const turnId = params.harness.talk.activeTurnId;
			params.invalidateOutputPlayback();
			params.harness.flushOutput(params.clearOutputPlayback);
			params.harness.finishOutputAudio(event.type);
			if (turnId) params.harness.talk.cancelTurn({
				turnId,
				payload: {
					...params.outputTalkPayload,
					reason: event.type
				}
			});
			return;
		}
		params.outputOwner.noteEvent(event);
		if (event.type === "input_audio_buffer.speech_started") params.harness.ensureTurn();
		else if (event.type === "input_audio_buffer.speech_stopped") {
			const turnId = params.harness.talk.activeTurnId;
			if (!turnId) return;
			params.harness.emit({
				type: "input.audio.committed",
				turnId,
				payload: {
					...params.outputTalkPayload,
					source: event.type
				},
				final: true
			});
		} else if (event.type === "error" && event.detail === MEETING_REALTIME_CANCELLATION_RACE_DETAIL) {
			if (params.outputOwner.clearBlocked()) {
				params.setOutputGenerationActive(false);
				params.harness.finishOutputAudio(event.type);
			}
		} else if (event.type === "error") params.harness.emit({
			type: "session.error",
			payload: { message: event.detail ?? "Realtime provider error" },
			final: true
		});
		if (event.type === "error" || event.type === "response.done" || event.type === "input_audio_buffer.speech_started" || event.type === "input_audio_buffer.speech_stopped" || event.type === "conversation.item.input_audio_transcription.completed" || event.type === "conversation.item.input_audio_transcription.failed") {
			const detail = event.detail ? ` ${event.detail}` : "";
			params.logger.info(`${params.logScope} ${params.realtimeLogScope} ${event.direction}:${event.type}${detail}`);
		}
	};
	const onResponseDone = (outcome) => {
		if (!params.outputOwner.terminal(outcome.responseId)) return;
		params.setOutputGenerationActive(false);
		if (outcome.status === "failed" || outcome.status === "incomplete") params.logger.warn(`${params.logScope} ${params.realtimeLogScope} response ${outcome.status}: ${outcome.message}`);
	};
	return {
		onEvent,
		onResponseDone
	};
}
//#endregion
//#region src/meeting-bot/realtime-output-owner.ts
const STALE_RESPONSE_LIMIT = 16;
const AUDIO_DELTA_EVENTS = /* @__PURE__ */ new Set([
	"conversation.output_audio.delta",
	"response.audio.delta",
	"response.output_audio.delta"
]);
function createMeetingRealtimeOutputOwner() {
	let nextResponseId;
	let announcedResponseId;
	let currentResponseId;
	let blocked;
	const staleResponseIds = /* @__PURE__ */ new Set();
	const rememberStale = (responseId) => {
		staleResponseIds.delete(responseId);
		staleResponseIds.add(responseId);
		while (staleResponseIds.size > STALE_RESPONSE_LIMIT) {
			const oldest = staleResponseIds.values().next().value;
			if (!oldest) break;
			staleResponseIds.delete(oldest);
		}
	};
	return {
		accept(responseId) {
			if (responseId && staleResponseIds.has(responseId)) return false;
			if (blocked) {
				if (!blocked.responseId || !responseId || responseId === blocked.responseId) return false;
				blocked = void 0;
			}
			if (responseId) currentResponseId = responseId;
			return true;
		},
		block() {
			if (blocked) return {
				blocked: false,
				token: blocked.token
			};
			const token = Symbol("meeting-realtime-output-blocked");
			const responseId = currentResponseId ?? announcedResponseId;
			blocked = {
				...responseId ? { responseId } : {},
				token
			};
			if (responseId) rememberStale(responseId);
			nextResponseId = void 0;
			return {
				blocked: true,
				token
			};
		},
		clearBlocked() {
			if (!blocked) return false;
			blocked = void 0;
			return true;
		},
		isBlockedBy(token) {
			return blocked?.token === token;
		},
		noteEvent(event) {
			if (event.direction === "server" && event.type === "response.created" && event.responseId) {
				announcedResponseId = event.responseId;
				nextResponseId = void 0;
				return;
			}
			nextResponseId = event.direction === "server" && AUDIO_DELTA_EVENTS.has(event.type) ? event.responseId ?? announcedResponseId : void 0;
		},
		providerClear() {
			if (blocked) {
				if (!blocked.responseId) blocked = void 0;
				return false;
			}
			const responseId = currentResponseId ?? announcedResponseId;
			if (responseId) {
				blocked = {
					responseId,
					token: Symbol("meeting-realtime-output-blocked")
				};
				rememberStale(responseId);
			}
			nextResponseId = void 0;
			return true;
		},
		reset() {
			nextResponseId = void 0;
			announcedResponseId = void 0;
			currentResponseId = void 0;
			blocked = void 0;
			staleResponseIds.clear();
		},
		takeNextResponseId() {
			const responseId = nextResponseId ?? announcedResponseId;
			nextResponseId = void 0;
			return responseId;
		},
		terminal(responseId) {
			if (!responseId || !blocked?.responseId || blocked.responseId === responseId) blocked = void 0;
			if (!responseId || announcedResponseId === responseId) announcedResponseId = void 0;
			if (responseId && currentResponseId && currentResponseId !== responseId) return false;
			currentResponseId = void 0;
			return true;
		}
	};
}
//#endregion
//#region src/meeting-bot/realtime-tool-continuity.ts
const meetingRealtimeToolAbortSignals = /* @__PURE__ */ new WeakMap();
function readMeetingRealtimeToolAbortSignal(session) {
	return meetingRealtimeToolAbortSignals.get(session);
}
function createMeetingRealtimeToolContinuity(handleToolCall) {
	let epoch = 0;
	const activeControllers = /* @__PURE__ */ new Set();
	const reset = (reason) => {
		epoch += 1;
		for (const controller of activeControllers) controller.abort(reason);
		activeControllers.clear();
	};
	const run = (params) => {
		const callEpoch = epoch;
		const controller = new AbortController();
		activeControllers.add(controller);
		const isActive = () => !controller.signal.aborted && callEpoch === epoch;
		const turnId = params.harness.ensureTurn();
		params.harness.emit({
			type: "tool.call",
			turnId,
			itemId: params.call.event.itemId,
			callId: params.call.event.callId,
			payload: {
				name: params.call.event.name,
				args: params.call.event.args
			}
		});
		const guardedSession = Object.create(params.session);
		meetingRealtimeToolAbortSignals.set(guardedSession, controller.signal);
		guardedSession.submitToolResult = (callId, result, options) => {
			if (!isActive()) return;
			return params.session.submitToolResult(callId, result, options);
		};
		return handleToolCall({
			...params.call,
			session: guardedSession,
			onTalkEvent: (event) => {
				if (isActive()) params.harness.emit({
					...event,
					turnId: event.turnId ?? turnId
				});
			}
		}).catch((error) => {
			if (isActive()) throw error;
		}).finally(() => {
			meetingRealtimeToolAbortSignals.delete(guardedSession);
			activeControllers.delete(controller);
		});
	};
	return {
		reset,
		run
	};
}
const MEETING_OUTPUT_ECHO_SUPPRESSION_TAIL_MS = 3e3;
const MEETING_TRANSCRIPT_ECHO_LOOKBACK_MS = 45e3;
const MEETING_REALTIME_OUTPUT_MAX_PENDING_MS = 2e3;
const MEETING_REALTIME_OUTPUT_MAX_WRITE_MS = 500;
const MEETING_REALTIME_OUTPUT_MAX_PENDING_FRAMES = 256;
async function startMeetingRealtimeEngine(params) {
	let stopped = false;
	let stopPromise;
	let bridgeClosed = false;
	let transportStopped = false;
	let transportDisposed = false;
	let bridge = void 0;
	let realtimeReady = false;
	let lastClearAt;
	let clearCount = 0;
	let outputGeneration = 0;
	let outputWriteActive = false;
	let outputTransportWriteStarted = false;
	let outputClearPending = 0;
	let outputClearAfterActive = false;
	let outputPendingBytes = 0;
	let outputPendingFrames = 0;
	let outputGenerationActive = false;
	let continuityResetActive = false;
	let outputClearTail = Promise.resolve();
	const outputQueue = [];
	const outputOwner = createMeetingRealtimeOutputOwner();
	const toolContinuity = createMeetingRealtimeToolContinuity(params.handleToolCall);
	const outputMaxPendingBytes = meetingOutputBytesPerMs(params.config.chrome.audioFormat) * MEETING_REALTIME_OUTPUT_MAX_PENDING_MS;
	const outputMaxWriteBytes = meetingOutputBytesPerMs(params.config.chrome.audioFormat) * MEETING_REALTIME_OUTPUT_MAX_WRITE_MS;
	const realtimeLogScope = params.logPrefix ? `${params.logPrefix} realtime` : "realtime";
	const invalidateOutputQueue = () => {
		outputGeneration += 1;
		outputQueue.length = 0;
		outputPendingBytes = 0;
		outputPendingFrames = 0;
		outputGenerationActive = false;
	};
	const stop = async () => {
		if (!stopped) {
			stopped = true;
			outputOwner.reset();
			outputClearAfterActive = false;
			invalidateOutputQueue();
			toolContinuity.reset("meeting realtime stopped");
		}
		if (stopPromise) {
			await stopPromise;
			return;
		}
		const cleanup = Promise.resolve().then(async () => {
			if (!bridgeClosed) {
				bridgeClosed = true;
				harness.close();
				try {
					bridge?.close();
				} catch (error) {
					params.logger.debug?.(`${params.platform.logScope} ${realtimeLogScope}${params.logPrefix ? "" : " voice"} bridge close ignored: ${formatErrorMessage(error)}`);
				}
			}
			let cleanupError;
			if (!transportStopped) try {
				await params.transport.stop();
				transportStopped = true;
			} catch (error) {
				cleanupError = error;
			}
			if (!transportDisposed) try {
				await params.transport.dispose();
				transportDisposed = true;
			} catch (error) {
				cleanupError ??= error;
			}
			if (cleanupError) throw cleanupError instanceof Error ? cleanupError : new Error("Meeting realtime transport cleanup failed", { cause: cleanupError });
		});
		stopPromise = cleanup;
		try {
			await cleanup;
		} finally {
			if (stopPromise === cleanup) stopPromise = void 0;
		}
	};
	const stopAfterFailure = (source) => {
		stop().catch((error) => {
			params.logger.warn(`${params.platform.logScope} ${realtimeLogScope} ${source} cleanup failed: ${formatErrorMessage(error)}`);
		});
	};
	const queueOutputClear = () => {
		if (stopped) return Promise.resolve();
		clearCount += 1;
		lastClearAt = (/* @__PURE__ */ new Date()).toISOString();
		outputClearPending += 1;
		const clear = outputClearTail.then(async () => {
			if (!stopped) await params.transport.clearOutput();
		}).catch((error) => {
			params.logger.warn(`${params.platform.logScope} ${params.logPrefix ? `${params.logPrefix} audio clear` : "audio output clear"} failed: ${formatErrorMessage(error)}`);
			stopAfterFailure("audio output clear");
		}).finally(() => {
			outputClearPending -= 1;
			pumpOutputQueue();
		});
		outputClearTail = clear;
		return clear;
	};
	const pumpOutputQueue = () => {
		if (stopped || outputWriteActive || outputClearPending > 0) return;
		const next = outputQueue.shift();
		if (!next) return;
		if (next.generation !== outputGeneration) {
			pumpOutputQueue();
			return;
		}
		const batch = [next.audio];
		let batchBytes = next.audio.byteLength;
		let batchFrames = 1;
		while (batchBytes < outputMaxWriteBytes) {
			const queued = outputQueue[0];
			if (!queued || queued.generation !== next.generation || queued.audio.byteLength > outputMaxWriteBytes - batchBytes) break;
			outputQueue.shift();
			batch.push(queued.audio);
			batchBytes += queued.audio.byteLength;
			batchFrames += 1;
		}
		const audio = batch.length === 1 ? next.audio : Buffer.concat(batch, batchBytes);
		outputWriteActive = true;
		Promise.resolve().then(async () => {
			if (stopped || next.generation !== outputGeneration) return;
			outputTransportWriteStarted = true;
			await params.transport.writeOutput(audio);
		}).catch((error) => {
			if (stopped || next.generation !== outputGeneration) return;
			params.logger.warn(`${params.platform.logScope} ${params.logPrefix ? `${params.logPrefix} audio output` : "audio output"} failed: ${formatErrorMessage(error)}`);
			stopAfterFailure("audio output");
		}).finally(() => {
			outputWriteActive = false;
			outputTransportWriteStarted = false;
			if (next.generation === outputGeneration) {
				outputPendingBytes -= batchBytes;
				outputPendingFrames -= batchFrames;
			}
			if (outputClearAfterActive && !stopped) {
				outputClearAfterActive = false;
				queueOutputClear();
				return;
			}
			pumpOutputQueue();
		});
	};
	const clearOutputPlayback = () => {
		queueOutputClear();
	};
	const invalidateOutputPlayback = () => {
		outputClearAfterActive ||= outputTransportWriteStarted;
		invalidateOutputQueue();
	};
	const invalidateAndClearOutputPlayback = () => {
		blockOutput();
		clearOutputPlayback();
	};
	const blockOutput = () => {
		const result = outputOwner.block();
		invalidateOutputPlayback();
		return result;
	};
	const handleOutputBackpressure = () => {
		const pendingBytes = outputPendingBytes;
		const pendingFrames = outputPendingFrames;
		const { blocked, token } = blockOutput();
		if (!blocked) return;
		params.logger.warn(`${params.platform.logScope} ${realtimeLogScope} audio output backpressured: pendingBytes=${pendingBytes} pendingFrames=${pendingFrames}`);
		harness.flushOutput(clearOutputPlayback);
		harness.finishOutputAudio("output-backpressure");
		queueMicrotask(() => {
			if (stopped || !outputOwner.isBlockedBy(token)) return;
			harness.handleBargeIn({
				audioPlaybackActive: true,
				force: true
			}, () => {});
		});
	};
	const queueOutputAudio = (audio, responseId) => {
		if (stopped || !outputOwner.accept(responseId)) return false;
		if (audio.byteLength > outputMaxPendingBytes - outputPendingBytes || outputPendingFrames >= MEETING_REALTIME_OUTPUT_MAX_PENDING_FRAMES) {
			handleOutputBackpressure();
			return false;
		}
		outputPendingBytes += audio.byteLength;
		outputPendingFrames += 1;
		outputQueue.push({
			audio,
			generation: outputGeneration
		});
		pumpOutputQueue();
		return true;
	};
	const startHumanBargeInMonitor = () => {
		if (!params.transport.startBargeInMonitor) return;
		params.transport.startBargeInMonitor(() => {
			if (stopped || !harness.outputActivity.isInterruptible()) return false;
			const now = Date.now();
			const playbackActive = harness.isOutputPlaybackWindowActive();
			const lastOutputAudioAt = harness.outputActivity.snapshot().lastAudioAt;
			if (!playbackActive && (lastOutputAudioAt === void 0 || now - lastOutputAudioAt > 1e3)) return false;
			harness.handleBargeIn({ audioPlaybackActive: true }, invalidateAndClearOutputPlayback);
			return true;
		});
	};
	const resolved = resolveMeetingRealtimeProvider({
		config: params.config,
		fullConfig: params.fullConfig,
		providers: params.providers
	});
	const strategy = params.config.realtime.strategy;
	params.logger.info(formatMeetingRealtimeVoiceModelLog({
		logScope: params.platform.logScope,
		strategy,
		provider: resolved.provider,
		providerConfig: resolved.providerConfig,
		fallbackModel: params.config.realtime.model,
		audioFormat: params.config.chrome.audioFormat
	}));
	const meetingTalkPayload = params.talkContext ? {
		bridgeId: params.talkContext.bridgeId,
		meetingSessionId: params.meetingSessionId
	} : { meetingSessionId: params.meetingSessionId };
	const outputTalkPayload = params.talkContext ? { bridgeId: params.talkContext.bridgeId } : { meetingSessionId: params.meetingSessionId };
	const reasonTalkPayload = (reason) => params.talkContext ? {
		bridgeId: params.talkContext.bridgeId,
		reason
	} : { reason };
	const harness = createRealtimeVoiceSessionHarness({
		talk: {
			sessionId: params.talkSessionId ?? `${params.platform.sessionIdPrefix}:${params.meetingSessionId}:command-realtime`,
			mode: "realtime",
			transport: "gateway-relay",
			brain: strategy === "bidi" ? "direct-tools" : "agent-consult",
			provider: resolved.provider.id
		},
		talkPayloads: {
			turnStarted: () => meetingTalkPayload,
			turnEnded: reasonTalkPayload,
			inputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
			outputAudioStarted: () => outputTalkPayload,
			outputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
			outputAudioDone: reasonTalkPayload
		},
		echoSuppression: {
			bytesPerMs: meetingOutputBytesPerMs(params.config.chrome.audioFormat),
			tailMs: MEETING_OUTPUT_ECHO_SUPPRESSION_TAIL_MS,
			transcriptLookbackMs: MEETING_TRANSCRIPT_ECHO_LOOKBACK_MS
		},
		talkback: {
			debounceMs: 900,
			logger: params.logger,
			logPrefix: `${params.platform.logScope} ${realtimeLogScope} agent`,
			responseStyle: "Brief, natural spoken answer for a live meeting.",
			fallbackText: "I hit an error while checking that. Please try again.",
			consult: ({ question, responseStyle }) => params.consultAgent({
				meetingSessionId: params.meetingSessionId,
				requesterSessionKey: params.requesterSessionKey,
				args: {
					question,
					responseStyle
				},
				transcript: harness.transcript
			}),
			deliver: (text) => {
				bridge?.sendUserMessage(buildMeetingSpeakExactUserMessage(text));
			}
		}
	});
	harness.emit({
		type: "session.started",
		payload: params.talkContext ? {
			...meetingTalkPayload,
			nodeId: params.talkContext.nodeId
		} : meetingTalkPayload
	});
	params.transport.onFatal(() => {
		stopAfterFailure("audio transport");
	});
	if (stopped) throw new Error(`${params.platform.displayName} audio transport failed before realtime provider setup`);
	const lifecycleHandlers = createMeetingRealtimeLifecycleHandlers({
		clearOutputPlayback,
		getContinuityResetActive: () => continuityResetActive,
		harness,
		invalidateOutputPlayback,
		logger: params.logger,
		logScope: params.platform.logScope,
		outputOwner,
		outputTalkPayload,
		realtimeLogScope,
		resetToolContinuity: (reason) => toolContinuity.reset(reason),
		setContinuityResetActive: (active) => continuityResetActive = active,
		setOutputGenerationActive: (active) => outputGenerationActive = active,
		setRealtimeReady: (ready) => realtimeReady = ready
	});
	try {
		bridge = harness.createBridge({
			provider: resolved.provider,
			cfg: params.fullConfig,
			agentId: params.config.realtime.agentId,
			providerConfig: resolved.providerConfig,
			audioFormat: resolveMeetingRealtimeAudioFormat(params.config.chrome.audioFormat),
			instructions: params.config.realtime.instructions,
			initialGreetingInstructions: params.config.realtime.introMessage,
			autoRespondToAudio: strategy === "bidi",
			triggerGreetingOnReady: false,
			markStrategy: "ack-immediately",
			tools: strategy === "bidi" ? params.tools : [],
			audioSink: {
				isOpen: () => !stopped,
				sendAudio: (audio) => {
					const responseId = outputOwner.takeNextResponseId();
					if (!queueOutputAudio(audio, responseId)) return;
					if (!outputGenerationActive) {
						params.transport.beginOutput?.();
						outputGenerationActive = true;
					}
					harness.outputActivity.markPlaybackStarted();
					harness.recordOutputAudio(audio);
				},
				clearAudio: () => {
					if (outputOwner.providerClear()) {
						invalidateOutputPlayback();
						harness.flushOutput(clearOutputPlayback);
						harness.finishOutputAudio("clear");
					}
				}
			},
			onTranscript: (role, text, isFinal) => {
				const turnId = harness.ensureTurn();
				const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
				const payload = role === "assistant" ? { text } : {
					role,
					text
				};
				harness.emit({
					type: eventType,
					turnId,
					payload,
					final: isFinal
				});
				if (role === "user" && isFinal) harness.emit({
					type: "input.audio.committed",
					turnId,
					payload: outputTalkPayload,
					final: true
				});
				if (isFinal) {
					params.logger.info(formatMeetingTranscriptSummaryLog(params.platform.logScope, `${realtimeLogScope} ${role}`, text));
					if (role === "user" && strategy === "agent") {
						if (harness.isLikelyAssistantEchoTranscript(text)) {
							params.logger.info(formatMeetingTranscriptSummaryLog(params.platform.logScope, `${realtimeLogScope} ignored assistant echo transcript`, text));
							return;
						}
					}
					if (role === "user" && strategy === "agent") harness.talkback?.enqueue(text);
				}
			},
			onEvent: lifecycleHandlers.onEvent,
			onResponseDone: lifecycleHandlers.onResponseDone,
			onToolCall: (event, session) => toolContinuity.run({
				session,
				call: {
					strategy,
					event,
					meetingSessionId: params.meetingSessionId,
					requesterSessionKey: params.requesterSessionKey,
					transcript: harness.transcript
				},
				harness
			}),
			onError: (error) => {
				harness.emit({
					type: "session.error",
					payload: { message: formatErrorMessage(error) },
					final: true
				});
				params.logger.warn(`${params.platform.logScope} ${realtimeLogScope} voice bridge failed: ${formatErrorMessage(error)}`);
			},
			onClose: (reason) => {
				outputGenerationActive = false;
				realtimeReady = false;
				harness.finishOutputAudio(reason);
				harness.emit({
					type: "session.closed",
					payload: { reason },
					final: true
				});
				stopAfterFailure("voice bridge close");
			},
			onReady: () => {
				realtimeReady = true;
				continuityResetActive = false;
				harness.emit({
					type: "session.ready",
					payload: outputTalkPayload
				});
			}
		});
		startHumanBargeInMonitor();
		params.transport.startInput((audio) => {
			if (stopped || audio.byteLength === 0) return;
			if (!harness.recordInputAudio(audio)) return;
			bridge?.sendAudio(audio);
		});
		await bridge.connect();
		if (stopped) throw new Error(`${params.platform.displayName} audio transport stopped during realtime provider setup`);
	} catch (error) {
		try {
			await stop();
		} catch (cleanupError) {
			params.logger.debug?.(`${params.platform.logScope} ${realtimeLogScope} failed-start cleanup ignored: ${formatErrorMessage(cleanupError)}`);
			try {
				await stop();
			} catch (retryError) {
				params.logger.debug?.(`${params.platform.logScope} ${realtimeLogScope} failed-start cleanup retry ignored: ${formatErrorMessage(retryError)}`);
			}
		}
		throw error;
	}
	return {
		providerId: resolved.provider.id,
		speak: (instructions) => {
			bridge?.triggerGreeting(instructions);
		},
		getHealth: () => ({
			...harness.getHealth({
				providerConnected: bridge?.bridge.isConnected() ?? false,
				realtimeReady
			}),
			...params.transport.getHealth?.(),
			lastClearAt,
			clearCount,
			bridgeClosed: stopped
		}),
		stop
	};
}
//#endregion
//#region src/meeting-bot/realtime-agent-engine.ts
async function startMeetingAgentRealtimeEngine(params) {
	let stopped = false;
	let stopPromise;
	let sttSession = null;
	let realtimeReady = false;
	let ttsQueue = Promise.resolve();
	const agentLogScope = params.logPrefix ? `${params.logPrefix} agent` : "agent";
	const resolved = resolveMeetingRealtimeTranscriptionProvider({
		config: params.config,
		fullConfig: params.fullConfig,
		providers: params.providers
	});
	params.logger.info(formatMeetingAgentAudioModelLog({
		logScope: params.platform.logScope,
		provider: resolved.provider,
		providerConfig: resolved.providerConfig,
		audioFormat: params.config.chrome.audioFormat
	}));
	const stop = async () => {
		if (stopped) {
			await stopPromise;
			return;
		}
		stopped = true;
		stopPromise = (async () => {
			harness.close();
			try {
				sttSession?.close();
			} catch (error) {
				params.logger.debug?.(`${params.platform.logScope} ${agentLogScope} transcription bridge close ignored: ${formatErrorMessage(error)}`);
			}
			harness.emit({
				type: "session.closed",
				final: true,
				payload: { meetingSessionId: params.meetingSessionId }
			});
			try {
				await params.transport.stop();
			} finally {
				await params.transport.dispose();
			}
		})();
		await stopPromise;
	};
	const stopAfterFailure = (source) => {
		stop().catch((error) => {
			params.logger.warn(`${params.platform.logScope} ${agentLogScope} ${source} cleanup failed: ${formatErrorMessage(error)}`);
		});
	};
	const writeOutputAudio = async (audio) => {
		params.transport.beginOutput?.();
		harness.outputActivity.markPlaybackStarted();
		harness.recordOutputAudio(audio);
		await params.transport.writeOutput(audio);
	};
	const enqueueSpeakText = (text) => {
		const normalized = normalizeMeetingTtsPromptText(text);
		if (!normalized || stopped) return;
		ttsQueue = ttsQueue.then(async () => {
			if (stopped) return;
			harness.recordTranscript("assistant", normalized);
			params.logger.info(formatMeetingTranscriptSummaryLog(params.platform.logScope, `${agentLogScope} assistant`, normalized));
			const turnId = harness.ensureTurn();
			harness.emit({
				type: "output.text.done",
				turnId,
				final: true,
				payload: {
					meetingSessionId: params.meetingSessionId,
					text: normalized
				}
			});
			const result = await params.runtime.tts.textToSpeechTelephony({
				text: normalized,
				cfg: params.fullConfig
			});
			if (!result.success || !result.audioBuffer || !result.sampleRate) throw new Error(result.error ?? "TTS conversion failed");
			params.logger.info(formatMeetingAgentTtsResultLog(params.platform.logScope, agentLogScope, result));
			await writeOutputAudio(convertMeetingTtsAudioForBridge(result.audioBuffer, result.sampleRate, params.config.chrome.audioFormat, result.outputFormat, params.platform.displayName));
			harness.finishOutputAudio("completed");
			harness.endTurn();
		}).catch((error) => {
			harness.finishOutputAudio("failed");
			harness.endTurn("failed");
			params.logger.warn(`${params.platform.logScope} ${agentLogScope} TTS failed: ${formatErrorMessage(error)}`);
		});
	};
	const harness = createRealtimeVoiceSessionHarness({
		talk: {
			sessionId: `${params.platform.sessionIdPrefix}:${params.meetingSessionId}:agent`,
			mode: "stt-tts",
			transport: "gateway-relay",
			brain: "agent-consult",
			provider: resolved.provider.id,
			turnIdPrefix: `${params.platform.sessionIdPrefix}:${params.meetingSessionId}:turn`
		},
		talkPayloads: {
			turnStarted: () => ({ meetingSessionId: params.meetingSessionId }),
			turnEnded: () => ({ meetingSessionId: params.meetingSessionId }),
			inputAudioDelta: (audio) => ({
				meetingSessionId: params.meetingSessionId,
				bytes: audio.byteLength
			}),
			outputAudioStarted: () => ({ meetingSessionId: params.meetingSessionId }),
			outputAudioDelta: (audio) => ({
				meetingSessionId: params.meetingSessionId,
				bytes: audio.byteLength
			}),
			outputAudioDone: () => ({ meetingSessionId: params.meetingSessionId })
		},
		echoSuppression: {
			bytesPerMs: meetingOutputBytesPerMs(params.config.chrome.audioFormat),
			tailMs: MEETING_OUTPUT_ECHO_SUPPRESSION_TAIL_MS,
			transcriptLookbackMs: MEETING_TRANSCRIPT_ECHO_LOOKBACK_MS
		},
		talkback: {
			debounceMs: 900,
			logger: params.logger,
			logPrefix: `${params.platform.logScope} ${agentLogScope}`,
			responseStyle: "Brief, natural spoken answer for a live meeting.",
			fallbackText: "I hit an error while checking that. Please try again.",
			consult: ({ question, responseStyle }) => params.consultAgent({
				meetingSessionId: params.meetingSessionId,
				requesterSessionKey: params.requesterSessionKey,
				args: {
					question,
					responseStyle
				},
				transcript: harness.transcript
			}),
			deliver: enqueueSpeakText
		}
	});
	params.transport.onFatal(() => {
		stopAfterFailure("audio transport");
	});
	if (stopped) throw new Error(`${params.platform.displayName} audio transport failed before transcription provider setup`);
	try {
		sttSession = resolved.provider.createSession({
			cfg: params.fullConfig,
			providerConfig: resolved.providerConfig,
			onTranscript: (text) => {
				const trimmed = text.trim();
				if (!trimmed || stopped) return;
				const turnId = harness.ensureTurn();
				harness.emit({
					type: "input.audio.committed",
					turnId,
					final: true,
					payload: { meetingSessionId: params.meetingSessionId }
				});
				harness.emit({
					type: "transcript.done",
					turnId,
					final: true,
					payload: {
						meetingSessionId: params.meetingSessionId,
						text: trimmed,
						role: "user"
					}
				});
				harness.recordTranscript("user", trimmed);
				params.logger.info(formatMeetingTranscriptSummaryLog(params.platform.logScope, `${agentLogScope} user`, trimmed));
				if (harness.isLikelyAssistantEchoTranscript(trimmed)) {
					params.logger.info(formatMeetingTranscriptSummaryLog(params.platform.logScope, `${agentLogScope} ignored assistant echo transcript`, trimmed));
					return;
				}
				harness.talkback?.enqueue(trimmed);
			},
			onError: (error) => {
				params.logger.warn(`${params.platform.logScope} ${agentLogScope} transcription bridge failed: ${formatErrorMessage(error)}`);
				harness.emit({
					type: "session.error",
					final: true,
					payload: {
						meetingSessionId: params.meetingSessionId,
						error: formatErrorMessage(error)
					}
				});
				stopAfterFailure("transcription bridge");
			}
		});
		harness.emit({
			type: "session.started",
			payload: {
				meetingSessionId: params.meetingSessionId,
				provider: resolved.provider.id
			}
		});
		params.transport.startInput((audio) => {
			if (stopped || !realtimeReady || audio.byteLength === 0) return;
			if (!harness.recordInputAudio(audio)) return;
			sttSession?.sendAudio(convertMeetingBridgeAudioForStt(audio, params.config.chrome.audioFormat));
		});
		await sttSession.connect();
	} catch (error) {
		try {
			await stop();
		} catch (cleanupError) {
			params.logger.debug?.(`${params.platform.logScope} ${agentLogScope} failed-start cleanup ignored: ${formatErrorMessage(cleanupError)}`);
		}
		throw error;
	}
	if (stopped) throw new Error(`${params.platform.displayName} audio transport stopped during transcription provider setup`);
	realtimeReady = true;
	harness.emit({
		type: "session.ready",
		payload: { meetingSessionId: params.meetingSessionId }
	});
	return {
		providerId: resolved.provider.id,
		speak: enqueueSpeakText,
		getHealth: () => ({
			...harness.getHealth({
				providerConnected: sttSession?.isConnected() ?? false,
				realtimeReady
			}),
			...params.transport.getHealth?.(),
			bridgeClosed: stopped
		}),
		stop
	};
}
//#endregion
//#region src/meeting-bot/bridge-process.ts
function hasExited(proc) {
	return proc.exitCode !== null || proc.signalCode !== null;
}
function waitForExit(proc, timeoutMs) {
	if (hasExited(proc)) return Promise.resolve(true);
	return new Promise((resolve) => {
		let settled = false;
		const finish = (exited) => {
			if (settled) return;
			settled = true;
			clearTimeout(timeout);
			proc.off("exit", onExit);
			resolve(exited);
		};
		const onExit = () => finish(true);
		const timeout = setTimeout(() => finish(hasExited(proc)), timeoutMs);
		timeout.unref?.();
		proc.once("exit", onExit);
		if (hasExited(proc)) finish(true);
	});
}
/** Settles after the bridge exits or the bounded force-kill sequence finishes. */
async function terminateMeetingBridgeProcess(proc, options) {
	if (!proc || hasExited(proc)) return;
	const initialSignal = options.initialSignal ?? "SIGTERM";
	try {
		if (!proc.kill(initialSignal)) return;
	} catch {
		return;
	}
	const forceKillWaitMs = options.forceKillWaitMs ?? 1e3;
	if (initialSignal === "SIGKILL") {
		await waitForExit(proc, forceKillWaitMs);
		return;
	}
	if (await waitForExit(proc, options.graceMs)) return;
	try {
		if (!proc.kill("SIGKILL")) return;
	} catch {
		return;
	}
	await waitForExit(proc, forceKillWaitMs);
}
//#endregion
//#region src/meeting-bot/command-argv.ts
function splitCommandArgv(argv, commandLabel) {
	const [command, ...args] = argv;
	if (!command) throw new Error(`${commandLabel} must not be empty`);
	return {
		command,
		args
	};
}
//#endregion
//#region src/meeting-bot/output-loopback-verifier.ts
const OUTPUT_LOOPBACK_OBSERVATION_WINDOW_MS = 5e3;
const OUTPUT_LOOPBACK_REFERENCE_MS = 500;
const OUTPUT_LOOPBACK_INPUT_HISTORY_MS = 1e4;
const OUTPUT_LOOPBACK_FINGERPRINT_POINTS = 64;
const OUTPUT_LOOPBACK_MIN_FINGERPRINT_POINTS = 16;
const OUTPUT_LOOPBACK_FULL_CORRELATION_THRESHOLD = .9;
const OUTPUT_LOOPBACK_SHORT_CORRELATION_THRESHOLD = .98;
const OUTPUT_LOOPBACK_RMS_THRESHOLD = 8;
const OUTPUT_LOOPBACK_PEAK_THRESHOLD = 32;
function decodeMeetingAudio(audio, audioFormat) {
	return audioFormat === "g711-ulaw-8khz" ? mulawToPcm(audio) : audio;
}
function hasSignal(stats) {
	return stats.rms >= OUTPUT_LOOPBACK_RMS_THRESHOLD || stats.peak >= OUTPUT_LOOPBACK_PEAK_THRESHOLD;
}
function createOutputFingerprint(pcm, fullReferenceBytes) {
	const totalSamples = Math.floor(pcm.byteLength * .5);
	let firstSignalSample = 0;
	while (firstSignalSample < totalSamples && Math.abs(pcm.readInt16LE(firstSignalSample * 2)) < OUTPUT_LOOPBACK_PEAK_THRESHOLD) firstSignalSample += 1;
	let lastSignalSample = totalSamples - 1;
	while (lastSignalSample >= firstSignalSample && Math.abs(pcm.readInt16LE(lastSignalSample * 2)) < OUTPUT_LOOPBACK_PEAK_THRESHOLD) lastSignalSample -= 1;
	const activePcm = pcm.subarray(firstSignalSample * 2, (lastSignalSample + 1) * 2);
	const sampleCount = Math.floor(activePcm.byteLength * .5);
	const pointCount = Math.min(OUTPUT_LOOPBACK_FINGERPRINT_POINTS, Math.floor(sampleCount * .25));
	if (pointCount < OUTPUT_LOOPBACK_MIN_FINGERPRINT_POINTS) return;
	const inversePointCount = pointCount ** -1;
	const selected = [];
	for (let point = 0; point < pointCount; point += 1) {
		const start = Math.floor(point * sampleCount * inversePointCount);
		const end = Math.max(start + 1, Math.floor((point + 1) * sampleCount * inversePointCount));
		let selectedIndex = start;
		let selectedSample = activePcm.readInt16LE(start * 2);
		for (let index = start + 1; index < end; index += 1) {
			const sample = activePcm.readInt16LE(index * 2);
			if (Math.abs(sample) > Math.abs(selectedSample)) {
				selectedIndex = index;
				selectedSample = sample;
			}
		}
		selected.push({
			index: selectedIndex,
			sample: selectedSample
		});
	}
	const baseIndex = selected[0]?.index ?? 0;
	const samples = selected.map((entry) => entry.sample);
	const inverseSamples = samples.length ** -1;
	const referenceMean = samples.reduce((sum, sample) => sum + sample, 0) * inverseSamples;
	const referenceVariance = samples.reduce((sum, sample) => {
		const delta = sample - referenceMean;
		return sum + delta * delta;
	}, 0);
	if (referenceVariance === 0) return;
	const offsets = selected.map((entry) => entry.index - baseIndex);
	return {
		offsets,
		referenceMean,
		referenceVariance,
		samples,
		spanSamples: (offsets.at(-1) ?? 0) + 1,
		threshold: activePcm.byteLength < fullReferenceBytes ? OUTPUT_LOOPBACK_SHORT_CORRELATION_THRESHOLD : OUTPUT_LOOPBACK_FULL_CORRELATION_THRESHOLD
	};
}
function fingerprintCorrelation(pcm, startSample, fingerprint) {
	let inputSum = 0;
	for (const offset of fingerprint.offsets) inputSum += pcm.readInt16LE((startSample + offset) * 2);
	const inputMean = inputSum * fingerprint.offsets.length ** -1;
	let covariance = 0;
	let inputVariance = 0;
	for (let index = 0; index < fingerprint.offsets.length; index += 1) {
		const referenceDelta = (fingerprint.samples[index] ?? 0) - fingerprint.referenceMean;
		const inputDelta = pcm.readInt16LE((startSample + (fingerprint.offsets[index] ?? 0)) * 2) - inputMean;
		covariance += referenceDelta * inputDelta;
		inputVariance += inputDelta * inputDelta;
	}
	if (inputVariance === 0) return 0;
	return Math.abs(covariance * (fingerprint.referenceVariance * inputVariance) ** -.5);
}
/** Correlates sink audio with the same waveform returning on the microphone capture path. */
function createMeetingOutputLoopbackVerifier(options) {
	const now = options.now ?? Date.now;
	const sampleRate = options.audioFormat === "g711-ulaw-8khz" ? 8e3 : 24e3;
	const fullReferenceBytes = sampleRate * OUTPUT_LOOPBACK_REFERENCE_MS * .001 * 2;
	const maxInputHistoryBytes = sampleRate * OUTPUT_LOOPBACK_INPUT_HISTORY_MS * .001 * 2;
	let generationStarted = false;
	let generationVerified = false;
	let outputGeneration = 0;
	let verifiedOutputGeneration;
	let inputPcm = Buffer.alloc(0);
	let nextInputStartSample = 0;
	let outputFingerprint;
	let pendingOutputPcm = Buffer.alloc(0);
	let outputObservationDeadlineMs = Number.NEGATIVE_INFINITY;
	let outputQueuedUntilMs = Number.NEGATIVE_INFINITY;
	let outputLoopbackSignalBytes = 0;
	let lastOutputLoopbackAt;
	let lastOutputLoopbackCorrelation;
	let lastOutputLoopbackPeak;
	let lastOutputLoopbackRms;
	const resetGeneration = (started) => {
		generationStarted = started;
		generationVerified = false;
		if (started) outputGeneration += 1;
		inputPcm = Buffer.alloc(0);
		nextInputStartSample = 0;
		outputFingerprint = void 0;
		pendingOutputPcm = Buffer.alloc(0);
		outputObservationDeadlineMs = Number.NEGATIVE_INFINITY;
		outputQueuedUntilMs = Number.NEGATIVE_INFINITY;
	};
	const refreshFingerprint = (fingerprint) => {
		outputFingerprint = fingerprint;
		const inputSampleCount = Math.floor(inputPcm.byteLength * .5);
		const rescanTailSamples = sampleRate * OUTPUT_LOOPBACK_REFERENCE_MS * .001;
		nextInputStartSample = Math.max(0, inputSampleCount - rescanTailSamples);
	};
	const consumePendingOutput = (allowShortReference) => {
		const pendingBytes = pendingOutputPcm.byteLength;
		for (let end = pendingBytes; end >= fullReferenceBytes; end -= fullReferenceBytes) {
			const fingerprint = createOutputFingerprint(pendingOutputPcm.subarray(end - fullReferenceBytes, end), fullReferenceBytes);
			if (fingerprint) {
				pendingOutputPcm = Buffer.alloc(0);
				refreshFingerprint(fingerprint);
				return;
			}
		}
		const residualBytes = pendingBytes % fullReferenceBytes;
		if (residualBytes > 0) {
			const fingerprint = createOutputFingerprint(pendingOutputPcm.subarray(0, residualBytes), fullReferenceBytes);
			if (fingerprint) {
				pendingOutputPcm = Buffer.alloc(0);
				refreshFingerprint(fingerprint);
				return;
			}
		}
		pendingOutputPcm = pendingBytes > fullReferenceBytes ? pendingOutputPcm.subarray(pendingBytes - fullReferenceBytes) : pendingOutputPcm;
		if (!outputFingerprint && allowShortReference && pendingOutputPcm.byteLength > 0) {
			const fingerprint = createOutputFingerprint(pendingOutputPcm, fullReferenceBytes);
			if (fingerprint) {
				pendingOutputPcm = Buffer.alloc(0);
				refreshFingerprint(fingerprint);
			}
		}
	};
	return {
		beginOutput() {
			resetGeneration(true);
		},
		cancelOutput() {
			resetGeneration(false);
		},
		recordInput(audio) {
			const capturedAtMs = now();
			if (!generationStarted || generationVerified || audio.byteLength === 0 || capturedAtMs > outputObservationDeadlineMs) return;
			const decoded = decodeMeetingAudio(audio, options.audioFormat);
			const chunkStats = readPcm16AudioStats(decoded);
			const combinedInput = inputPcm.byteLength > 0 ? Buffer.concat([inputPcm, decoded]) : decoded;
			const droppedInputBytes = Math.max(0, combinedInput.byteLength - maxInputHistoryBytes);
			inputPcm = droppedInputBytes > 0 ? combinedInput.subarray(droppedInputBytes) : combinedInput;
			if (droppedInputBytes > 0) nextInputStartSample = Math.max(0, nextInputStartSample - Math.floor(droppedInputBytes * .5));
			const fingerprint = outputFingerprint;
			if (!fingerprint || !hasSignal(chunkStats)) return;
			const lastStartSample = Math.floor(inputPcm.byteLength * .5) - fingerprint.spanSamples;
			for (let startSample = nextInputStartSample; startSample <= lastStartSample; startSample += 1) {
				const matchedCorrelation = fingerprintCorrelation(inputPcm, startSample, fingerprint);
				if (matchedCorrelation < fingerprint.threshold) continue;
				const matchedPcm = inputPcm.subarray(startSample * 2, (startSample + fingerprint.spanSamples) * 2);
				const matchedStats = readPcm16AudioStats(matchedPcm);
				if (!hasSignal(matchedStats)) continue;
				generationVerified = true;
				verifiedOutputGeneration = outputGeneration;
				outputLoopbackSignalBytes += audio.byteLength;
				lastOutputLoopbackAt = new Date(capturedAtMs).toISOString();
				lastOutputLoopbackCorrelation = matchedCorrelation;
				lastOutputLoopbackPeak = matchedStats.peak;
				lastOutputLoopbackRms = matchedStats.rms;
				return;
			}
			nextInputStartSample = Math.max(0, lastStartSample + 1);
		},
		recordOutput(audio) {
			const outputAtMs = now();
			if (!generationStarted || outputObservationDeadlineMs !== Number.NEGATIVE_INFINITY && outputAtMs > outputObservationDeadlineMs) resetGeneration(true);
			if (audio.byteLength === 0) return;
			const decoded = decodeMeetingAudio(audio, options.audioFormat);
			const durationMs = decoded.byteLength * .5 * sampleRate ** -1 * 1e3;
			outputQueuedUntilMs = Math.max(outputAtMs, outputQueuedUntilMs) + durationMs;
			pendingOutputPcm = Buffer.concat([pendingOutputPcm, decoded]);
			if (!outputFingerprint) consumePendingOutput(true);
			else if (pendingOutputPcm.byteLength >= fullReferenceBytes) consumePendingOutput(false);
			outputObservationDeadlineMs = Math.max(outputObservationDeadlineMs, outputQueuedUntilMs + OUTPUT_LOOPBACK_OBSERVATION_WINDOW_MS);
		},
		getHealth() {
			return {
				lastOutputLoopbackAt,
				lastOutputLoopbackCorrelation,
				lastOutputLoopbackPeak,
				lastOutputLoopbackRms,
				outputLoopbackSignalBytes,
				outputGeneration,
				verifiedOutputGeneration
			};
		}
	};
}
//#endregion
//#region src/meeting-bot/realtime-local-audio-transport.ts
const LOCAL_BRIDGE_TERMINATION_GRACE_MS = 1e3;
const STDERR_LINE_TRUNCATED_PREFIX = "[stderr line truncated] ";
const MAX_STDERR_CHUNK_BYTES = 8192;
function attachStderrLineLogger(params) {
	if (!params.stderr) return;
	if (!params.logger.debug) {
		params.stderr.on("data", () => {});
		return;
	}
	const debug = (message) => params.logger.debug?.(message);
	if (!(params.stderr instanceof Readable)) {
		params.stderr.on("data", (chunk) => {
			debug(`${params.prefix}: ${String(chunk).trim()}`);
		});
		return;
	}
	onDecodedOutput(params.stderr, (chunk) => {
		const trimmed = chunk.trim();
		if (!trimmed) return;
		const truncated = Buffer.byteLength(trimmed, "utf8") > MAX_STDERR_CHUNK_BYTES;
		const value = truncated ? truncateUtf8Suffix(trimmed, MAX_STDERR_CHUNK_BYTES) : trimmed;
		debug(`${params.prefix}: ${truncated ? STDERR_LINE_TRUNCATED_PREFIX : ""}${value}`);
	});
}
function createLocalMeetingRealtimeAudioTransport(params) {
	const input = splitCommandArgv(params.inputCommand, "audio bridge command");
	const output = splitCommandArgv(params.outputCommand, "audio bridge command");
	const spawnFn = params.spawn ?? ((command, args, options) => spawn(command, args, options));
	const spawnOutputProcess = () => spawnFn(output.command, output.args, { stdio: [
		"pipe",
		"ignore",
		"pipe"
	] });
	let outputProcess = spawnOutputProcess();
	const inputProcess = spawnFn(input.command, input.args, { stdio: [
		"ignore",
		"pipe",
		"pipe"
	] });
	let bargeInInputProcess;
	let stopped = false;
	let inputStarted = false;
	let fatalSignaled = false;
	let fatalHandler;
	let stopPromise;
	const retiredOutputStops = /* @__PURE__ */ new Set();
	const outputWriteWaiters = /* @__PURE__ */ new Set();
	const outputLoopbackVerifier = createMeetingOutputLoopbackVerifier({ audioFormat: params.audioFormat ?? "pcm16-24khz" });
	const signalFatal = () => {
		if (!fatalSignaled) {
			fatalSignaled = true;
			fatalHandler?.();
		}
	};
	const fail = (label) => (error) => {
		params.logger.warn(`${params.logScope} ${label} failed: ${formatErrorMessage(error)}`);
		signalFatal();
	};
	const attachOutputProcessHandlers = (proc) => {
		proc.on("error", (error) => {
			if (proc === outputProcess) fail("audio output command")(error);
		});
		proc.stdin?.on?.("error", (error) => {
			if (proc === outputProcess) fail("audio output command")(error);
		});
		proc.on("exit", (code, signal) => {
			if (proc === outputProcess && !stopped) {
				params.logger.warn(`${params.logScope} audio output command exited (${code ?? signal ?? "done"})`);
				signalFatal();
			}
		});
		attachStderrLineLogger({
			stderr: proc.stderr,
			logger: params.logger,
			prefix: `${params.logScope} audio output`
		});
		proc.stderr?.on("error", (error) => {
			if (proc === outputProcess) fail("audio output command stderr")(error);
		});
	};
	const writeOutputChunk = (proc, stdin, audio) => new Promise((resolve, reject) => {
		let settled = false;
		const finish = (error) => {
			if (settled) return;
			settled = true;
			outputWriteWaiters.delete(waiter);
			if (error) reject(error);
			else resolve();
		};
		const waiter = {
			proc,
			release: () => finish()
		};
		outputWriteWaiters.add(waiter);
		try {
			stdin.write(audio, (error) => finish(error ?? void 0));
		} catch (error) {
			finish(error instanceof Error ? error : new Error(formatErrorMessage(error)));
			return;
		}
		if (stdin.destroyed || stdin.writableEnded) finish(/* @__PURE__ */ new Error("audio output stream is closed"));
	});
	const releaseOutputWriteWaiters = (proc) => {
		for (const waiter of outputWriteWaiters) if (!proc || waiter.proc === proc) waiter.release();
	};
	attachOutputProcessHandlers(outputProcess);
	inputProcess.on("error", fail("audio input command"));
	inputProcess.on("exit", (code, signal) => {
		if (!stopped) {
			params.logger.warn(`${params.logScope} audio input command exited (${code ?? signal ?? "done"})`);
			signalFatal();
		}
	});
	attachStderrLineLogger({
		stderr: inputProcess.stderr,
		logger: params.logger,
		prefix: `${params.logScope} audio input`
	});
	inputProcess.stdout?.on("error", fail("audio input command stdout"));
	inputProcess.stderr?.on("error", fail("audio input command stderr"));
	const transport = {
		onFatal: (handler) => {
			fatalHandler = handler;
			if (fatalSignaled) handler();
		},
		startInput: (onAudio) => {
			if (inputStarted) throw new Error("audio input transport already started");
			inputStarted = true;
			inputProcess.stdout?.on("data", (chunk) => {
				if (!stopped) {
					const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
					outputLoopbackVerifier.recordInput(audio);
					onAudio(audio);
				}
			});
		},
		beginOutput: () => outputLoopbackVerifier.beginOutput(),
		stop: () => {
			stopPromise ??= (async () => {
				stopped = true;
				releaseOutputWriteWaiters();
				await Promise.all([
					terminateMeetingBridgeProcess(inputProcess, { graceMs: LOCAL_BRIDGE_TERMINATION_GRACE_MS }),
					terminateMeetingBridgeProcess(outputProcess, { graceMs: LOCAL_BRIDGE_TERMINATION_GRACE_MS }),
					terminateMeetingBridgeProcess(bargeInInputProcess, { graceMs: LOCAL_BRIDGE_TERMINATION_GRACE_MS }),
					...retiredOutputStops
				]);
			})();
			return stopPromise;
		},
		writeOutput: async (audio) => {
			if (stopped) return;
			const proc = outputProcess;
			const stdin = proc.stdin;
			if (!stdin) return;
			outputLoopbackVerifier.recordOutput(audio);
			try {
				await writeOutputChunk(proc, stdin, audio);
			} catch (error) {
				if (stopped || proc !== outputProcess || fatalSignaled) return;
				fail("audio output command")(error instanceof Error ? error : new Error(formatErrorMessage(error)));
			}
		},
		clearOutput: async () => {
			if (stopped) return;
			outputLoopbackVerifier.cancelOutput();
			const previousOutput = outputProcess;
			outputProcess = spawnOutputProcess();
			attachOutputProcessHandlers(outputProcess);
			releaseOutputWriteWaiters(previousOutput);
			params.logger.debug?.(`${params.logScope} cleared realtime audio output buffer by restarting playback command`);
			const retiredOutputStop = terminateMeetingBridgeProcess(previousOutput, {
				graceMs: LOCAL_BRIDGE_TERMINATION_GRACE_MS,
				initialSignal: "SIGKILL"
			});
			retiredOutputStops.add(retiredOutputStop);
			retiredOutputStop.finally(() => {
				retiredOutputStops.delete(retiredOutputStop);
			});
		},
		dispose: async () => {
			await transport.stop();
		},
		getHealth: () => outputLoopbackVerifier.getHealth()
	};
	if (!params.bargeInInputCommand) return transport;
	return {
		...transport,
		startBargeInMonitor: (onBargeIn) => {
			if (bargeInInputProcess || stopped) return;
			const command = splitCommandArgv(params.bargeInInputCommand ?? [], "audio bridge command");
			const bargeInGate = createSpeechThresholdGate({
				rmsThreshold: params.bargeInRmsThreshold,
				peakThreshold: params.bargeInPeakThreshold,
				cooldownMs: params.bargeInCooldownMs
			});
			bargeInInputProcess = spawnFn(command.command, command.args, { stdio: [
				"ignore",
				"pipe",
				"pipe"
			] });
			bargeInInputProcess.stdout?.on("data", (chunk) => {
				const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
				if (stopped) return;
				const stats = readPcm16AudioStats(audio);
				if (!bargeInGate.accept(stats, {
					nowMs: Date.now(),
					onTrigger: () => onBargeIn(audio)
				})) return;
				params.logger.debug?.(`${params.logScope} human barge-in detected by local input (rms=${Math.round(stats.rms)}, peak=${stats.peak})`);
			});
			bargeInInputProcess.stdout?.on("error", (error) => {
				params.logger.warn(`${params.logScope} human barge-in input stdout failed: ${formatErrorMessage(error)}`);
			});
			attachStderrLineLogger({
				stderr: bargeInInputProcess.stderr,
				logger: params.logger,
				prefix: `${params.logScope} barge-in input`
			});
			bargeInInputProcess.stderr?.on("error", (error) => {
				params.logger.warn(`${params.logScope} human barge-in input stderr failed: ${formatErrorMessage(error)}`);
			});
			bargeInInputProcess.on("error", (error) => {
				params.logger.warn(`${params.logScope} human barge-in input failed: ${formatErrorMessage(error)}`);
			});
			bargeInInputProcess.on("exit", (code, signal) => {
				if (!stopped) params.logger.debug?.(`${params.logScope} human barge-in input exited (${code ?? signal ?? "done"})`);
			});
		}
	};
}
//#endregion
//#region src/meeting-bot/audio-base64.ts
function decodeMeetingAudioBase64(base64, action) {
	const canonicalBase64 = canonicalizeBase64(base64);
	if (!canonicalBase64) throw new Error(`${action} base64 must be a valid audio payload`);
	return Buffer.from(canonicalBase64, "base64");
}
function isMeetingAudioBase64(base64) {
	return canonicalizeBase64(base64) !== void 0;
}
//#endregion
//#region src/meeting-bot/realtime-node-audio-transport.ts
const NODE_OUTPUT_GENERATION_CAPABILITY = Symbol.for("openclaw.internal.meeting-node-output-generation.v1");
function createNodeMeetingRealtimeAudioTransport(params) {
	let stopped = false;
	let inputStarted = false;
	let consecutiveInputErrors = 0;
	let lastInputError;
	let fatalSignaled = false;
	let fatalHandler;
	let outputGeneration = 0;
	let outputGenerationSupported = false;
	let legacyOutputTail = Promise.resolve();
	const outputLoopbackVerifier = createMeetingOutputLoopbackVerifier({ audioFormat: params.audioFormat ?? "pcm16-24khz" });
	const runOutputCommand = (task) => {
		if (outputGenerationSupported) return task();
		const result = legacyOutputTail.then(task, task);
		legacyOutputTail = result.then(() => void 0, () => void 0);
		return result;
	};
	const signalFatal = () => {
		if (!fatalSignaled) {
			fatalSignaled = true;
			fatalHandler?.();
		}
	};
	const transport = {
		onFatal: (handler) => {
			fatalHandler = handler;
			if (fatalSignaled) handler();
		},
		startInput: (onAudio) => {
			if (inputStarted) throw new Error("audio input transport already started");
			inputStarted = true;
			(async () => {
				for (;;) {
					if (stopped) break;
					try {
						const raw = await params.runtime.nodes.invoke({
							nodeId: params.nodeId,
							command: params.commandName,
							params: {
								action: "pullAudio",
								bridgeId: params.bridgeId,
								timeoutMs: 250
							},
							timeoutMs: 2e3
						});
						const rawRecord = asOptionalRecord(raw);
						const result = asOptionalRecord(rawRecord?.payload ?? raw) ?? {};
						const base64 = readNonBlankString(result.base64);
						if (base64) {
							const audio = decodeMeetingAudioBase64(base64, "pullAudio");
							outputLoopbackVerifier.recordInput(audio);
							onAudio(audio);
						}
						consecutiveInputErrors = 0;
						lastInputError = void 0;
						if (result.closed === true) {
							signalFatal();
							break;
						}
					} catch (error) {
						if (stopped) break;
						const message = formatErrorMessage(error);
						consecutiveInputErrors += 1;
						lastInputError = message;
						params.logger.warn(`${params.logScope} ${params.logPrefix} audio input failed (${consecutiveInputErrors}/5): ${message}`);
						if (consecutiveInputErrors >= 5 || /unknown bridgeId|bridge is not open/i.test(message)) {
							signalFatal();
							break;
						}
						await new Promise((resolve) => {
							setTimeout(resolve, 250);
						});
					}
				}
			})();
		},
		beginOutput: () => outputLoopbackVerifier.beginOutput(),
		stop: async () => {
			if (stopped) return;
			stopped = true;
			try {
				await params.runtime.nodes.invoke({
					nodeId: params.nodeId,
					command: params.commandName,
					params: {
						action: "stop",
						bridgeId: params.bridgeId
					},
					timeoutMs: 5e3
				});
			} catch (error) {
				params.logger.debug?.(`${params.logScope} node audio bridge stop ignored: ${formatErrorMessage(error)}`);
			}
		},
		writeOutput: async (audio) => {
			if (stopped) return;
			const generation = outputGeneration;
			outputLoopbackVerifier.recordOutput(audio);
			try {
				await runOutputCommand(async () => {
					if (stopped) return;
					await params.runtime.nodes.invoke({
						nodeId: params.nodeId,
						command: params.commandName,
						params: {
							action: "pushAudio",
							bridgeId: params.bridgeId,
							base64: audio.toString("base64"),
							...outputGenerationSupported ? { outputGeneration: generation } : {}
						},
						timeoutMs: 5e3
					});
				});
			} catch (error) {
				if (!stopped && generation === outputGeneration) outputLoopbackVerifier.cancelOutput();
				throw error;
			}
		},
		clearOutput: async () => {
			if (stopped) return;
			outputGeneration += 1;
			outputLoopbackVerifier.cancelOutput();
			await runOutputCommand(async () => {
				if (stopped) return;
				await params.runtime.nodes.invoke({
					nodeId: params.nodeId,
					command: params.commandName,
					params: {
						action: "clearAudio",
						bridgeId: params.bridgeId,
						...outputGenerationSupported ? { outputGeneration } : {}
					},
					timeoutMs: 5e3
				});
			});
		},
		dispose: async () => {
			await transport.stop();
		},
		getHealth: () => ({
			consecutiveInputErrors,
			lastInputError,
			...outputLoopbackVerifier.getHealth()
		})
	};
	Object.defineProperty(transport, NODE_OUTPUT_GENERATION_CAPABILITY, {
		get() {
			return outputGenerationSupported;
		},
		set(value) {
			outputGenerationSupported = value === true;
		}
	});
	return transport;
}
//#endregion
//#region src/meeting-bot/session-cleanup-tracker.ts
var MeetingSessionCleanupTracker = class {
	#states = /* @__PURE__ */ new Map();
	begin(sessionId, browserLeft) {
		if (this.#states.has(sessionId)) return false;
		this.#states.set(sessionId, {
			browserLeft,
			browserSettled: false,
			stopSettled: false
		});
		return true;
	}
	isPending(sessionId) {
		return this.#states.has(sessionId);
	}
	async cleanup(params) {
		const state = this.#states.get(params.sessionId);
		if (!state) throw new Error("Missing cleanup state for meeting session " + params.sessionId);
		let cleanupError;
		if (!state.stopSettled) try {
			await params.stop?.();
			state.stopSettled = true;
		} catch (error) {
			cleanupError = error;
		}
		if (!state.browserSettled) try {
			if (params.keepBrowserTab) state.browserSettled = true;
			else {
				state.browserLeft = await params.releaseBrowser();
				state.browserSettled = state.browserLeft !== false;
			}
		} catch (error) {
			cleanupError ??= error;
		}
		const complete = this.#completeIfSettled(params.sessionId, state);
		if (cleanupError) throw cleanupError instanceof Error ? cleanupError : new Error("Meeting session cleanup failed", { cause: cleanupError });
		return {
			browserLeft: state.browserLeft,
			complete,
			stopSettled: state.stopSettled
		};
	}
	async retryBrowserAfterFailedJoin(params) {
		const state = this.#states.get(params.sessionId);
		if (!state) return {
			browserLeft: params.browserLeft,
			complete: true,
			incomplete: false
		};
		if (!params.hasBrowserTab()) state.browserSettled ||= state.browserLeft !== false;
		else if (!state.browserSettled) try {
			state.browserLeft = await params.releaseBrowser();
			state.browserSettled = state.browserLeft !== false;
		} catch (error) {
			return {
				browserLeft: state.browserLeft,
				complete: false,
				error,
				incomplete: params.hasBrowserTab()
			};
		}
		return {
			browserLeft: state.browserLeft,
			complete: this.#completeIfSettled(params.sessionId, state),
			incomplete: params.hasBrowserTab()
		};
	}
	async rollbackFailedJoin(params) {
		let retryFullCleanup = false;
		try {
			await params.leave();
		} catch (error) {
			params.warn(`replacement cleanup failed: ${params.formatError(error)}`);
			retryFullCleanup = true;
		}
		if (retryFullCleanup) try {
			await params.leave();
		} catch (error) {
			params.warn(`replacement cleanup retry failed: ${params.formatError(error)}`);
		}
		const retry = await this.retryBrowserAfterFailedJoin(params);
		params.onBrowserResult(retry.browserLeft);
		if (retry.error) params.warn(`replacement browser cleanup retry failed: ${params.formatError(retry.error)}`);
		if (retry.complete) params.onComplete();
		if (retry.incomplete) params.warn("replacement browser cleanup incomplete after failed join");
	}
	#completeIfSettled(sessionId, state) {
		if (!state.stopSettled || !state.browserSettled) return false;
		this.#states.delete(sessionId);
		return true;
	}
};
//#endregion
//#region src/meeting-bot/session-durable-transcripts.ts
const STOP_RETRY_DELAY_MS = 1e3;
const STOP_RETRY_MAX_DELAY_MS = 6e4;
var MeetingSessionDurableTranscripts = class {
	#bridge;
	#stopRetries = /* @__PURE__ */ new Map();
	constructor(options) {
		this.options = options;
	}
	async ingest(session, lines) {
		await (await this.#getBridge())?.ingest(session, lines);
	}
	async start(session) {
		if (!this.options.isBrowserSession(session)) return;
		await (await this.#getBridge())?.start(session, async () => await this.options.transcriptStore.captureNotes(session));
	}
	async stop(session, options) {
		const finalCapture = async () => await this.options.transcriptStore.captureNotes(session, { finalize: true });
		const bridge = await this.#getBridge();
		if (bridge?.enabled) try {
			if (await bridge.stop(session, finalCapture)) {
				this.#stopRetries.delete(session.id);
				return true;
			}
		} catch (error) {
			this.options.logger.warn(`${this.options.logScope} durable transcript finalization queued for retry: ${this.options.formatError(error)}`);
			this.#scheduleStopRetry(session, bridge);
			return false;
		}
		if (options.allowFallback && this.options.isTranscribeSession(session)) await finalCapture().catch((error) => {
			this.options.logger.debug?.(`${this.options.logScope} final transcript snapshot ignored: ${this.options.formatError(error)}`);
		});
		return true;
	}
	async startSource(request) {
		const bridge = await this.#getBridge();
		if (!bridge?.enabled) return {
			ok: false,
			error: "meeting transcripts are disabled"
		};
		const session = this.#findSourceSession(request.session.source);
		if (!session) return {
			ok: false,
			error: "No active meeting session matches the transcript source."
		};
		if (request.session.source.agentId !== session.agentId) return {
			ok: false,
			error: "meeting transcript source belongs to another agent"
		};
		return await bridge.attach(session, request);
	}
	async stopSource(request) {
		const bridge = await this.#getBridge();
		return bridge ? await bridge.detach(request) : {
			ok: true,
			sessionId: request.sessionId,
			stoppedAt: (/* @__PURE__ */ new Date()).toISOString()
		};
	}
	#findSourceSession(source) {
		const agentId = source.agentId?.trim();
		const meetingUrl = source.meetingUrl?.trim();
		const sessionId = source.channelId?.trim();
		if (!agentId || !meetingUrl && !sessionId) return;
		return this.options.listSessions().filter((session) => session.state === "active").toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)).find((session) => session.agentId === agentId && (!sessionId || session.id === sessionId) && (!meetingUrl || this.options.sameMeetingUrl(session.url, meetingUrl)));
	}
	async #getBridge() {
		if (!this.options.config) return;
		this.#bridge ??= import("../transcripts-bridge.runtime-C8_Q08et.js").then(({ createMeetingDurableTranscriptBridge }) => createMeetingDurableTranscriptBridge({
			logger: this.options.logger,
			options: this.options.config
		})).catch((error) => {
			this.options.logger.warn(`${this.options.logScope} durable transcripts unavailable: ${this.options.formatError(error)}`);
		});
		return await this.#bridge;
	}
	#scheduleStopRetry(session, bridge) {
		if (this.#stopRetries.has(session.id)) return;
		const token = Symbol(session.id);
		this.#stopRetries.set(session.id, {
			attempt: 1,
			token
		});
		const retry = () => {
			const attempt = this.#stopRetries.get(session.id)?.attempt ?? 1;
			const delayMs = Math.min(STOP_RETRY_DELAY_MS * 2 ** Math.min(attempt - 1, 16), STOP_RETRY_MAX_DELAY_MS);
			setTimeout(() => void run(), delayMs).unref?.();
		};
		const run = async () => {
			const state = this.#stopRetries.get(session.id);
			if (!state || state.token !== token) return;
			try {
				if (!await bridge.stop(session, async () => await this.options.transcriptStore.flushPending(session))) throw new Error("durable transcript capture is no longer active");
				if (this.#stopRetries.get(session.id)?.token !== token) return;
				this.options.transcriptStore.retire(session.id);
				this.#stopRetries.delete(session.id);
			} catch {
				if (this.#stopRetries.get(session.id)?.token !== token) return;
				this.#stopRetries.set(session.id, {
					attempt: state.attempt + 1,
					token
				});
				retry();
			}
		};
		retry();
	}
};
//#endregion
//#region src/meeting-bot/session-join-lock.ts
/** Serializes adoption and departure for one physical browser meeting. */
var MeetingSessionJoinLock = class {
	#tails = /* @__PURE__ */ new Map();
	async run(key, operation) {
		const previous = this.#tails.get(key) ?? Promise.resolve();
		let release;
		const gate = new Promise((resolve) => {
			release = resolve;
		});
		const tail = previous.then(() => gate);
		this.#tails.set(key, tail);
		await previous;
		try {
			return await operation();
		} finally {
			release?.();
			if (this.#tails.get(key) === tail) this.#tails.delete(key);
		}
	}
};
//#endregion
//#region src/meeting-bot/session-speech-readiness.ts
function evaluateMeetingSpeechReadiness(params) {
	const { browser, speech } = params;
	if (!params.talkBack || !browser) return { ready: true };
	if (!params.managedBrowser) return browser.hasAudioBridge ? { ready: true } : {
		ready: false,
		reason: speech.audioBridgeUnavailableReason,
		message: speech.audioBridgeUnavailable
	};
	const health = browser.health;
	if (health?.manualAction) return {
		ready: false,
		reason: health.manualAction.reason,
		message: health.manualAction.message
	};
	if (health?.inCall === true) {
		if (health.micMuted !== false) {
			const muted = health.micMuted === true;
			return {
				ready: false,
				reason: muted ? speech.microphoneMutedReason : speech.browserUnverifiedReason,
				message: muted ? speech.microphoneMuted : speech.browserUnverified
			};
		}
		return browser.hasAudioBridge ? { ready: true } : {
			ready: false,
			reason: speech.audioBridgeUnavailableReason,
			message: speech.audioBridgeUnavailable
		};
	}
	if (health?.inCall === false) return {
		ready: false,
		reason: speech.notInCallReason,
		message: speech.notInCall
	};
	return {
		ready: false,
		reason: speech.browserUnverifiedReason,
		message: speech.browserUnverified
	};
}
//#endregion
//#region src/meeting-bot/session-runtime.ts
const nowIso$1 = () => (/* @__PURE__ */ new Date()).toISOString();
/** Shared lifecycle owner; platform strategies perform transport-specific I/O only. */
var MeetingSessionRuntime = class {
	#sessions = /* @__PURE__ */ new Map();
	#sessionLeaves = /* @__PURE__ */ new Map();
	#sessionCleanup = new MeetingSessionCleanupTracker();
	#meetingLock = new MeetingSessionJoinLock();
	#sessionStops = /* @__PURE__ */ new Map();
	#sessionSpeakers = /* @__PURE__ */ new Map();
	#sessionHealth = /* @__PURE__ */ new Map();
	#durableTranscripts;
	#transcriptStore;
	constructor(options) {
		this.options = options;
		this.#transcriptStore = new MeetingSessionTranscriptStore({
			getSession: (sessionId) => this.#sessions.get(sessionId),
			isBrowserSession: (session) => this.options.isBrowserTransport(session.transport),
			isTranscribeSession: (session) => this.options.isTranscribeMode(session.mode),
			hasBrowserTab: (session) => Boolean(this.options.getBrowser(session)?.tab),
			capture: async (session, captureOptions) => await this.options.captureTranscript(session, captureOptions),
			onLines: async (session, lines) => await this.#durableTranscripts.ingest(session, lines)
		});
		this.#durableTranscripts = new MeetingSessionDurableTranscripts({
			config: options.durableTranscripts,
			formatError: (error) => options.formatError(error),
			isBrowserSession: (session) => options.isBrowserTransport(session.transport),
			isTranscribeSession: (session) => options.isTranscribeMode(session.mode),
			listSessions: () => [...this.#sessions.values()],
			logger: options.logger,
			logScope: options.logScope,
			sameMeetingUrl: (left, right) => options.sameMeetingUrl(left, right),
			transcriptStore: this.#transcriptStore
		});
	}
	list() {
		this.refreshHealth();
		return [...this.#sessions.values()].toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));
	}
	getSession(sessionId) {
		return this.#sessions.get(sessionId);
	}
	async status(sessionId) {
		this.refreshHealth(sessionId);
		if (!sessionId) {
			const sessions = [...this.#sessions.values()].toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));
			await Promise.all(sessions.map((session) => this.options.refreshStatus(session)));
			return {
				found: true,
				sessions
			};
		}
		const session = this.#sessions.get(sessionId);
		if (session) await this.options.refreshStatus(session);
		return session ? {
			found: true,
			session
		} : { found: false };
	}
	async transcript(sessionId, options = {}) {
		return await this.#transcriptStore.read(sessionId, options);
	}
	async startTranscriptSource(request) {
		return await this.#durableTranscripts.startSource(request);
	}
	async stopTranscriptSource(request) {
		return await this.#durableTranscripts.stopSource(request);
	}
	isReusableSession(session, resolved) {
		return session.state === "active" && this.options.sameMeetingUrl(session.url, resolved.url) && session.transport === resolved.transport && session.mode === resolved.mode && session.agentId === resolved.agentId;
	}
	async join(request) {
		const resolved = this.options.resolveJoin(request);
		return await this.#meetingLock.run(this.#meetingKey(resolved.transport, resolved.url), async () => await this.#joinUnlocked(request, resolved));
	}
	async leave(sessionId, options) {
		const session = this.#sessions.get(sessionId);
		if (!session) return { found: false };
		return await this.#meetingLock.run(this.#meetingKey(session.transport, session.url), async () => await this.#leaveUnlocked(sessionId, options));
	}
	async speak(sessionId, instructions) {
		const session = this.#sessions.get(sessionId);
		if (!session) return {
			found: false,
			spoken: false
		};
		if (session.state !== "active") return {
			found: true,
			spoken: false,
			session
		};
		const delegated = await this.options.speakViaTransport(session, instructions);
		if (session.state !== "active") return {
			found: true,
			spoken: false,
			session
		};
		if (delegated?.handled) return {
			found: true,
			spoken: delegated.spoken,
			session
		};
		await this.refreshBrowserHealth(session);
		if (session.state !== "active") return {
			found: true,
			spoken: false,
			session
		};
		const handles = await this.options.ensureRealtimeBridge(session);
		if (session.state !== "active") {
			await handles?.stop?.();
			return {
				found: true,
				spoken: false,
				session
			};
		}
		if (handles) this.#attachRuntimeHandles(session, handles);
		const speak = this.#sessionSpeakers.get(sessionId);
		if (!speak || session.state !== "active") return {
			found: true,
			spoken: false,
			session
		};
		const readiness = this.refreshSpeechReadiness(session);
		if (!readiness.ready) {
			const note = readiness.message ? `Realtime speech blocked: ${readiness.message}` : this.options.messages.speechBlockedFallback;
			this.#noteSession(session, note);
			session.updatedAt = nowIso$1();
			return {
				found: true,
				spoken: false,
				session
			};
		}
		speak(instructions || this.options.defaultSpeechInstructions);
		session.updatedAt = nowIso$1();
		this.refreshHealth(sessionId);
		return {
			found: true,
			spoken: true,
			session
		};
	}
	async speakWhenReady(session, instructions) {
		let result = await this.speak(session.id, instructions);
		if (result.spoken || !this.options.isBrowserTransport(session.transport)) return result.spoken;
		const waitMs = Math.min(Math.max(0, this.options.waitForInCallMs), Math.max(0, this.options.joinTimeoutMs));
		const deadline = Date.now() + waitMs;
		while (Date.now() < deadline) {
			await new Promise((resolve) => {
				setTimeout(resolve, Math.min(250, Math.max(0, deadline - Date.now())));
			});
			result = await this.speak(session.id, instructions);
			if (result.spoken) return true;
			const health = this.options.getBrowser(result.session)?.health;
			if (health?.manualAction || result.session?.state !== "active") return false;
			const blocked = health?.speechBlockedReason;
			if (blocked && !this.options.transientSpeechBlockedReasons.has(blocked)) return false;
		}
		return false;
	}
	hasHealthHandle(sessionId) {
		return this.#sessionHealth.has(sessionId);
	}
	refreshHealth(sessionId) {
		const ids = sessionId ? [sessionId] : [...this.#sessionHealth.keys()];
		for (const id of ids) {
			const session = this.#sessions.get(id);
			const getHealth = this.#sessionHealth.get(id);
			const browser = session ? this.options.getBrowser(session) : void 0;
			if (!session || !browser || !getHealth) continue;
			this.options.setBrowserHealth(session, {
				...browser.health,
				...getHealth()
			});
			this.refreshSpeechReadiness(session);
		}
	}
	async refreshBrowserHealth(session, options = {}) {
		if (!this.#isManagedBrowserSession(session)) {
			this.refreshSpeechReadiness(session);
			return;
		}
		if (!options.force && this.options.isTalkBackMode(session.mode) && this.#evaluateSpeechReadiness(session).ready) {
			this.refreshSpeechReadiness(session);
			return;
		}
		await this.options.refreshBrowserHealth(session, options);
		this.refreshSpeechReadiness(session);
	}
	async refreshCaptionHealth(session) {
		if (!this.options.isTranscribeMode(session.mode)) {
			this.refreshSpeechReadiness(session);
			return;
		}
		await this.refreshBrowserHealth(session);
	}
	refreshSpeechReadiness(session) {
		const readiness = this.#evaluateSpeechReadiness(session);
		if (readiness.ready) session.notes = session.notes.filter((note) => !note.startsWith("Realtime speech blocked:"));
		const browser = this.options.getBrowser(session);
		if (browser) this.options.setBrowserHealth(session, {
			...browser.health,
			speechReady: readiness.ready,
			speechBlockedReason: readiness.reason,
			speechBlockedMessage: readiness.message
		});
		return readiness;
	}
	markSessionEnded(session, reason) {
		session.state = "ended";
		session.updatedAt = nowIso$1();
		this.#dropRuntimeHandles(session.id);
		this.#noteSession(session, reason);
	}
	async #joinUnlocked(request, resolved) {
		const activeSessions = this.list().filter((session) => session.state === "active" && this.options.sameMeetingUrl(session.url, resolved.url) && session.transport === resolved.transport);
		const retained = [];
		if (this.options.isBrowserTransport(resolved.transport)) for (const session of activeSessions) {
			if (this.isReusableSession(session, resolved)) continue;
			const browser = this.options.getBrowser(session);
			const tab = this.options.reuseExistingBrowserTab ? browser?.tab : void 0;
			const keepBrowserParticipant = Boolean(tab) || browser?.launched === false;
			if (tab) retained.push({
				session,
				tab
			});
			try {
				if ((await this.#leaveUnlocked(session.id, keepBrowserParticipant ? { keepBrowserTab: true } : void 0)).browserLeft === false) throw new Error(this.options.messages.previousBrowserLeaveFailed);
			} catch (error) {
				await this.#settleRetainedBrowserTabsAfterFailure(retained);
				throw error;
			}
			this.#noteSession(session, this.options.messages.reassignedSessionNote);
		}
		let reusable = activeSessions.find((session) => this.isReusableSession(session, resolved));
		if (reusable) {
			const refreshResult = await this.options.refreshReusableSession(reusable, request, resolved);
			if (reusable.state !== "active") {
				await this.#leaveSession(reusable, { keepBrowserTab: refreshResult?.keepBrowserTab ?? true });
				reusable = void 0;
			}
		}
		const speechInstructions = this.options.resolveSpeechInstructions(request);
		if (reusable) {
			await this.#durableTranscripts.start(reusable);
			await this.refreshBrowserHealth(reusable);
			this.#noteSession(reusable, this.options.messages.reusedSessionNote);
			reusable.updatedAt = nowIso$1();
			const spoken = this.options.isTalkBackMode(resolved.mode) && speechInstructions ? await this.speakWhenReady(reusable, speechInstructions) : false;
			return {
				session: reusable,
				spoken
			};
		}
		const session = this.options.createSession({
			request,
			resolved,
			createdAt: nowIso$1()
		});
		let delegatedSpoken;
		try {
			delegatedSpoken = (await this.options.joinTransport({
				request,
				session,
				context: {
					attachRuntimeHandles: (target, handles) => this.#attachRuntimeHandles(target, handles),
					inheritedBrowserTab: (params) => this.#inheritBrowserTabOwnership(params)
				}
			})).delegatedSpoken === true;
			const browser = this.options.getBrowser(session);
			if (!await this.#settleRetainedBrowserTabs(retained, browser?.tab ? {
				transport: session.transport,
				nodeId: browser.nodeId,
				tab: browser.tab
			} : void 0)) throw new Error(this.options.messages.replacementBrowserLeaveFailed);
		} catch (error) {
			await this.#rollbackFailedJoinSession(session);
			await this.#settleRetainedBrowserTabsAfterFailure(retained);
			this.options.logger.warn(`${this.options.logScope} join failed: ${this.options.formatError(error)}`);
			throw error;
		}
		this.#sessions.set(session.id, session);
		await this.#durableTranscripts.start(session);
		return {
			session,
			spoken: delegatedSpoken ? true : this.options.isTalkBackMode(resolved.mode) && speechInstructions ? await this.speakWhenReady(session, speechInstructions) : false
		};
	}
	async #leaveUnlocked(sessionId, options) {
		const inFlight = this.#sessionLeaves.get(sessionId);
		if (inFlight) return await inFlight;
		const session = this.#sessions.get(sessionId);
		if (!session) return { found: false };
		if (session.state === "ended" && !this.#sessionCleanup.isPending(sessionId)) return {
			found: true,
			session,
			...session.browserLeft === void 0 ? {} : { browserLeft: session.browserLeft }
		};
		const leave = this.#leaveSession(session, options);
		this.#sessionLeaves.set(sessionId, leave);
		try {
			return await leave;
		} finally {
			if (this.#sessionLeaves.get(sessionId) === leave) this.#sessionLeaves.delete(sessionId);
		}
	}
	async #leaveSession(session, options) {
		const firstAttempt = this.#sessionCleanup.begin(session.id, session.browserLeft);
		const transcribe = this.options.isTranscribeMode(session.mode);
		let transcriptStopped = false;
		if (transcribe) this.#transcriptStore.startFinalizing(session.id);
		try {
			transcriptStopped = await this.#durableTranscripts.stop(session, { allowFallback: firstAttempt });
			session.state = "ended";
			session.updatedAt = nowIso$1();
			this.#sessionSpeakers.delete(session.id);
			this.#sessionHealth.delete(session.id);
			const stop = this.#sessionStops.get(session.id);
			const cleanup = await this.#sessionCleanup.cleanup({
				sessionId: session.id,
				stop,
				keepBrowserTab: options?.keepBrowserTab === true,
				releaseBrowser: async () => await this.options.releaseBrowserTab(session)
			});
			session.browserLeft = cleanup.browserLeft;
			const browser = this.options.getBrowser(session);
			if (cleanup.browserLeft === true && browser?.health) this.options.setBrowserHealth(session, {
				...browser.health,
				inCall: false,
				micMuted: void 0,
				manualAction: void 0,
				speechReady: false,
				speechBlockedReason: void 0,
				speechBlockedMessage: void 0
			});
			if (cleanup.stopSettled && stop && this.#sessionStops.get(session.id) === stop) this.#sessionStops.delete(session.id);
			if (cleanup.complete) this.#dropRuntimeHandles(session.id);
			return {
				found: true,
				session,
				...cleanup.browserLeft === void 0 ? {} : { browserLeft: cleanup.browserLeft }
			};
		} finally {
			if (transcriptStopped) this.#transcriptStore.retire(session.id);
			if (transcribe) this.#transcriptStore.finishFinalizing(session.id);
		}
	}
	#meetingKey(transport, url) {
		return `${transport}:${this.options.normalizeMeetingUrlForReuse(url) ?? url}`;
	}
	#inheritBrowserTabOwnership(params) {
		if (!params.tab) return;
		return [...this.#sessions.values()].some((session) => {
			const browser = this.options.getBrowser(session);
			const browserTab = browser?.tab;
			return session.transport === params.transport && this.options.sameMeetingUrl(session.url, params.meetingUrl) && browser?.nodeId === params.nodeId && browserTab?.targetId === params.tab?.targetId && browserTab?.openedByPlugin === true;
		}) ? {
			...params.tab,
			openedByPlugin: true
		} : params.tab;
	}
	async #settleRetainedBrowserTabs(retained, adopted) {
		let settled = true;
		for (let index = 0; index < retained.length;) {
			const retainedTab = retained[index];
			if (!retainedTab) break;
			const { session, tab } = retainedTab;
			const browser = this.options.getBrowser(session);
			if (adopted?.transport === session.transport && adopted.nodeId === browser?.nodeId && adopted.tab.targetId === tab.targetId) {
				this.options.setBrowserTab(session, void 0);
				retained.splice(index, 1);
				continue;
			}
			if (await this.options.releaseBrowserTab(session) === false) {
				settled = false;
				index += 1;
				continue;
			}
			retained.splice(index, 1);
		}
		return settled;
	}
	async #rollbackFailedJoinSession(session) {
		await this.#sessionCleanup.rollbackFailedJoin({
			sessionId: session.id,
			browserLeft: session.browserLeft,
			leave: async () => await this.#leaveSession(session),
			hasBrowserTab: () => Boolean(this.options.getBrowser(session)?.tab),
			releaseBrowser: async () => await this.options.releaseBrowserTab(session),
			formatError: (error) => this.options.formatError(error),
			warn: (message) => this.options.logger.warn(`${this.options.logScope} ${message}`),
			onBrowserResult: (left) => session.browserLeft = left,
			onComplete: () => this.#dropRuntimeHandles(session.id)
		});
	}
	async #settleRetainedBrowserTabsAfterFailure(retained) {
		for (let attempt = 0; attempt < 2 && retained.length > 0; attempt += 1) try {
			if (await this.#settleRetainedBrowserTabs(retained)) return;
		} catch (error) {
			this.options.logger.warn(`${this.options.logScope} retained browser cleanup failed: ${this.options.formatError(error)}`);
		}
		if (retained.length > 0) this.options.logger.warn(`${this.options.logScope} retained browser cleanup incomplete after failed join`);
	}
	#attachRuntimeHandles(session, handles) {
		if (handles.stop) this.#sessionStops.set(session.id, handles.stop);
		if (handles.speak) this.#sessionSpeakers.set(session.id, handles.speak);
		if (handles.getHealth) this.#sessionHealth.set(session.id, handles.getHealth);
	}
	#dropRuntimeHandles(sessionId) {
		this.#sessionStops.delete(sessionId);
		this.#sessionSpeakers.delete(sessionId);
		this.#sessionHealth.delete(sessionId);
	}
	#isManagedBrowserSession(session) {
		const browser = this.options.getBrowser(session);
		return Boolean(this.options.isBrowserTransport(session.transport) && browser?.launched);
	}
	#evaluateSpeechReadiness(session) {
		return evaluateMeetingSpeechReadiness({
			browser: this.options.getBrowser(session),
			managedBrowser: this.#isManagedBrowserSession(session),
			speech: this.options.messages.speech,
			talkBack: this.options.isTalkBackMode(session.mode)
		});
	}
	#noteSession(session, note) {
		session.notes = [...session.notes.filter((item) => item !== note), note];
	}
};
//#endregion
//#region src/meeting-bot/session-factory.ts
function createMeetingSession(params) {
	const { config, createdAt, platform, resolved } = params;
	return {
		id: `${platform.session.idPrefix}_${randomUUID()}`,
		...resolved,
		state: "active",
		createdAt,
		updatedAt: createdAt,
		participantIdentity: platform.session.participantIdentity(resolved.transport),
		realtime: {
			enabled: resolved.mode === "agent" || resolved.mode === "bidi",
			strategy: resolved.mode === "bidi" ? "bidi" : "agent",
			provider: resolved.mode === "bidi" ? config.realtime.voiceProvider ?? config.realtime.provider : void 0,
			model: resolved.mode === "bidi" ? config.realtime.model : void 0,
			transcriptionProvider: resolved.mode === "agent" ? config.realtime.transcriptionProvider ?? config.realtime.provider : void 0,
			toolPolicy: config.realtime.toolPolicy
		},
		notes: []
	};
}
//#endregion
//#region src/meeting-bot/browser-act-lock.ts
const browserActLock = new MeetingSessionJoinLock();
const BROWSER_ACT_TIMEOUT_MESSAGE = "Meeting browser operation timed out waiting for browser tab control.";
async function runMeetingBrowserAct(params) {
	const waitMs = Math.floor(params.deadline - Date.now());
	if (waitMs <= 0) throw new Error(BROWSER_ACT_TIMEOUT_MESSAGE);
	let acquired = false;
	let markAcquired;
	const acquisition = new Promise((resolve) => {
		markAcquired = resolve;
	});
	let timeout;
	const queued = browserActLock.run(params.targetId, async () => {
		const remainingMs = Math.floor(params.deadline - Date.now());
		if (remainingMs <= 0) throw new Error(BROWSER_ACT_TIMEOUT_MESSAGE);
		acquired = true;
		clearTimeout(timeout);
		markAcquired?.();
		return await params.operation(remainingMs);
	});
	queued.catch(() => void 0);
	const expired = new Promise((_resolve, reject) => {
		timeout = setTimeout(() => {
			if (!acquired) reject(/* @__PURE__ */ new Error(BROWSER_ACT_TIMEOUT_MESSAGE));
		}, waitMs);
	});
	try {
		await Promise.race([acquisition, expired]);
	} finally {
		clearTimeout(timeout);
	}
	return await queued;
}
//#endregion
//#region src/meeting-bot/browser-navigation-errors.ts
function isMeetingBrowserTransientNavigationError(error) {
	const message = error instanceof Error ? error.message : String(error);
	return /execution context was destroyed.*navigation|cannot find context with specified id/i.test(message);
}
//#endregion
//#region src/meeting-bot/browser-request.ts
function asMeetingBrowserTabs(result) {
	const record = result && typeof result === "object" ? result : {};
	return Array.isArray(record.tabs) ? record.tabs : [];
}
function readMeetingBrowserTab(result) {
	return result && typeof result === "object" ? result : void 0;
}
function resolveBrowserGatewayTimeoutMs(timeoutMs) {
	return addTimerTimeoutGraceMs(timeoutMs) ?? 1;
}
async function callLocalBrowserRequest(params) {
	return await callGatewayFromCli("browser.request", {
		json: true,
		timeout: String(resolveBrowserGatewayTimeoutMs(params.timeoutMs))
	}, {
		method: params.method,
		path: params.path,
		body: params.body,
		timeoutMs: params.timeoutMs
	}, { progress: false });
}
async function resolveLocalMeetingBrowserRequest(runtime) {
	if (!await runtime.gateway.isAvailable()) return callLocalBrowserRequest;
	return async (params) => await runtime.gateway.request("browser.request", {
		method: params.method,
		path: params.path,
		body: params.body,
		timeoutMs: params.timeoutMs
	}, {
		timeoutMs: resolveBrowserGatewayTimeoutMs(params.timeoutMs),
		scopes: ["operator.admin"]
	});
}
//#endregion
//#region src/meeting-bot/browser-controller.ts
function mergeBrowserNotes(browser, notes) {
	if (!browser || notes.length === 0) return browser;
	return {
		...browser,
		notes: uniqueStrings([...browser.notes ?? [], ...notes])
	};
}
function applyMeetingManualAction(browser, manual) {
	return browser && manual ? {
		...browser,
		manualAction: {
			reason: manual.reason,
			message: manual.message
		}
	} : browser;
}
async function prepareMeetingBrowserTab(params) {
	const plan = params.adapter.browser.permissions({
		allowMicrophone: params.allowMicrophone,
		meetingUrl: params.meetingUrl
	});
	if (!plan) return params.adapter.browser.permissionNotes({ allowMicrophone: params.allowMicrophone });
	try {
		const result = await params.callBrowser({
			method: "POST",
			path: "/permissions/grant",
			body: {
				origin: plan.origin,
				permissions: plan.permissions,
				optionalPermissions: plan.optionalPermissions,
				targetId: params.targetId,
				timeoutMs: Math.min(params.timeoutMs, 5e3)
			},
			timeoutMs: Math.min(params.timeoutMs, 5e3)
		});
		return params.adapter.browser.permissionNotes({
			allowMicrophone: params.allowMicrophone,
			result
		});
	} catch (error) {
		return params.adapter.browser.permissionNotes({
			allowMicrophone: params.allowMicrophone,
			error
		});
	}
}
function selectReusableTab(params) {
	const matches = params.tabs.filter((tab) => params.adapter.urls.isRecoverableTab(tab, params.url));
	const accountHint = params.adapter.urls.accountHint(params.url);
	return {
		matches,
		tab: matches.find((candidate) => params.adapter.urls.isPreferredJoinUrl(candidate.url) && (!accountHint || params.adapter.urls.accountHint(candidate.url) === accountHint))
	};
}
async function openMeetingWithBrowser(params) {
	if (!params.config.launch) return { launched: false };
	const timeoutMs = Math.max(1e3, params.config.joinTimeoutMs);
	let targetId;
	let tab;
	let openSession = params.session;
	let openedByPlugin = false;
	if (params.config.reuseExistingTab) {
		const tabs = asMeetingBrowserTabs(await params.callBrowser({
			method: "GET",
			path: "/tabs",
			timeoutMs: Math.min(timeoutMs, 5e3)
		}));
		const reusable = selectReusableTab({
			adapter: params.adapter,
			tabs,
			url: params.session.url
		});
		tab = reusable.tab;
		if (!tab && !params.adapter.urls.accountHint(params.session.url)) {
			const fallbackUrl = reusable.matches.find((candidate) => candidate.url)?.url;
			if (fallbackUrl) openSession = {
				...params.session,
				url: fallbackUrl
			};
		}
		targetId = tab?.targetId;
		if (tab && targetId) await params.callBrowser({
			method: "POST",
			path: "/tabs/focus",
			body: { targetId },
			timeoutMs: Math.min(timeoutMs, 5e3)
		});
	}
	if (!targetId) {
		tab = readMeetingBrowserTab(await params.callBrowser({
			method: "POST",
			path: "/tabs/open",
			body: { url: params.adapter.urls.buildJoinUrl(openSession) },
			timeoutMs
		}));
		targetId = tab?.targetId;
		openedByPlugin = Boolean(targetId);
	}
	if (!targetId) return {
		launched: true,
		browser: {
			status: "browser-control",
			notes: [`Browser proxy opened ${params.adapter.browserLabel} but did not return a targetId.`],
			browserUrl: tab?.url,
			browserTitle: tab?.title
		}
	};
	const tabIdentity = {
		targetId,
		openedByPlugin
	};
	const allowMicrophone = params.adapter.browser.allowsMicrophone(params.session.mode);
	const permissionNotes = await prepareMeetingBrowserTab({
		adapter: params.adapter,
		allowMicrophone,
		callBrowser: params.callBrowser,
		meetingUrl: params.session.url,
		targetId,
		timeoutMs
	});
	const deadline = Date.now() + Math.max(0, params.config.waitForInCallMs);
	let browser = {
		status: "browser-control",
		browserUrl: tab?.url,
		browserTitle: tab?.title,
		notes: permissionNotes
	};
	let allowSessionAdoption = true;
	do {
		try {
			const adoptSession = allowSessionAdoption;
			allowSessionAdoption = false;
			const actionTimeoutMs = Math.min(timeoutMs, 1e4);
			const evaluated = await runMeetingBrowserAct({
				deadline: Date.now() + actionTimeoutMs,
				targetId,
				operation: async (remainingMs) => await params.callBrowser({
					method: "POST",
					path: "/act",
					body: {
						kind: "evaluate",
						targetId,
						fn: params.adapter.browser.buildStatusJoinScript({
							...params.session,
							allowSessionAdoption: adoptSession,
							autoJoin: params.config.autoJoin,
							captureCaptions: params.session.captureCaptions ?? params.adapter.browser.captions.enabled(params.session.mode),
							guestName: params.config.guestName,
							waitForInCallMs: params.config.waitForInCallMs
						})
					},
					timeoutMs: remainingMs
				})
			});
			browser = mergeBrowserNotes(params.adapter.browser.parseStatus(evaluated) ?? browser, permissionNotes);
			const manual = browser ? params.adapter.browser.classifyManualAction(browser) : void 0;
			browser = applyMeetingManualAction(browser, manual);
			const shouldRetry = browser ? params.adapter.browser.shouldRetryJoinStatus?.(browser) === true : false;
			if (!shouldRetry && browser?.inCall === true && browser.manualAction === void 0 && (!allowMicrophone || browser.micMuted !== true)) return {
				launched: true,
				browser,
				tab: tabIdentity
			};
			if (!shouldRetry && browser?.manualAction) return {
				launched: true,
				browser,
				tab: tabIdentity
			};
		} catch (error) {
			if (isMeetingBrowserTransientNavigationError(error) && Date.now() < deadline) browser = mergeBrowserNotes(browser, [`${params.adapter.browserLabel} navigated while joining; retrying browser inspection.`]);
			else {
				const manual = params.adapter.browser.browserControlUnavailable(error);
				browser = {
					...browser,
					inCall: false,
					manualAction: {
						reason: manual.reason,
						message: manual.message
					},
					notes: [...permissionNotes, `Browser control could not inspect or auto-join ${params.adapter.browserLabel}: ${error instanceof Error ? error.message : String(error)}`]
				};
				break;
			}
		}
		const remainingWaitMs = deadline - Date.now();
		if (remainingWaitMs > 0) await new Promise((resolve) => {
			setTimeout(resolve, Math.min(750, remainingWaitMs));
		});
	} while (Date.now() < deadline);
	return {
		launched: true,
		browser,
		tab: tabIdentity
	};
}
function findRecoverableTab(params) {
	const candidates = params.tabs.filter((tab) => params.adapter.urls.isRecoverableTab(tab, params.requestedMeetingUrl));
	if (!params.requestedMeetingUrl) {
		const meetingCandidates = candidates.filter((tab) => params.adapter.urls.normalizeForReuse(tab.url));
		return meetingCandidates.find((tab) => params.adapter.urls.isPreferredJoinUrl(tab.url)) ?? meetingCandidates[0] ?? candidates[0];
	}
	const accountHint = params.adapter.urls.accountHint(params.requestedMeetingUrl);
	const accountCandidates = accountHint ? candidates.filter((tab) => params.adapter.urls.accountHint(tab.url) === accountHint) : candidates;
	return accountCandidates.find((tab) => params.adapter.urls.isPreferredJoinUrl(tab.url)) ?? accountCandidates[0];
}
async function inspectRecoverableTab(params) {
	const allowMicrophone = params.adapter.browser.allowsMicrophone(params.mode);
	const focusTimeoutMs = params.deadline === void 0 ? params.timeoutMs : Math.floor(params.deadline - Date.now());
	if (focusTimeoutMs <= 0) throw new Error("Meeting browser recovery timed out.");
	await params.callBrowser({
		method: "POST",
		path: "/tabs/focus",
		body: { targetId: params.targetId },
		timeoutMs: Math.min(focusTimeoutMs, 5e3)
	});
	const localeAction = params.adapter.urls.localeAction(params.tab);
	if (localeAction) return {
		found: true,
		targetId: params.targetId,
		tab: params.tab,
		browser: {
			status: "browser-control",
			browserUrl: params.tab.url,
			browserTitle: params.tab.title,
			manualAction: {
				reason: localeAction.reason,
				message: localeAction.message
			}
		},
		message: localeAction.message
	};
	const permissionNotes = params.readOnly ? [] : await prepareMeetingBrowserTab({
		adapter: params.adapter,
		allowMicrophone,
		callBrowser: params.callBrowser,
		meetingUrl: params.requestedMeetingUrl ?? params.tab.url ?? "",
		targetId: params.targetId,
		timeoutMs: params.deadline === void 0 ? params.timeoutMs : Math.max(1, Math.floor(params.deadline - Date.now()))
	});
	const navigationNotes = [];
	const inspectionDeadline = params.deadline ?? Date.now() + Math.min(params.timeoutMs, 1e4);
	let allowSessionAdoption = params.allowSessionAdoption ?? false;
	let evaluated;
	for (;;) try {
		const adoptSession = allowSessionAdoption;
		allowSessionAdoption = false;
		evaluated = await runMeetingBrowserAct({
			deadline: inspectionDeadline,
			targetId: params.targetId,
			operation: async (remainingMs) => await params.callBrowser({
				method: "POST",
				path: "/act",
				body: {
					kind: "evaluate",
					targetId: params.targetId,
					fn: params.adapter.browser.buildStatusJoinScript({
						allowSessionAdoption: adoptSession,
						meetingSessionId: params.meetingSessionId ?? "",
						mode: params.mode,
						url: params.requestedMeetingUrl ?? params.tab.url ?? "",
						autoJoin: params.autoJoin ?? false,
						captureCaptions: params.captureCaptions ?? params.adapter.browser.captions.enabled(params.mode),
						guestName: params.config.guestName,
						readOnly: params.readOnly,
						waitForInCallMs: params.config.waitForInCallMs
					})
				},
				timeoutMs: remainingMs
			})
		});
		break;
	} catch (error) {
		const remainingMs = inspectionDeadline - Date.now();
		if (!isMeetingBrowserTransientNavigationError(error) || remainingMs <= 0) throw error;
		navigationNotes.push(`${params.adapter.browserLabel} navigated while recovering; retrying browser inspection.`);
		await new Promise((resolve) => {
			setTimeout(resolve, Math.min(250, remainingMs));
		});
		if (Date.now() >= inspectionDeadline) throw error;
	}
	const browser = mergeBrowserNotes(params.adapter.browser.parseStatus(evaluated) ?? {
		status: "browser-control",
		browserUrl: params.tab.url,
		browserTitle: params.tab.title
	}, [...permissionNotes, ...navigationNotes]);
	const manual = browser ? params.adapter.browser.classifyManualAction(browser) : void 0;
	const recoveredBrowser = applyMeetingManualAction(browser, manual);
	const message = manual?.message ?? (recoveredBrowser?.inCall ? `Existing ${params.adapter.browserLabel} tab is in-call.` : `Existing ${params.adapter.browserLabel} tab focused.`);
	return {
		found: true,
		targetId: params.targetId,
		tab: params.tab,
		browser: recoveredBrowser,
		message
	};
}
async function recoverMeetingBrowserTab(params) {
	const configuredTimeoutMs = Math.max(1e3, params.config.joinTimeoutMs);
	const timeoutMs = params.timeoutMs === void 0 ? configuredTimeoutMs : Math.max(1, Math.min(configuredTimeoutMs, params.timeoutMs));
	const deadline = params.timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
	const tabs = asMeetingBrowserTabs(await params.callBrowser({
		method: "GET",
		path: "/tabs",
		timeoutMs: deadline === void 0 ? Math.min(timeoutMs, 5e3) : Math.min(Math.max(1, Math.floor(deadline - Date.now())), 5e3)
	}));
	const trackedCandidate = params.trackedTargetId ? tabs.find((tab) => tab.targetId === params.trackedTargetId) : void 0;
	const trackedUrlHasMeetingIdentity = Boolean(params.adapter.urls.normalizeForReuse(trackedCandidate?.url));
	const trackedIdentityMatches = params.adapter.urls.isSameMeeting(params.trackedMeetingUrl, params.requestedMeetingUrl);
	const trackedUrlMatches = params.adapter.urls.isSameMeeting(trackedCandidate?.url, params.requestedMeetingUrl);
	const tab = (trackedCandidate && trackedIdentityMatches && (!trackedUrlHasMeetingIdentity || trackedUrlMatches) ? trackedCandidate : void 0) ?? findRecoverableTab({
		adapter: params.adapter,
		tabs,
		requestedMeetingUrl: params.requestedMeetingUrl
	});
	const targetId = tab?.targetId;
	if (!tab || !targetId) return {
		found: false,
		tab,
		message: params.requestedMeetingUrl ? `No existing ${params.adapter.browserLabel} tab matched ${params.requestedMeetingUrl}.` : `No existing ${params.adapter.browserLabel} tab found ${params.locationLabel}.`
	};
	return await inspectRecoverableTab({
		adapter: params.adapter,
		allowSessionAdoption: params.allowSessionAdoption,
		autoJoin: params.autoJoin,
		callBrowser: params.callBrowser,
		captureCaptions: params.captureCaptions,
		config: params.config,
		...deadline === void 0 ? {} : { deadline },
		meetingSessionId: params.meetingSessionId,
		mode: params.mode,
		readOnly: params.readOnly,
		requestedMeetingUrl: params.requestedMeetingUrl,
		timeoutMs,
		tab,
		targetId
	});
}
//#endregion
//#region src/meeting-bot/browser-session-control.ts
async function leaveMeetingInPage(params) {
	const deadline = Date.now() + params.timeoutMs;
	let clickedLeave = false;
	let clickedConfirmation = false;
	let ownershipRetained = false;
	do {
		const remainingMs = Math.floor(deadline - Date.now());
		if (remainingMs <= 0) throw new Error("Meeting browser leave timed out.");
		const evaluated = await params.callBrowser({
			method: "POST",
			path: "/act",
			body: {
				kind: "evaluate",
				targetId: params.targetId,
				fn: params.adapter.browser.buildSessionLeaveScript?.({
					leaveInitiated: clickedLeave,
					meetingSessionId: params.meetingSessionId ?? "",
					meetingUrl: params.meetingUrl
				}) ?? params.adapter.browser.buildLeaveScript(params.meetingUrl)
			},
			timeoutMs: remainingMs
		});
		const step = params.adapter.browser.parseLeaveResult(evaluated);
		clickedLeave ||= step.leaveAction === "leave";
		clickedConfirmation ||= step.leaveAction === "confirm";
		if (step.sessionMatched === false) {
			const stepOwnershipRetained = clickedLeave && step.sessionConflict !== true;
			if (step.departed || !stepOwnershipRetained) return {
				departed: stepOwnershipRetained ? step.departed : false,
				clickedLeave,
				clickedConfirmation,
				ownershipRetained: stepOwnershipRetained,
				sessionConflict: step.sessionConflict,
				sessionMatched: false,
				urlMatched: step.urlMatched
			};
			ownershipRetained = true;
		}
		if (step.departed || step.urlMatched !== true) return {
			departed: step.departed,
			clickedLeave,
			clickedConfirmation,
			...ownershipRetained && step.sessionConflict !== true ? { ownershipRetained: true } : {},
			urlMatched: step.urlMatched
		};
		if (!step.leaveAction && !clickedLeave) return {
			departed: false,
			clickedLeave,
			clickedConfirmation,
			urlMatched: true
		};
		if (!step.leaveAction) await new Promise((resolve) => {
			setTimeout(resolve, 100);
		});
	} while (Date.now() < deadline);
	return {
		departed: false,
		clickedLeave,
		clickedConfirmation,
		...ownershipRetained ? {
			ownershipRetained: true,
			sessionMatched: false
		} : {},
		urlMatched: true
	};
}
async function leaveMeetingWithBrowser(params) {
	if (!params.launch) return {
		left: false,
		note: "Browser leave skipped because chrome.launch is disabled."
	};
	const timeoutMs = Math.min(Math.max(1e3, params.timeoutMs), 5e3);
	const { targetId, openedByPlugin } = params.tab;
	try {
		if (!asMeetingBrowserTabs(await params.callBrowser({
			method: "GET",
			path: "/tabs",
			timeoutMs
		})).find((entry) => entry.targetId === targetId)) return {
			left: true,
			note: `${params.adapter.browserLabel} tab is already closed.`
		};
		let leaveResult;
		let tabClosed = false;
		try {
			const locked = await runMeetingBrowserAct({
				deadline: Date.now() + timeoutMs,
				targetId,
				operation: async (remainingMs) => {
					const operationDeadline = Date.now() + remainingMs;
					const closeReserveMs = openedByPlugin ? Math.min(1e3, Math.max(250, Math.floor(remainingMs / 4))) : 0;
					const result = await leaveMeetingInPage({
						adapter: params.adapter,
						callBrowser: params.callBrowser,
						meetingSessionId: params.meetingSessionId,
						meetingUrl: params.meetingUrl,
						targetId,
						timeoutMs: Math.max(1, remainingMs - closeReserveMs)
					});
					if (!(openedByPlugin && (result.urlMatched === true || result.departed) && (result.sessionMatched !== false || result.ownershipRetained === true))) return {
						leaveResult: result,
						tabClosed: false
					};
					const closeTimeoutMs = Math.floor(operationDeadline - Date.now());
					if (closeTimeoutMs <= 0) throw new Error("Meeting browser leave timed out before the tab could close.");
					await params.callBrowser({
						method: "DELETE",
						path: `/tabs/${targetId}`,
						timeoutMs: closeTimeoutMs
					});
					return {
						leaveResult: result,
						tabClosed: true
					};
				}
			});
			leaveResult = locked.leaveResult;
			tabClosed = locked.tabClosed;
		} catch (error) {
			return {
				left: false,
				note: `Browser control could not verify the ${params.adapter.browserLabel} tab before leaving: ${error instanceof Error ? error.message : String(error)}`
			};
		}
		if (leaveResult.urlMatched === false) return {
			left: true,
			note: `${params.adapter.browserLabel} tab moved away from this session; left its current page untouched.`
		};
		if (leaveResult.sessionMatched === false && leaveResult.ownershipRetained !== true) {
			if (leaveResult.sessionConflict !== true) return {
				left: false,
				note: `Browser control could not verify that the ${params.adapter.browserLabel} tab still belongs to this OpenClaw meeting session.`
			};
			return {
				left: true,
				note: `${params.adapter.browserLabel} tab belongs to another OpenClaw meeting session; left its current call untouched.`
			};
		}
		if (leaveResult.urlMatched !== true && !leaveResult.departed) return {
			left: false,
			note: "Browser control could not verify that the tracked tab still showed this meeting."
		};
		const { clickedLeave, departed } = leaveResult;
		return {
			left: openedByPlugin ? tabClosed : departed,
			note: openedByPlugin ? clickedLeave ? `Clicked ${params.adapter.browserLabel}'s Leave call button and closed the ${params.adapter.browserLabel} tab.` : `Closed the ${params.adapter.browserLabel} tab to leave the meeting (Leave call button was not found).` : departed ? `Clicked ${params.adapter.browserLabel}'s Leave call button; kept the reused browser tab open.` : clickedLeave ? `Clicked ${params.adapter.browserLabel}'s Leave call button, but could not verify departure; leave it manually.` : `Could not find ${params.adapter.browserLabel}'s Leave call button in the reused browser tab; leave it manually.`
		};
	} catch (error) {
		return {
			left: false,
			note: `Browser control could not leave the ${params.adapter.browserLabel} tab: ${error instanceof Error ? error.message : String(error)}`
		};
	}
}
async function readMeetingTranscriptWithBrowser(params) {
	const result = await runMeetingBrowserAct({
		deadline: Date.now() + Math.max(1, params.timeoutMs),
		targetId: params.tab.targetId,
		operation: async (remainingMs) => await params.callBrowser({
			method: "POST",
			path: "/act",
			body: {
				kind: "evaluate",
				targetId: params.tab.targetId,
				fn: params.adapter.browser.captions.buildTranscriptScript({
					finalize: params.finalize,
					meetingSessionId: params.meetingSessionId,
					meetingUrl: params.meetingUrl
				})
			},
			timeoutMs: remainingMs
		})
	});
	const snapshot = params.adapter.browser.captions.parseTranscript(result);
	if (snapshot.urlMatched === false) throw new Error(`The tracked ${params.adapter.browserLabel} tab no longer shows this session's meeting URL.`);
	if (snapshot.sessionMatched === false) throw new Error(`The tracked ${params.adapter.browserLabel} tab now belongs to another OpenClaw meeting session.`);
	return {
		droppedLines: snapshot.droppedLines,
		...snapshot.epoch ? { epoch: snapshot.epoch } : {},
		lines: snapshot.lines
	};
}
//#endregion
//#region src/meeting-bot/browser-node.ts
function isMeetingBrowserNode(node, adapter) {
	const commands = Array.isArray(node.commands) ? node.commands : [];
	const caps = Array.isArray(node.caps) ? node.caps : [];
	return node.connected === true && commands.includes(adapter.nodeCommandName) && (commands.includes("browser.proxy") || caps.includes("browser"));
}
function matchesRequestedNode(node, requested) {
	return [
		node.nodeId,
		node.displayName,
		node.remoteIp
	].some((value) => value === requested);
}
function formatNodeLabel(node) {
	const parts = [
		node.displayName,
		node.nodeId,
		node.remoteIp
	].filter(Boolean);
	return parts.length > 0 ? parts.join(" / ") : "unknown node";
}
function describeNodeUsabilityIssues(node, adapter) {
	const commands = Array.isArray(node.commands) ? node.commands : [];
	const caps = Array.isArray(node.caps) ? node.caps : [];
	const issues = [];
	if (node.connected !== true) issues.push("offline");
	if (!commands.includes(adapter.nodeCommandName)) issues.push(`missing ${adapter.nodeCommandName}`);
	if (!commands.includes("browser.proxy") && !caps.includes("browser")) issues.push("missing browser.proxy/browser capability");
	return issues;
}
async function listMeetingNodes(runtime, adapter, params) {
	try {
		return params ? await runtime.nodes.list(params) : await runtime.nodes.list();
	} catch (error) {
		throw new Error(`${adapter.displayName} node inventory unavailable`, { cause: error });
	}
}
async function resolveMeetingBrowserNodeInfo(params) {
	const requested = params.requestedNode?.trim();
	if (requested) {
		const matches = (await listMeetingNodes(params.runtime, params.adapter)).nodes.filter((node) => matchesRequestedNode(node, requested));
		if (matches.length > 1) throw new Error(`Configured ${params.adapter.displayName} node ${requested} is ambiguous (${matches.length} matches). Pin ${params.adapter.nodeConfigPath} to a unique node id, display name, or remote IP.`);
		const [node] = matches;
		if (!node) throw new Error(`Configured ${params.adapter.displayName} node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`);
		if (isMeetingBrowserNode(node, params.adapter)) return node;
		throw new Error(`Configured ${params.adapter.displayName} node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node, params.adapter).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow ${params.adapter.nodeCommandName} plus browser.proxy.`);
	}
	const nodes = (await listMeetingNodes(params.runtime, params.adapter, { connected: true })).nodes.filter((node) => isMeetingBrowserNode(node, params.adapter));
	const [node] = nodes;
	if (!node) throw new Error(`No connected ${params.adapter.displayName}-capable node with browser proxy. Run \`openclaw node run\` on the Chrome host with browser proxy enabled, approve pairing, and allow ${params.adapter.nodeCommandName} plus browser.proxy.`);
	if (nodes.length === 1) return node;
	throw new Error(`Multiple ${params.adapter.displayName}-capable nodes connected. Set ${params.adapter.nodeConfigPath}.`);
}
async function resolveMeetingBrowserNode(params) {
	const node = await resolveMeetingBrowserNodeInfo(params);
	if (!node.nodeId) throw new Error(`${params.adapter.displayName} node did not include a node id.`);
	return node.nodeId;
}
function unwrapNodeInvokePayload(raw, adapter) {
	const record = raw && typeof raw === "object" ? raw : {};
	if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) try {
		return JSON.parse(record.payloadJSON);
	} catch (error) {
		throw new Error(`${adapter.displayName} browser proxy returned malformed payloadJSON.`, { cause: error });
	}
	if ("payload" in record) return record.payload;
	return raw;
}
function parseBrowserProxyResult(raw, adapter) {
	const payload = unwrapNodeInvokePayload(raw, adapter);
	const proxy = payload && typeof payload === "object" ? payload : void 0;
	if (!proxy || !("result" in proxy)) throw new Error(`${adapter.displayName} browser proxy returned an invalid result.`);
	return proxy.result;
}
async function callMeetingBrowserProxyOnNode(params) {
	return parseBrowserProxyResult(await params.runtime.nodes.invoke({
		nodeId: params.nodeId,
		command: "browser.proxy",
		params: {
			method: params.method,
			path: params.path,
			body: params.body,
			timeoutMs: params.timeoutMs
		},
		timeoutMs: addTimerTimeoutGraceMs(params.timeoutMs) ?? 1,
		scopes: ["operator.admin"]
	}), params.adapter);
}
//#endregion
//#region src/meeting-bot/sox-audio-command.ts
function formatArgs(format) {
	return [
		"-t",
		"raw",
		"-r",
		String(format.sampleRate),
		"-c",
		String(format.channels),
		"-e",
		format.encoding,
		"-b",
		String(format.bits),
		...format.endian === "little" ? ["-L"] : format.endian === "big" ? ["-B"] : [],
		"-"
	];
}
function withBuffer(executable, bufferBytes, args) {
	return [
		executable,
		"-q",
		"--buffer",
		String(bufferBytes),
		...args
	];
}
function buildMeetingSoxAudioCommands(params) {
	const wire = formatArgs(params.format);
	if (!params.device) return {
		inputCommand: withBuffer(params.inputExecutable ?? "rec", params.bufferBytes, wire),
		outputCommand: withBuffer(params.outputExecutable ?? "play", params.bufferBytes, wire)
	};
	const deviceType = params.deviceType ?? "coreaudio";
	return {
		inputCommand: withBuffer(params.inputExecutable ?? "sox", params.bufferBytes, [
			"-t",
			deviceType,
			params.device,
			...wire
		]),
		outputCommand: withBuffer(params.outputExecutable ?? "sox", params.bufferBytes, [
			...wire,
			"-t",
			deviceType,
			params.device
		])
	};
}
//#endregion
//#region src/meeting-bot/audio-backend.ts
const PIPEWIRE_SINK_NAME = "openclaw_meeting_audio";
const PIPEWIRE_MONITOR_NAME = `${PIPEWIRE_SINK_NAME}.monitor`;
const PIPEWIRE_SOURCE_NAME = PIPEWIRE_SINK_NAME;
const PIPEWIRE_MEETING_AUDIO_DEVICE_LABEL = "OpenClaw Meeting Audio";
const BLACKHOLE_MEETING_AUDIO_DEVICE_LABEL = "BlackHole 2ch";
function resolvePulseFormat(config) {
	if (config.encoding === "mu-law" && config.bits === 8) return "ulaw";
	if (config.encoding === "signed-integer" && config.bits === 16) return config.endian === "big" ? "s16be" : "s16le";
	throw new Error(`PipeWire-Pulse does not support meeting audio format ${config.encoding}/${config.bits}.`);
}
function resolveMeetingAudioBackend(selection, platform = process.platform) {
	if (selection && selection !== "auto") {
		if (selection === "blackhole-2ch" && platform !== "darwin") throw new Error("BlackHole 2ch meeting audio requires macOS.");
		if (selection === "pipewire-pulse" && platform !== "linux") throw new Error("PipeWire-Pulse meeting audio requires Linux.");
		return selection;
	}
	if (platform === "darwin") return "blackhole-2ch";
	if (platform === "linux") return "pipewire-pulse";
	throw new Error(`Local Chrome meeting talk-back is unsupported on ${platform}; use transcribe mode or a macOS/Linux Chrome node.`);
}
function resolveMeetingAudioRuntime(config, platform = process.platform) {
	const backend = resolveMeetingAudioBackend(config.backend, platform);
	const bytesPerSecond = config.format.sampleRate * config.format.channels * Math.ceil(config.format.bits / 8);
	const pulseLatencyMs = Math.max(1, Math.ceil(config.bufferBytes / bytesPerSecond * 1e3));
	const defaults = backend === "blackhole-2ch" ? buildMeetingSoxAudioCommands({
		bufferBytes: config.bufferBytes,
		device: BLACKHOLE_MEETING_AUDIO_DEVICE_LABEL,
		deviceType: "coreaudio",
		format: config.format
	}) : {
		inputCommand: [
			"parec",
			"--raw",
			`--device=${PIPEWIRE_SOURCE_NAME}`,
			`--format=${resolvePulseFormat(config.format)}`,
			`--rate=${config.format.sampleRate}`,
			`--channels=${config.format.channels}`,
			`--latency-msec=${pulseLatencyMs}`
		],
		outputCommand: [
			"pacat",
			"--raw",
			"--playback",
			`--device=${PIPEWIRE_SINK_NAME}`,
			`--format=${resolvePulseFormat(config.format)}`,
			`--rate=${config.format.sampleRate}`,
			`--channels=${config.format.channels}`,
			`--latency-msec=${pulseLatencyMs}`
		]
	};
	return {
		backend,
		deviceLabel: backend === "blackhole-2ch" ? BLACKHOLE_MEETING_AUDIO_DEVICE_LABEL : PIPEWIRE_MEETING_AUDIO_DEVICE_LABEL,
		inputCommand: config.inputCommand ? [...config.inputCommand] : defaults.inputCommand,
		outputCommand: config.outputCommand ? [...config.outputCommand] : defaults.outputCommand
	};
}
function resolveMeetingAudioRuntimeForFormat(params) {
	return resolveMeetingAudioRuntime({
		backend: params.backend,
		bufferBytes: params.bufferBytes,
		format: params.format === "g711-ulaw-8khz" ? {
			sampleRate: 8e3,
			channels: 1,
			encoding: "mu-law",
			bits: 8
		} : {
			sampleRate: 24e3,
			channels: 1,
			encoding: "signed-integer",
			bits: 16,
			endian: "little"
		},
		inputCommand: params.inputCommand,
		outputCommand: params.outputCommand
	}, params.platform);
}
function commandOutput(result) {
	return `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
}
function pulseListContains(output, name) {
	return output.split(/\r?\n/u).some((line) => line.split(/\s+/u)[1] === name);
}
function assertCommandSucceeded(result, message) {
	if (result.code === 0) return;
	const detail = commandOutput(result).trim();
	throw new Error(detail ? `${message}: ${detail}` : message);
}
async function ensureMeetingAudioBackend(params) {
	if (params.backend === "blackhole-2ch") {
		const result = await params.run(["/usr/sbin/system_profiler", "SPAudioDataType"], params.timeoutMs);
		if (result.code !== 0 || !commandOutput(result).toLowerCase().includes("blackhole 2ch")) throw new Error("BlackHole 2ch audio device not found. Install BlackHole 2ch and SoX, then reboot.");
		return;
	}
	assertCommandSucceeded(await params.run(["pactl", "info"], params.timeoutMs), "PipeWire-Pulse is unavailable. Start pipewire-pulse and install pulseaudio-utils");
	let sinks = await params.run([
		"pactl",
		"list",
		"short",
		"sinks"
	], params.timeoutMs);
	if (!pulseListContains(sinks.stdout ?? "", PIPEWIRE_SINK_NAME)) {
		const loaded = await params.run([
			"pactl",
			"load-module",
			"module-null-sink",
			`sink_name=${PIPEWIRE_SINK_NAME}`,
			"rate=48000",
			"channels=2",
			"channel_map=front-left,front-right",
			`sink_properties='device.description="${PIPEWIRE_MEETING_AUDIO_DEVICE_LABEL}"'`
		], params.timeoutMs);
		if (loaded.code !== 0) {
			sinks = await params.run([
				"pactl",
				"list",
				"short",
				"sinks"
			], params.timeoutMs);
			if (!pulseListContains(sinks.stdout ?? "", PIPEWIRE_SINK_NAME)) assertCommandSucceeded(loaded, "Could not create the OpenClaw PipeWire-Pulse sink");
		}
	}
	let sources = await params.run([
		"pactl",
		"list",
		"short",
		"sources"
	], params.timeoutMs);
	if (!pulseListContains(sources.stdout ?? "", PIPEWIRE_SOURCE_NAME)) {
		const loaded = await params.run([
			"pactl",
			"load-module",
			"module-remap-source",
			`source_name=${PIPEWIRE_SOURCE_NAME}`,
			`master=${PIPEWIRE_MONITOR_NAME}`,
			"channels=2",
			"master_channel_map=front-left,front-right",
			"channel_map=front-left,front-right",
			"remix=no",
			`source_properties='device.description="${PIPEWIRE_MEETING_AUDIO_DEVICE_LABEL}"'`
		], params.timeoutMs);
		if (loaded.code !== 0) {
			sources = await params.run([
				"pactl",
				"list",
				"short",
				"sources"
			], params.timeoutMs);
			if (!pulseListContains(sources.stdout ?? "", PIPEWIRE_SOURCE_NAME)) assertCommandSucceeded(loaded, "Could not create the OpenClaw PipeWire-Pulse source");
		}
	}
	sinks = await params.run([
		"pactl",
		"list",
		"short",
		"sinks"
	], params.timeoutMs);
	sources = await params.run([
		"pactl",
		"list",
		"short",
		"sources"
	], params.timeoutMs);
	if (!pulseListContains(sinks.stdout ?? "", PIPEWIRE_SINK_NAME) || !pulseListContains(sources.stdout ?? "", PIPEWIRE_SOURCE_NAME)) throw new Error("OpenClaw PipeWire-Pulse sink or monitor source was not created.");
}
//#endregion
//#region src/meeting-bot/chrome-node-result.ts
/** Unwraps the node.invoke envelope while keeping result validation transport-generic. */
function parseMeetingChromeNodeResult(raw, invalidMessage) {
	const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
	if (!value || typeof value !== "object") throw new Error(invalidMessage);
	return value;
}
//#endregion
//#region src/meeting-bot/chrome-transport.ts
function createMeetingChromeTransport(options) {
	async function openOrRecoverMeeting(params) {
		const captureCaptions = params.mode === "transcribe" || resolveTranscriptsConfig(params.fullConfig.transcripts).enabled;
		if (params.config.chrome.launch) return await openMeetingWithBrowser({
			adapter: options.platform,
			callBrowser: params.callBrowser,
			config: params.config.chrome,
			session: {
				captureCaptions,
				meetingSessionId: params.meetingSessionId,
				mode: params.mode,
				url: params.url
			}
		});
		const recovered = await recoverMeetingBrowserTab({
			adapter: options.platform,
			allowSessionAdoption: true,
			autoJoin: params.config.chrome.autoJoin,
			callBrowser: params.callBrowser,
			captureCaptions,
			config: params.config.chrome,
			locationLabel: params.locationLabel,
			meetingSessionId: params.meetingSessionId,
			mode: params.mode,
			requestedMeetingUrl: params.url,
			trackedMeetingUrl: params.url,
			trackedTargetId: params.trackedTargetId
		});
		return {
			launched: false,
			browser: recovered.browser,
			tab: recovered.targetId ? {
				targetId: recovered.targetId,
				openedByPlugin: false
			} : void 0
		};
	}
	async function rollbackBrowserJoin(params) {
		if (!params.tab) return;
		const result = await leaveMeetingWithBrowser({
			adapter: options.platform,
			callBrowser: params.callBrowser,
			launch: true,
			meetingSessionId: params.meetingSessionId,
			meetingUrl: params.url,
			tab: params.tab,
			timeoutMs: params.config.chrome.joinTimeoutMs
		}).catch((error) => ({
			left: false,
			note: error instanceof Error ? error.message : String(error)
		}));
		if (!result.left) params.logger.warn(`${options.platform.logScope} browser rollback after realtime startup failure did not complete: ${result.note}`);
	}
	async function prepareAudioRuntime(params) {
		const audio = resolveMeetingAudioRuntimeForFormat({
			backend: params.config.chrome.audioBackend,
			bufferBytes: params.config.chrome.audioBufferBytes,
			format: params.config.chrome.audioFormat,
			inputCommand: params.config.chrome.audioInputCommandOverride,
			outputCommand: params.config.chrome.audioOutputCommandOverride
		});
		await ensureMeetingAudioBackend({
			backend: audio.backend,
			timeoutMs: params.timeoutMs,
			run: async (argv, timeoutMs) => {
				const result = await params.runtime.system.runCommandWithTimeout(argv, { timeoutMs });
				return {
					...result,
					code: result.code ?? 1
				};
			}
		});
		return audio;
	}
	async function assertAudioDeviceAvailable(params) {
		await prepareAudioRuntime(params);
	}
	async function startLocalAudioBridge(params) {
		if (!options.isTalkBackMode(params.mode)) return;
		const transport = options.runtime.createLocalAudioTransport({
			inputCommand: params.audio.inputCommand,
			outputCommand: params.audio.outputCommand,
			audioFormat: params.config.chrome.audioFormat,
			bargeInInputCommand: params.config.chrome.bargeInInputCommand,
			bargeInRmsThreshold: params.config.chrome.bargeInRmsThreshold,
			bargeInPeakThreshold: params.config.chrome.bargeInPeakThreshold,
			bargeInCooldownMs: params.config.chrome.bargeInCooldownMs,
			logger: params.logger,
			logScope: options.platform.logScope
		});
		const bindings = options.runtime.createBindings({
			platform: options.platform,
			...params
		});
		try {
			return {
				type: "command-pair",
				...params.mode === "agent" ? await options.runtime.startAgentRealtimeEngine({
					config: params.config,
					fullConfig: params.fullConfig,
					runtime: params.runtime,
					platform: bindings.platform,
					meetingSessionId: params.meetingSessionId,
					requesterSessionKey: params.requesterSessionKey,
					transport,
					logger: params.logger,
					consultAgent: bindings.consultAgent
				}) : await options.runtime.startRealtimeEngine({
					config: {
						...params.config,
						realtime: {
							...params.config.realtime,
							strategy: "bidi"
						}
					},
					fullConfig: params.fullConfig,
					runtime: params.runtime,
					...bindings,
					meetingSessionId: params.meetingSessionId,
					requesterSessionKey: params.requesterSessionKey,
					transport,
					logger: params.logger
				})
			};
		} catch (error) {
			await transport.dispose().catch(() => {});
			throw error;
		}
	}
	async function launchInChrome(params) {
		const audio = options.isTalkBackMode(params.mode) ? await prepareAudioRuntime({
			runtime: params.runtime,
			config: params.config,
			timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
		}) : void 0;
		const callBrowser = await resolveLocalMeetingBrowserRequest(params.runtime);
		const result = await openOrRecoverMeeting({
			callBrowser,
			config: params.config,
			fullConfig: params.fullConfig,
			locationLabel: "in local Chrome",
			meetingSessionId: params.meetingSessionId,
			mode: params.mode,
			trackedTargetId: params.trackedTargetId,
			url: params.url
		});
		if (!options.isRealtimeRouteReady(params.mode, result.browser)) return {
			...result,
			audioBackend: audio?.backend
		};
		try {
			return {
				...result,
				audioBackend: audio?.backend,
				audioBridge: audio ? await startLocalAudioBridge({
					...params,
					audio
				}) : void 0
			};
		} catch (error) {
			if (!options.preserveTrackedBrowserOnEngineFailure || !params.trackedTargetId) await rollbackBrowserJoin({
				callBrowser,
				config: params.config,
				logger: params.logger,
				meetingSessionId: params.meetingSessionId,
				tab: result.tab,
				url: params.url
			});
			throw error;
		}
	}
	async function resolveChromeNode(params) {
		return await resolveMeetingBrowserNode({
			...params,
			adapter: options.browserNodeAdapter
		});
	}
	async function callNodeBrowser(params) {
		return await callMeetingBrowserProxyOnNode({
			...params,
			adapter: options.browserNodeAdapter
		});
	}
	const parseNodeResult = (raw) => parseMeetingChromeNodeResult(raw, `${options.meetingLabel} node returned an invalid start result.`);
	async function launchOnNode(params) {
		const nodeId = await resolveChromeNode({
			runtime: params.runtime,
			requestedNode: params.config.chromeNode.node
		});
		try {
			await params.runtime.nodes.invoke({
				nodeId,
				command: options.nodeCommandName,
				params: {
					action: "stopByUrl",
					url: params.url,
					mode: params.mode
				},
				timeoutMs: 5e3
			});
		} catch (error) {
			params.logger.debug?.(`${options.platform.logScope} node bridge cleanup ignored: ${error instanceof Error ? error.message : String(error)}`);
		}
		const audioSetup = options.isTalkBackMode(params.mode) ? parseNodeResult(await params.runtime.nodes.invoke({
			nodeId,
			command: options.nodeCommandName,
			params: {
				action: "setup",
				audioBackend: params.config.chrome.audioBackend,
				audioFormat: params.config.chrome.audioFormat,
				audioBufferBytes: params.config.chrome.audioBufferBytes,
				...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
				...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {}
			},
			timeoutMs: 12e3
		})) : void 0;
		const callBrowser = async (request) => await callNodeBrowser({
			runtime: params.runtime,
			nodeId,
			method: request.method,
			path: request.path,
			body: request.body,
			timeoutMs: request.timeoutMs
		});
		const browser = await openOrRecoverMeeting({
			callBrowser,
			config: params.config,
			fullConfig: params.fullConfig,
			locationLabel: "on the selected Chrome node",
			meetingSessionId: params.meetingSessionId,
			mode: params.mode,
			trackedTargetId: params.trackedTargetId,
			url: params.url
		});
		if (!options.isRealtimeRouteReady(params.mode, browser.browser)) return {
			nodeId,
			launched: browser.launched,
			audioBackend: audioSetup?.audioBackend,
			browser: browser.browser,
			tab: browser.tab
		};
		try {
			const raw = await params.runtime.nodes.invoke({
				nodeId,
				command: options.nodeCommandName,
				params: {
					action: "start",
					url: params.url,
					mode: params.mode,
					launch: false,
					browserProfile: params.config.chrome.browserProfile,
					joinTimeoutMs: params.config.chrome.joinTimeoutMs,
					...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
					...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {},
					audioBackend: params.config.chrome.audioBackend,
					audioFormat: params.config.chrome.audioFormat,
					audioBufferBytes: params.config.chrome.audioBufferBytes
				},
				timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1
			});
			const result = parseNodeResult(raw);
			if (result.audioBridge?.type !== "node-command-pair") return {
				nodeId,
				launched: browser.launched || result.launched === true,
				audioBackend: result.audioBackend ?? audioSetup?.audioBackend,
				browser: browser.browser ?? result.browser,
				tab: browser.tab
			};
			if (!result.bridgeId) throw new Error(`${options.meetingLabel} node did not return an audio bridge id.`);
			const transport = options.runtime.createNodeAudioTransport({
				runtime: params.runtime,
				nodeId,
				bridgeId: result.bridgeId,
				audioFormat: params.config.chrome.audioFormat,
				logger: params.logger,
				commandName: options.nodeCommandName,
				logScope: options.platform.logScope,
				logPrefix: params.mode === "agent" ? "node agent" : "node"
			});
			Reflect.set(transport, Symbol.for("openclaw.internal.meeting-node-output-generation.v1"), result.audioBridge.outputGeneration === true);
			const bindings = options.runtime.createBindings({
				platform: options.platform,
				...params
			});
			let engine;
			try {
				engine = params.mode === "agent" ? await options.runtime.startAgentRealtimeEngine({
					config: params.config,
					fullConfig: params.fullConfig,
					runtime: params.runtime,
					platform: bindings.platform,
					meetingSessionId: params.meetingSessionId,
					requesterSessionKey: params.requesterSessionKey,
					logPrefix: "node",
					transport,
					logger: params.logger,
					consultAgent: bindings.consultAgent
				}) : await options.runtime.startRealtimeEngine({
					config: {
						...params.config,
						realtime: {
							...params.config.realtime,
							strategy: "bidi"
						}
					},
					fullConfig: params.fullConfig,
					runtime: params.runtime,
					...bindings,
					meetingSessionId: params.meetingSessionId,
					requesterSessionKey: params.requesterSessionKey,
					logPrefix: "node",
					talkSessionId: `${options.platform.id}:${params.meetingSessionId}:${result.bridgeId}:node-realtime`,
					talkContext: {
						nodeId,
						bridgeId: result.bridgeId
					},
					transport,
					logger: params.logger
				});
			} catch (error) {
				await transport.dispose().catch(() => {});
				throw error;
			}
			return {
				nodeId,
				launched: browser.launched || result.launched === true,
				audioBackend: result.audioBackend ?? audioSetup?.audioBackend,
				audioBridge: {
					type: "node-command-pair",
					nodeId,
					bridgeId: result.bridgeId,
					...engine
				},
				browser: browser.browser ?? result.browser,
				tab: browser.tab
			};
		} catch (error) {
			await params.runtime.nodes.invoke({
				nodeId,
				command: options.nodeCommandName,
				params: {
					action: "stopByUrl",
					url: params.url,
					mode: params.mode
				},
				timeoutMs: 5e3
			}).catch(() => {});
			if (!options.preserveTrackedBrowserOnEngineFailure || !params.trackedTargetId) await rollbackBrowserJoin({
				callBrowser,
				config: params.config,
				logger: params.logger,
				meetingSessionId: params.meetingSessionId,
				tab: browser.tab,
				url: params.url
			});
			throw error;
		}
	}
	async function recoverCurrentTab(params) {
		const nodeId = params.transport === "chrome-node" ? params.nodeId ?? await resolveChromeNode({
			runtime: params.runtime,
			requestedNode: params.config.chromeNode.node
		}) : void 0;
		return {
			transport: params.transport,
			...nodeId ? { nodeId } : {},
			...await recoverMeetingBrowserTab({
				adapter: options.platform,
				callBrowser: nodeId ? async (request) => await callNodeBrowser({
					runtime: params.runtime,
					nodeId,
					method: request.method,
					path: request.path,
					body: request.body,
					timeoutMs: request.timeoutMs
				}) : await resolveLocalMeetingBrowserRequest(params.runtime),
				captureCaptions: params.mode === "transcribe" || resolveTranscriptsConfig(params.fullConfig?.transcripts).enabled,
				config: params.config.chrome,
				locationLabel: nodeId ? "on the selected Chrome node" : "in local Chrome",
				meetingSessionId: params.meetingSessionId,
				mode: params.mode,
				readOnly: params.readOnly,
				requestedMeetingUrl: params.url,
				trackedMeetingUrl: params.trackedMeetingUrl,
				trackedTargetId: params.trackedTargetId,
				timeoutMs: params.timeoutMs
			})
		};
	}
	async function leaveInBrowser(params) {
		const nodeId = params.nodeId;
		return await leaveMeetingWithBrowser({
			adapter: options.platform,
			callBrowser: nodeId ? async (request) => await callNodeBrowser({
				runtime: params.runtime,
				nodeId,
				method: request.method,
				path: request.path,
				body: request.body,
				timeoutMs: request.timeoutMs
			}) : await resolveLocalMeetingBrowserRequest(params.runtime),
			launch: params.config.chrome.launch || !params.tab.openedByPlugin,
			meetingSessionId: params.meetingSessionId,
			meetingUrl: params.meetingUrl,
			tab: params.tab,
			timeoutMs: params.config.chrome.joinTimeoutMs
		});
	}
	async function readTranscript(params) {
		const nodeId = params.nodeId;
		return await readMeetingTranscriptWithBrowser({
			adapter: options.platform,
			callBrowser: nodeId ? async (request) => await callNodeBrowser({
				runtime: params.runtime,
				nodeId,
				method: request.method,
				path: request.path,
				body: request.body,
				timeoutMs: request.timeoutMs
			}) : await resolveLocalMeetingBrowserRequest(params.runtime),
			finalize: params.finalize === true,
			meetingUrl: params.meetingUrl,
			meetingSessionId: params.meetingSessionId,
			tab: params.tab,
			timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
		});
	}
	return {
		assertAudioDeviceAvailable,
		launchInChrome,
		launchOnNode,
		leaveInBrowser,
		readTranscript,
		recoverCurrentTab
	};
}
//#endregion
//#region src/meeting-bot/node-audio-config.ts
const DEFAULT_NODE_AUDIO_BUFFER_BYTES = 4096;
const DEFAULT_NODE_AUDIO_FORMAT = "pcm16-24khz";
function readMeetingNodeCommand(value) {
	if (!Array.isArray(value)) return;
	const result = value.filter((entry) => typeof entry === "string" && entry.length > 0);
	return result.length > 0 ? result : void 0;
}
function readAudioBackend(value) {
	if (value === void 0) return;
	if (value === "auto" || value === "blackhole-2ch" || value === "pipewire-pulse") return value;
	throw new Error("audioBackend must be auto, blackhole-2ch, or pipewire-pulse");
}
function readAudioFormat(value) {
	if (value === void 0) return;
	if (value === "pcm16-24khz" || value === "g711-ulaw-8khz") return value;
	throw new Error("audioFormat must be pcm16-24khz or g711-ulaw-8khz");
}
function readAudioBufferBytes(value) {
	if (value === void 0) return;
	if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
	throw new Error("audioBufferBytes must be a positive safe integer");
}
/** Resolves untrusted node params before any backend or command process starts. */
async function prepareMeetingNodeAudio(params, timeoutMs, options) {
	const defaults = options.defaultAudio;
	const config = {
		backend: readAudioBackend(params.audioBackend) ?? defaults?.backend ?? "auto",
		bufferBytes: readAudioBufferBytes(params.audioBufferBytes) ?? defaults?.bufferBytes ?? DEFAULT_NODE_AUDIO_BUFFER_BYTES,
		format: readAudioFormat(params.audioFormat) ?? defaults?.format ?? DEFAULT_NODE_AUDIO_FORMAT,
		inputCommand: readMeetingNodeCommand(params.audioInputCommand),
		outputCommand: readMeetingNodeCommand(params.audioOutputCommand),
		bargeInInputCommand: readMeetingNodeCommand(params.bargeInInputCommand)
	};
	if (options.prepareAudio) return await options.prepareAudio(config, timeoutMs);
	await options.assertAudioAvailable(timeoutMs);
	return {
		backend: "blackhole-2ch",
		deviceLabel: "BlackHole 2ch",
		inputCommand: config.inputCommand ?? [...options.defaultAudioInputCommand],
		outputCommand: config.outputCommand ?? [...options.defaultAudioOutputCommand]
	};
}
//#endregion
//#region src/meeting-bot/node-audio-pull-waiters.ts
/** Internal pull-wait ownership used by the node-host long poll. */
var MeetingNodeAudioPullWaiters = class {
	#waiters = /* @__PURE__ */ new Set();
	get size() {
		return this.#waiters.size;
	}
	async wait(timeoutMs) {
		let wake;
		const ready = new Promise((resolve) => {
			wake = resolve;
			this.#waiters.add(wake);
		});
		let timer;
		const timeout = new Promise((resolve) => {
			timer = setTimeout(resolve, timeoutMs);
		});
		try {
			await Promise.race([timeout, ready]);
		} finally {
			this.#waiters.delete(wake);
			if (timer !== void 0) clearTimeout(timer);
		}
	}
	wake() {
		const waiters = [...this.#waiters];
		this.#waiters.clear();
		for (const waiter of waiters) waiter();
	}
};
//#endregion
//#region src/meeting-bot/node-host.ts
const NODE_BRIDGE_TERMINATION_GRACE_MS = 2e3;
const NODE_BRIDGE_INPUT_DRAIN_MS = 3e3;
const NODE_BRIDGE_TERMINAL_RETENTION_MS = 5e3;
const NODE_BRIDGE_MAX_QUEUED_INPUT_CHUNKS = 200;
const NODE_BRIDGE_MAX_QUEUED_INPUT_BYTES = 1048576;
function readPositiveNumberOr(value, fallback) {
	return asPositiveFiniteNumber(value) ?? fallback;
}
function readOutputGeneration$1(value) {
	if (value === void 0) return;
	if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
	throw new Error("outputGeneration must be a non-negative integer");
}
function runCommandWithTimeout(argv, timeoutMs) {
	const { command, args } = splitCommandArgv(argv, "command");
	const result = spawnSync(command, args, {
		encoding: "utf8",
		timeout: timeoutMs
	});
	const errorMessage = result.error ? formatErrorMessage(result.error) : "";
	const stderr = errorMessage && result.stderr ? `${errorMessage}: ${result.stderr}` : errorMessage || result.stderr || (result.signal ? `terminated by ${result.signal}` : "");
	return {
		code: typeof result.status === "number" ? result.status : 1,
		stdout: result.stdout ?? "",
		stderr
	};
}
function waitForInputDrain(stream, timeoutMs) {
	if (!stream || stream.readableEnded || stream.destroyed) return Promise.resolve();
	return new Promise((resolve) => {
		let settled = false;
		const finish = () => {
			if (settled) return;
			settled = true;
			clearTimeout(timeout);
			stream.off("end", finish);
			stream.off("close", finish);
			resolve();
		};
		const timeout = setTimeout(finish, timeoutMs);
		timeout.unref?.();
		stream.once("end", finish);
		stream.once("close", finish);
		if (stream.readableEnded || stream.destroyed) finish();
	});
}
function createMeetingNodeHost(options) {
	const sessions = /* @__PURE__ */ new Map();
	const wake = (session) => {
		session.waiters.wake();
	};
	const releaseOutputWriteWaiters = (session, output) => {
		for (const waiter of session.outputWriteWaiters) if (!output || waiter.output === output) waiter.release();
	};
	const retireOutputProcess = (session, outputProcess) => {
		const stopPromise = terminateMeetingBridgeProcess(outputProcess, { graceMs: NODE_BRIDGE_TERMINATION_GRACE_MS });
		session.retiredOutputStops.add(stopPromise);
		stopPromise.finally(() => {
			session.retiredOutputStops.delete(stopPromise);
		});
	};
	const deleteSession = (session) => {
		if (session.terminalEvictionTimer) {
			clearTimeout(session.terminalEvictionTimer);
			session.terminalEvictionTimer = void 0;
		}
		session.chunks.length = 0;
		session.queuedInputBytes = 0;
		if (sessions.get(session.id) === session) sessions.delete(session.id);
	};
	const armTerminalEviction = (session) => {
		if (session.terminalEvictionTimer) clearTimeout(session.terminalEvictionTimer);
		session.terminalEvictionTimer = setTimeout(() => {
			deleteSession(session);
		}, NODE_BRIDGE_TERMINAL_RETENTION_MS);
		session.terminalEvictionTimer.unref?.();
	};
	const stopSession = (session, stopOptions = {}) => {
		session.stopping = true;
		if (stopOptions.discardQueuedAudio) {
			session.discardQueuedAudioOnStop = true;
			if (session.terminalEvictionTimer) {
				clearTimeout(session.terminalEvictionTimer);
				session.terminalEvictionTimer = void 0;
			}
			session.chunks.length = 0;
			session.queuedInputBytes = 0;
			if (!session.closed) {
				session.closed = true;
				session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
			}
			wake(session);
		}
		releaseOutputWriteWaiters(session);
		if (session.stopPromise) return session.stopPromise;
		const terminalReady = session.discardQueuedAudioOnStop ? Promise.resolve() : waitForInputDrain(session.input?.stdout, NODE_BRIDGE_INPUT_DRAIN_MS).then(() => {
			if (session.discardQueuedAudioOnStop || session.closed) return;
			session.closed = true;
			session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
			wake(session);
		});
		session.stopPromise = Promise.all([
			terminateMeetingBridgeProcess(session.input, { graceMs: NODE_BRIDGE_TERMINATION_GRACE_MS }),
			terminateMeetingBridgeProcess(session.output, { graceMs: NODE_BRIDGE_TERMINATION_GRACE_MS }),
			...session.retiredOutputStops,
			terminalReady
		]).then(() => {
			if (session.discardQueuedAudioOnStop) {
				deleteSession(session);
				return;
			}
			armTerminalEviction(session);
		});
		return session.stopPromise;
	};
	const attachOutputProcessHandlers = (session, outputProcess) => {
		const stopIfCurrent = () => {
			if (session.output === outputProcess) stopSession(session);
		};
		outputProcess.on("exit", stopIfCurrent);
		outputProcess.on("error", stopIfCurrent);
		outputProcess.stdin?.on("error", stopIfCurrent);
		outputProcess.stderr?.on("error", stopIfCurrent);
	};
	const startOutputProcess = (command) => spawn(command.command, command.args, { stdio: [
		"pipe",
		"ignore",
		"pipe"
	] });
	const startCommandPair = (params) => {
		const input = splitCommandArgv(params.inputCommand, "audio command");
		const output = splitCommandArgv(params.outputCommand, "audio command");
		const session = {
			id: `${options.bridgeIdPrefix}${randomUUID()}`,
			url: params.url,
			mode: params.mode,
			outputCommand: output,
			chunks: [],
			queuedInputBytes: 0,
			waiters: new MeetingNodeAudioPullWaiters(),
			closed: false,
			createdAt: (/* @__PURE__ */ new Date()).toISOString(),
			lastInputBytes: 0,
			lastOutputBytes: 0,
			clearCount: 0,
			outputGeneration: 0,
			outputWriteWaiters: /* @__PURE__ */ new Set(),
			retiredOutputStops: /* @__PURE__ */ new Set(),
			stopping: false,
			discardQueuedAudioOnStop: false
		};
		const outputProcess = startOutputProcess(output);
		let inputProcess;
		try {
			inputProcess = spawn(input.command, input.args, { stdio: [
				"ignore",
				"pipe",
				"pipe"
			] });
		} catch (error) {
			terminateMeetingBridgeProcess(outputProcess, { graceMs: NODE_BRIDGE_TERMINATION_GRACE_MS });
			throw error;
		}
		session.input = inputProcess;
		session.output = outputProcess;
		inputProcess.stdout?.on("data", (chunk) => {
			if (session.discardQueuedAudioOnStop || session.closed) return;
			const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
			session.lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
			session.lastInputBytes += audio.byteLength;
			const retainedAudio = Buffer.from(audio.subarray(Math.max(0, audio.byteLength - NODE_BRIDGE_MAX_QUEUED_INPUT_BYTES)));
			session.chunks.push(retainedAudio);
			session.queuedInputBytes += retainedAudio.byteLength;
			while (session.chunks.length > NODE_BRIDGE_MAX_QUEUED_INPUT_CHUNKS || session.queuedInputBytes > NODE_BRIDGE_MAX_QUEUED_INPUT_BYTES) {
				const evicted = session.chunks.shift();
				if (evicted) session.queuedInputBytes -= evicted.byteLength;
			}
			if (!session.stopPromise) wake(session);
		});
		const stop = () => {
			stopSession(session);
		};
		inputProcess.on("exit", stop);
		inputProcess.on("error", stop);
		inputProcess.stdout?.on("error", stop);
		inputProcess.stderr?.on("error", stop);
		attachOutputProcessHandlers(session, outputProcess);
		sessions.set(session.id, session);
		return session;
	};
	const pullAudio = async (params) => {
		const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
		if (!bridgeId) throw new Error("bridgeId required");
		const session = sessions.get(bridgeId);
		if (!session) throw new Error(`unknown bridgeId: ${bridgeId}`);
		const timeoutMs = Math.min(readPositiveNumberOr(params.timeoutMs, 250), 2e3);
		if (session.chunks.length === 0 && !session.closed) await session.waiters.wait(timeoutMs);
		const chunk = session.chunks.shift();
		if (chunk) session.queuedInputBytes -= chunk.byteLength;
		const closed = session.closed && session.chunks.length === 0;
		if (closed) deleteSession(session);
		else if (chunk && session.closed && session.terminalEvictionTimer) armTerminalEviction(session);
		return {
			bridgeId,
			closed,
			base64: chunk ? chunk.toString("base64") : void 0
		};
	};
	const staleOutputResult = (session) => ({
		bridgeId: session.id,
		ok: true,
		stale: true
	});
	const writeOutputChunk = (session, output, audio) => new Promise((resolve, reject) => {
		const stdin = output.stdin;
		if (!stdin) {
			reject(/* @__PURE__ */ new Error("audio output stream is closed"));
			return;
		}
		let settled = false;
		const finish = (error) => {
			if (settled) return;
			settled = true;
			session.outputWriteWaiters.delete(waiter);
			if (error) reject(error);
			else resolve();
		};
		const waiter = {
			output,
			release: () => finish()
		};
		session.outputWriteWaiters.add(waiter);
		try {
			stdin.write(audio, (error) => finish(error ?? void 0));
		} catch (error) {
			finish(error instanceof Error ? error : new Error(formatErrorMessage(error)));
			return;
		}
		if (stdin.destroyed || stdin.writableEnded) finish(/* @__PURE__ */ new Error("audio output stream is closed"));
	});
	const pushAudio = async (params) => {
		const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
		const base64 = readNonEmptyStringPreservingWhitespace(params.base64);
		if (!bridgeId || !base64) throw new Error("bridgeId and base64 required");
		const session = sessions.get(bridgeId);
		if (!session || session.stopping || session.closed) throw new Error(`bridge is not open: ${bridgeId}`);
		const requestedGeneration = readOutputGeneration$1(params.outputGeneration);
		if (requestedGeneration !== void 0 && requestedGeneration !== session.outputGeneration) return staleOutputResult(session);
		const output = session.output;
		if (!output?.stdin) throw new Error(`bridge is not open: ${bridgeId}`);
		const audio = decodeMeetingAudioBase64(base64, "pushAudio");
		try {
			await writeOutputChunk(session, output, audio);
		} catch {
			if (session.output !== output || requestedGeneration !== void 0 && requestedGeneration !== session.outputGeneration) return staleOutputResult(session);
			stopSession(session);
			throw new Error(`bridge is not open: ${bridgeId}`);
		}
		if (session.stopping || session.closed || session.output !== output || requestedGeneration !== void 0 && requestedGeneration !== session.outputGeneration) return staleOutputResult(session);
		session.lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
		session.lastOutputBytes += audio.byteLength;
		return {
			bridgeId,
			ok: true
		};
	};
	const clearAudio = (params) => {
		const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
		if (!bridgeId) throw new Error("bridgeId required");
		const session = sessions.get(bridgeId);
		if (!session || session.stopping || session.closed) throw new Error(`bridge is not open: ${bridgeId}`);
		const requestedGeneration = readOutputGeneration$1(params.outputGeneration);
		if (requestedGeneration !== void 0 && requestedGeneration <= session.outputGeneration) return staleOutputResult(session);
		if (requestedGeneration === void 0 && session.outputGeneration >= Number.MAX_SAFE_INTEGER) throw new Error("outputGeneration exhausted");
		const nextGeneration = requestedGeneration ?? session.outputGeneration + 1;
		const previousOutput = session.output;
		const outputProcess = startOutputProcess(session.outputCommand);
		session.output = outputProcess;
		session.outputGeneration = nextGeneration;
		attachOutputProcessHandlers(session, outputProcess);
		releaseOutputWriteWaiters(session, previousOutput);
		session.clearCount += 1;
		session.lastClearAt = (/* @__PURE__ */ new Date()).toISOString();
		retireOutputProcess(session, previousOutput);
		return {
			bridgeId,
			ok: true,
			clearCount: session.clearCount
		};
	};
	const startBrowser = async (params) => {
		const url = options.normalizeUrl(params.url);
		const timeoutMs = readPositiveNumberOr(params.joinTimeoutMs, 3e4);
		const mode = readNonEmptyStringPreservingWhitespace(params.mode);
		let audioRuntime;
		let bridgeId;
		let audioBridge;
		if (mode && options.talkBackModes.has(mode)) {
			audioRuntime = await prepareMeetingNodeAudio(params, Math.min(timeoutMs, 1e4), options);
			const healthCommand = readMeetingNodeCommand(params.audioBridgeHealthCommand);
			if (healthCommand) {
				const health = runCommandWithTimeout(healthCommand, timeoutMs);
				if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
			}
			const bridgeCommand = readMeetingNodeCommand(params.audioBridgeCommand);
			if (bridgeCommand) {
				if (mode === options.agentMode) throw new Error("Chrome agent mode requires audioInputCommand and audioOutputCommand so OpenClaw can run STT and regular TTS directly.");
				const bridge = runCommandWithTimeout(bridgeCommand, timeoutMs);
				if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
				audioBridge = { type: "external-command" };
			} else {
				bridgeId = startCommandPair({
					inputCommand: audioRuntime.inputCommand,
					outputCommand: audioRuntime.outputCommand,
					url,
					mode
				}).id;
				audioBridge = {
					type: "node-command-pair",
					outputGeneration: true
				};
			}
		}
		if (params.launch !== false) {
			const argv = [
				"open",
				"-a",
				options.browser.application,
				url
			];
			const browserProfile = readNonEmptyStringPreservingWhitespace(params.browserProfile);
			if (browserProfile) argv.push(...options.browser.buildProfileArgs(browserProfile));
			try {
				const result = runCommandWithTimeout(argv, timeoutMs);
				if (result.code !== 0) throw new Error(`failed to launch Chrome for ${options.browserLabel}: ${result.stderr || result.stdout || result.code}`);
			} catch (error) {
				if (bridgeId) {
					const session = sessions.get(bridgeId);
					if (session) stopSession(session, { discardQueuedAudio: true });
				}
				throw error;
			}
		}
		return {
			launched: params.launch !== false,
			audioBackend: audioRuntime?.backend,
			audioDeviceLabel: audioRuntime?.deviceLabel,
			bridgeId,
			audioBridge,
			browser: params.launch !== false ? {
				status: options.browser.openedStatus,
				browserUrl: url,
				notes: options.browser.openedNotes
			} : void 0
		};
	};
	const bridgeStatus = (params) => {
		const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
		const session = bridgeId ? sessions.get(bridgeId) : void 0;
		return { bridge: session ? {
			bridgeId,
			closed: session.stopping || session.closed,
			createdAt: session.createdAt,
			lastInputAt: session.lastInputAt,
			lastOutputAt: session.lastOutputAt,
			lastClearAt: session.lastClearAt,
			lastInputBytes: session.lastInputBytes,
			lastOutputBytes: session.lastOutputBytes,
			clearCount: session.clearCount,
			queuedInputChunks: session.chunks.length
		} : bridgeId ? {
			bridgeId,
			closed: true
		} : void 0 };
	};
	const summarizeSession = (session) => ({
		bridgeId: session.id,
		url: session.url,
		mode: session.mode,
		closed: session.closed,
		createdAt: session.createdAt,
		closedAt: session.closedAt,
		lastInputAt: session.lastInputAt,
		lastOutputAt: session.lastOutputAt,
		lastInputBytes: session.lastInputBytes,
		lastOutputBytes: session.lastOutputBytes
	});
	const listSessions = (params) => {
		const urlKey = options.normalizeMeetingKey(readNonEmptyStringPreservingWhitespace(params.url));
		const mode = readNonEmptyStringPreservingWhitespace(params.mode);
		return { bridges: [...sessions.values()].filter((session) => !session.stopping && !session.closed).filter((session) => !urlKey || options.normalizeMeetingKey(session.url) === urlKey).filter((session) => !mode || session.mode === mode).map(summarizeSession) };
	};
	const stopSessionsByUrl = async (params) => {
		const urlKey = options.normalizeMeetingKey(readNonEmptyStringPreservingWhitespace(params.url));
		if (!urlKey) throw new Error("url required");
		const mode = readNonEmptyStringPreservingWhitespace(params.mode);
		const exceptBridgeId = readNonEmptyStringPreservingWhitespace(params.exceptBridgeId);
		let stopped = 0;
		const stopping = [];
		for (const [bridgeId, session] of sessions) {
			if (exceptBridgeId && bridgeId === exceptBridgeId) continue;
			if (options.normalizeMeetingKey(session.url) !== urlKey) continue;
			if (mode && session.mode !== mode) continue;
			const wasClosed = session.stopping || session.closed;
			stopping.push(stopSession(session, { discardQueuedAudio: true }));
			if (!wasClosed) stopped += 1;
		}
		await Promise.all(stopping);
		return {
			ok: true,
			stopped
		};
	};
	const stopBrowser = async (params) => {
		const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
		if (!bridgeId) return {
			ok: true,
			stopped: false
		};
		const session = sessions.get(bridgeId);
		if (!session) return {
			ok: true,
			stopped: false
		};
		const wasStopped = session.stopping || session.closed;
		await stopSession(session, { discardQueuedAudio: true });
		return {
			ok: true,
			stopped: !wasStopped
		};
	};
	return { async handleCommand(paramsJSON) {
		let raw = {};
		if (paramsJSON) try {
			raw = JSON.parse(paramsJSON);
		} catch {
			throw new Error(`${options.displayName} node host received malformed params JSON.`);
		}
		const params = asOptionalRecord(raw) ?? {};
		const action = readNonEmptyStringPreservingWhitespace(params.action);
		let result;
		switch (action) {
			case "setup":
				{
					const audioRuntime = await prepareMeetingNodeAudio(params, 1e4, options);
					result = {
						ok: true,
						audioBackend: audioRuntime.backend,
						audioDeviceLabel: audioRuntime.deviceLabel
					};
				}
				break;
			case "start":
				result = await startBrowser(params);
				break;
			case "status":
				result = bridgeStatus(params);
				break;
			case "list":
				result = listSessions(params);
				break;
			case "stopByUrl":
				result = await stopSessionsByUrl(params);
				break;
			case "pullAudio":
				result = await pullAudio(params);
				break;
			case "pushAudio":
				result = await pushAudio(params);
				break;
			case "clearAudio":
				result = clearAudio(params);
				break;
			case "stop":
				result = await stopBrowser(params);
				break;
			default: throw new Error(`unsupported ${options.commandName} action`);
		}
		return JSON.stringify(result);
	} };
}
//#endregion
//#region src/meeting-bot/configured-node-host.ts
function isSpawnSyncTimeout(error) {
	return error instanceof Error && "code" in error && error.code === "ETIMEDOUT";
}
function createMeetingConfiguredNodeHost(options) {
	const runCommand = (argv, timeoutMs) => {
		const [command, ...args] = argv;
		if (!command) return {
			code: 1,
			stderr: "command must not be empty"
		};
		const result = spawnSync(command, args, {
			encoding: "utf8",
			timeout: timeoutMs
		});
		if (isSpawnSyncTimeout(result.error)) throw new Error(`${options.meetingLabel} audio prerequisite check timed out on the node.`);
		const stderr = [
			result.error ? result.error instanceof Error ? result.error.message : String(result.error) : "",
			result.stderr,
			result.signal ? `terminated by ${result.signal}` : ""
		].filter(Boolean).join(": ");
		return {
			code: typeof result.status === "number" ? result.status : 1,
			stdout: result.stdout ?? "",
			stderr
		};
	};
	const probeCommand = (command, timeoutMs) => {
		const result = spawnSync("/bin/sh", [
			"-lc",
			"command -v \"$1\" >/dev/null 2>&1",
			"sh",
			command
		], {
			encoding: "utf8",
			timeout: timeoutMs
		});
		if (isSpawnSyncTimeout(result.error)) return "timed-out";
		return result.status === 0 ? "found" : "missing";
	};
	const prepareAudio = async (config, timeoutMs) => {
		const deadline = Date.now() + timeoutMs;
		const commandTimeout = () => {
			if (!options.sharePrerequisiteDeadline) return timeoutMs;
			const remainingMs = deadline - Date.now();
			if (remainingMs <= 0) throw new Error(`${options.meetingLabel} audio prerequisite check timed out on the node.`);
			return remainingMs;
		};
		const runtime = resolveMeetingAudioRuntimeForFormat({
			backend: config.backend,
			bufferBytes: config.bufferBytes,
			format: config.format,
			inputCommand: config.inputCommand,
			outputCommand: config.outputCommand
		});
		await ensureMeetingAudioBackend({
			backend: runtime.backend,
			run: async (argv, requestedTimeoutMs) => {
				return runCommand(argv, Math.min(requestedTimeoutMs, commandTimeout()));
			},
			timeoutMs
		});
		const commandNames = new Set([
			runtime.inputCommand,
			runtime.outputCommand,
			config.bargeInInputCommand
		].flatMap((command) => command?.[0] ? [command[0]] : []));
		for (const command of commandNames) {
			const probeResult = probeCommand(command, commandTimeout());
			if (probeResult === "timed-out") throw new Error(`${options.meetingLabel} audio prerequisite check timed out on the node.`);
			if (probeResult === "missing") throw new Error(`Configured audio command not found on the node: ${command}`);
		}
		return runtime;
	};
	const host = createMeetingNodeHost({
		...options,
		assertAudioAvailable: async (timeoutMs) => {
			await prepareAudio({
				backend: options.defaultAudio?.backend ?? "auto",
				bufferBytes: options.defaultAudio?.bufferBytes ?? 4096,
				format: options.defaultAudio?.format ?? "pcm16-24khz"
			}, timeoutMs);
		},
		prepareAudio
	});
	return async (paramsJSON) => await host.handleCommand(paramsJSON);
}
//#endregion
//#region src/meeting-bot/meeting-modes.ts
function isMeetingTalkBackMode(mode) {
	return mode === "agent" || mode === "bidi";
}
function isMeetingRealtimeRouteReady(mode, health) {
	return isMeetingTalkBackMode(mode) && health?.inCall === true && health.micMuted === false && health.audioInputRouted === true && health.audioOutputRouted === true && health.manualAction === void 0;
}
//#endregion
//#region src/meeting-bot/plugin-cli.ts
function parseTimeout(value) {
	if (value === void 0) return;
	const parsed = parseStrictNonNegativeInteger(value);
	if (parsed === void 0 || parsed === 0) throw new Error("timeout-ms must be a positive integer");
	return parsed;
}
function parseMeetingCliMode(value) {
	if (value === void 0 || value === "agent" || value === "bidi" || value === "transcribe") return value;
	throw new Error(`mode must be agent, bidi, or transcribe; received ${value}`);
}
function parseMeetingCliTransport(value) {
	if (value === void 0 || value === "chrome" || value === "chrome-node") return value;
	throw new Error(`transport must be chrome or chrome-node; received ${value}`);
}
function markMeetingSetupFailure(result) {
	if (result && typeof result === "object" && "ok" in result && result.ok === false) process.exitCode = 1;
}
function joinPayload(url, options) {
	return {
		url,
		...options.transport ? { transport: parseMeetingCliTransport(options.transport) } : {},
		...options.mode ? { mode: parseMeetingCliMode(options.mode) } : {},
		...options.message ? { message: options.message } : {},
		...options.timeoutMs ? { timeoutMs: parseTimeout(options.timeoutMs) } : {}
	};
}
function addJoinOptions(command) {
	return command.option("--transport <transport>", "chrome or chrome-node").option("--mode <mode>", "agent, bidi, or transcribe").option("--message <text>", "instructions to speak after joining");
}
function registerMeetingPluginCli(options) {
	const call = async (method, payload) => {
		const requestedTimeoutMs = typeof payload?.timeoutMs === "number" ? payload.timeoutMs : void 0;
		const timeoutMs = options.resolveGatewayTimeoutMs({
			config: options.config,
			method,
			requestedTimeoutMs
		});
		const result = await options.callGateway(method, {
			json: true,
			timeout: String(timeoutMs)
		}, payload, {
			progress: false,
			scopes: ["operator.admin"]
		});
		process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
		return result;
	};
	const method = (action) => `${options.methodPrefix}.${action}`;
	const root = options.program.command(options.commandName).description(options.descriptions.root);
	const command = (usage, description) => root.command(usage).description(description);
	addJoinOptions(command("join <url>", options.descriptions.join)).action(async (url, joinOptions) => {
		await call(method("join"), joinPayload(url, joinOptions));
	});
	command("leave <session-id>", options.descriptions.leave).action(async (sessionId) => {
		await call(method("leave"), { sessionId });
	});
	command("status [session-id]", options.descriptions.status).action(async (sessionId) => {
		await call(method("status"), sessionId ? { sessionId } : {});
	});
	command("transcript <session-id>", "read the current transcript snapshot").option("--since-index <index>", "resume from a prior transcript index").action(async (sessionId, transcriptOptions) => {
		const sinceIndex = transcriptOptions.sinceIndex === void 0 ? void 0 : parseStrictNonNegativeInteger(transcriptOptions.sinceIndex);
		if (transcriptOptions.sinceIndex !== void 0 && sinceIndex === void 0) throw new Error("since-index must be a non-negative integer");
		await call(method("transcript"), {
			sessionId,
			...sinceIndex === void 0 ? {} : { sinceIndex }
		});
	});
	command("speak <session-id> [message]", "speak through an active talk-back session").action(async (sessionId, message) => {
		await call(method("speak"), {
			sessionId,
			...message ? { message } : {}
		});
	});
	command("setup", options.descriptions.setup).option("--transport <transport>", "chrome or chrome-node").option("--mode <mode>", "agent, bidi, or transcribe").action(async (setupOptions) => {
		markMeetingSetupFailure(await call(method("setup"), {
			transport: parseMeetingCliTransport(setupOptions.transport),
			mode: parseMeetingCliMode(setupOptions.mode)
		}));
	});
	for (const [name, action, description] of [[
		"test-speech",
		"testSpeech",
		options.descriptions.testSpeech
	], [
		"test-listen",
		"testListen",
		options.descriptions.testListen
	]]) addJoinOptions(command(`${name} <url>`, description)).option("--timeout-ms <ms>", "probe timeout in milliseconds").action(async (url, joinOptions) => {
		await call(method(action), joinPayload(url, joinOptions));
	});
}
//#endregion
//#region src/meeting-bot/plugin-config.ts
const DEFAULT_AUDIO_BUFFER_BYTES = 4096;
const DEFAULT_AUDIO_FORMAT = "pcm16-24khz";
const DEFAULT_MODE_HELP = "Agent consults OpenClaw, bidi uses direct realtime voice, and transcribe observes only.";
const CHROME_NODE_HELP = "Node id/name/IP that owns Chrome and the native virtual-audio backend.";
function resolveBoolean(value, fallback) {
	return typeof value === "boolean" ? value : fallback;
}
function resolvePositiveNumber(value, fallback) {
	return asPositiveFiniteNumber(value) ?? fallback;
}
function resolveTimer(value, fallback) {
	return resolvePositiveTimerTimeoutMs(resolvePositiveNumber(value, fallback), fallback);
}
function resolveMode(value) {
	const normalized = normalizeOptionalLowercaseString(value);
	return normalized === "agent" || normalized === "bidi" || normalized === "transcribe" ? normalized : "agent";
}
function resolveAudioFormat(value) {
	const normalized = normalizeOptionalLowercaseString(value)?.replaceAll("_", "-");
	return normalized === "g711-ulaw-8khz" ? normalized : DEFAULT_AUDIO_FORMAT;
}
function resolveAudioBackend(value) {
	const normalized = normalizeOptionalLowercaseString(value)?.replaceAll("_", "-");
	return normalized === "blackhole-2ch" || normalized === "pipewire-pulse" ? normalized : "auto";
}
function resolveProviders(value) {
	const providers = {};
	for (const [key, entry] of Object.entries(asRecord(value))) {
		const id = normalizeOptionalLowercaseString(key);
		if (id) providers[id] = asRecord(entry);
	}
	return providers;
}
function createMeetingPluginConfigSchema(options) {
	const buildAudioCommands = (backend, format, bufferBytes) => {
		const platform = backend === "blackhole-2ch" ? "darwin" : backend === "pipewire-pulse" ? "linux" : process.platform;
		try {
			return resolveMeetingAudioRuntimeForFormat({
				backend,
				bufferBytes,
				format,
				platform
			});
		} catch {
			return resolveMeetingAudioRuntimeForFormat({
				backend: "blackhole-2ch",
				bufferBytes,
				format,
				platform: "darwin"
			});
		}
	};
	const defaultAudioRuntime = buildAudioCommands("auto", DEFAULT_AUDIO_FORMAT, DEFAULT_AUDIO_BUFFER_BYTES);
	const defaults = {
		enabled: true,
		defaultMode: "agent",
		chrome: {
			audioBackend: "auto",
			audioFormat: DEFAULT_AUDIO_FORMAT,
			audioBufferBytes: DEFAULT_AUDIO_BUFFER_BYTES,
			launch: true,
			guestName: "OpenClaw Agent",
			reuseExistingTab: true,
			autoJoin: true,
			joinTimeoutMs: 3e4,
			waitForInCallMs: 6e4,
			audioInputCommand: defaultAudioRuntime.inputCommand,
			audioOutputCommand: defaultAudioRuntime.outputCommand,
			bargeInRmsThreshold: 650,
			bargeInPeakThreshold: 2500,
			bargeInCooldownMs: 900
		},
		chromeNode: {},
		realtime: {
			strategy: "agent",
			provider: "openai",
			transcriptionProvider: "openai",
			instructions: options.defaultRealtimeInstructions,
			introMessage: "Say exactly: I'm here and listening.",
			toolPolicy: "safe-read-only",
			providers: {}
		}
	};
	const resolveConfig = (input) => {
		const raw = asRecord(input);
		const chrome = asRecord(raw.chrome);
		const chromeNode = asRecord(raw.chromeNode);
		const realtime = asRecord(raw.realtime);
		const audioFormat = resolveAudioFormat(chrome.audioFormat);
		const audioBufferBytes = Math.max(17, Math.trunc(resolvePositiveNumber(chrome.audioBufferBytes, DEFAULT_AUDIO_BUFFER_BYTES)));
		const audioBackend = resolveAudioBackend(chrome.audioBackend);
		const generatedCommands = buildAudioCommands(audioBackend, audioFormat, audioBufferBytes);
		const audioInputCommandOverride = normalizeOptionalTrimmedStringList(chrome.audioInputCommand);
		const audioOutputCommandOverride = normalizeOptionalTrimmedStringList(chrome.audioOutputCommand);
		const provider = normalizeOptionalString(realtime.provider) ?? defaults.realtime.provider;
		return {
			enabled: resolveBoolean(raw.enabled, defaults.enabled),
			defaultMode: resolveMode(raw.defaultMode),
			chrome: {
				audioBackend,
				audioFormat,
				audioBufferBytes,
				launch: resolveBoolean(chrome.launch, defaults.chrome.launch),
				browserProfile: normalizeOptionalString(chrome.browserProfile),
				guestName: normalizeOptionalString(chrome.guestName) ?? defaults.chrome.guestName,
				reuseExistingTab: resolveBoolean(chrome.reuseExistingTab, defaults.chrome.reuseExistingTab),
				autoJoin: resolveBoolean(chrome.autoJoin, defaults.chrome.autoJoin),
				joinTimeoutMs: resolveTimer(chrome.joinTimeoutMs, defaults.chrome.joinTimeoutMs),
				waitForInCallMs: resolveTimer(chrome.waitForInCallMs, defaults.chrome.waitForInCallMs),
				audioInputCommand: audioInputCommandOverride ?? generatedCommands.inputCommand,
				audioOutputCommand: audioOutputCommandOverride ?? generatedCommands.outputCommand,
				audioInputCommandOverride,
				audioOutputCommandOverride,
				bargeInInputCommand: normalizeOptionalTrimmedStringList(chrome.bargeInInputCommand),
				bargeInRmsThreshold: resolvePositiveNumber(chrome.bargeInRmsThreshold, defaults.chrome.bargeInRmsThreshold),
				bargeInPeakThreshold: resolvePositiveNumber(chrome.bargeInPeakThreshold, defaults.chrome.bargeInPeakThreshold),
				bargeInCooldownMs: resolveTimer(chrome.bargeInCooldownMs, defaults.chrome.bargeInCooldownMs)
			},
			chromeNode: { node: normalizeOptionalString(chromeNode.node) },
			realtime: {
				strategy: normalizeOptionalLowercaseString(realtime.strategy) === "bidi" ? "bidi" : "agent",
				provider,
				transcriptionProvider: normalizeOptionalString(realtime.transcriptionProvider) ?? defaults.realtime.transcriptionProvider,
				voiceProvider: normalizeOptionalString(realtime.voiceProvider),
				model: normalizeOptionalString(realtime.model),
				instructions: normalizeOptionalString(realtime.instructions) ?? defaults.realtime.instructions,
				introMessage: typeof realtime.introMessage === "string" ? realtime.introMessage.trim() : defaults.realtime.introMessage,
				agentId: normalizeOptionalString(realtime.agentId),
				toolPolicy: resolveRealtimeVoiceAgentConsultToolPolicy(realtime.toolPolicy, defaults.realtime.toolPolicy),
				providers: resolveProviders(realtime.providers)
			}
		};
	};
	return {
		configSchema: {
			parse: resolveConfig,
			uiHints: {
				defaultMode: {
					label: "Default Mode",
					help: DEFAULT_MODE_HELP
				},
				"chrome.audioBackend": {
					label: "Chrome Audio Backend",
					help: "Auto selects BlackHole 2ch on macOS or PipeWire-Pulse on Linux."
				},
				"chrome.browserProfile": {
					label: "Chrome Profile",
					advanced: true
				},
				"chrome.guestName": { label: "Guest Name" },
				"chrome.waitForInCallMs": {
					label: "Wait For In-Call (ms)",
					advanced: true
				},
				"chrome.audioInputCommand": {
					label: "Audio Input Command",
					advanced: true
				},
				"chrome.audioOutputCommand": {
					label: "Audio Output Command",
					advanced: true
				},
				"chromeNode.node": {
					label: "Chrome Node",
					help: CHROME_NODE_HELP,
					advanced: true
				},
				"realtime.transcriptionProvider": { label: "Realtime Transcription Provider" },
				"realtime.voiceProvider": { label: "Bidi Voice Provider" },
				"realtime.model": {
					label: "Bidi Realtime Model",
					advanced: true
				},
				"realtime.instructions": {
					label: "Realtime Instructions",
					advanced: true
				},
				"realtime.introMessage": { label: "Realtime Intro Message" },
				"realtime.agentId": {
					label: "Realtime Consult Agent",
					advanced: true
				},
				"realtime.toolPolicy": {
					label: "Realtime Tool Policy",
					advanced: true
				}
			}
		},
		defaultAudioInputCommand: defaultAudioRuntime.inputCommand,
		defaultAudioOutputCommand: defaultAudioRuntime.outputCommand,
		resolveConfig,
		resolveGatewayOperationTimeoutMs: (config) => options.resolveGatewayOperationTimeoutMs(config)
	};
}
//#endregion
//#region src/meeting-bot/plugin-entry.ts
function readErrorDetails(error) {
	return error && typeof error === "object" && "details" in error ? error.details : void 0;
}
function createMeetingPluginEntryOptions(options) {
	const invalidRequest = (message) => {
		throw options.invalidRequest(message);
	};
	const normalizeTransport = (value) => {
		if (value === void 0) return;
		if (value === "chrome" || value === "chrome-node") return value;
		return invalidRequest("transport must be chrome or chrome-node");
	};
	const normalizeMode = (value) => {
		if (value === void 0) return;
		if (value === "agent" || value === "bidi" || value === "transcribe") return value;
		return invalidRequest("mode must be agent, bidi, or transcribe");
	};
	const requireString = (value, name) => {
		return normalizeOptionalString(value) ?? invalidRequest(`${name} required`);
	};
	const readSinceIndex = (raw) => {
		try {
			return readNonNegativeIntegerParam(raw, "sinceIndex");
		} catch (error) {
			return invalidRequest(formatErrorMessage(error));
		}
	};
	const keepTrustedToolContext = (raw, client) => {
		const { agentId: rawAgentId, requesterSessionKey: rawRequesterSessionKey, ...rest } = raw;
		const trustedOwner = client?.internal?.pluginRuntimeOwnerId === options.id;
		const agentId = trustedOwner ? normalizeOptionalString(rawAgentId) : void 0;
		const requesterSessionKey = options.normalizeRequesterSessionKey(rawRequesterSessionKey, trustedOwner);
		return {
			...rest,
			...agentId ? { agentId } : {},
			...requesterSessionKey ? { requesterSessionKey } : {}
		};
	};
	const trustedToolAgentId = (raw, client) => normalizeOptionalString(keepTrustedToolContext(raw, client).agentId);
	const joinRequest = (raw, joinOptions) => {
		if (!joinOptions?.allowTimeout && raw.timeoutMs !== void 0) return invalidRequest("timeoutMs is supported only by testSpeech or testListen");
		try {
			return {
				url: options.normalizeUrl(requireString(raw.url, "url")),
				transport: normalizeTransport(raw.transport),
				mode: normalizeMode(raw.mode),
				message: normalizeOptionalString(raw.message),
				requesterSessionKey: normalizeOptionalString(raw.requesterSessionKey),
				agentId: normalizeOptionalString(raw.agentId),
				timeoutMs: readPositiveIntegerParam(raw, "timeoutMs")
			};
		} catch (error) {
			if (options.isInvalidRequest(error)) throw error;
			return invalidRequest(formatErrorMessage(error));
		}
	};
	const gatewayMethod = (action) => `${options.gatewayMethodPrefix}.${action}`;
	const callGatewayFromTool = async (params) => {
		try {
			const timeoutMs = options.resolveGatewayTimeoutMs(params.config);
			if (params.runtime) return await params.runtime.gateway.request(gatewayMethod(params.action), params.raw, {
				timeoutMs,
				scopes: ["operator.admin"]
			});
			return await callGatewayFromCli(gatewayMethod(params.action), {
				json: true,
				timeout: String(timeoutMs)
			}, params.raw, {
				progress: false,
				scopes: ["operator.admin"]
			});
		} catch (error) {
			const details = readErrorDetails(error);
			if (details && typeof details === "object") return details;
			throw error;
		}
	};
	return {
		id: options.id,
		name: options.name,
		description: options.description,
		configSchema: options.configSchema,
		register(api) {
			const config = options.configSchema.parse(api.pluginConfig);
			let runtime;
			const ensureRuntime = async () => {
				if (!config.enabled) throw new Error(options.disabledMessage);
				runtime ??= options.createRuntime({
					api,
					config
				});
				return runtime;
			};
			if (options.transcriptSource) api.registerTranscriptSourceProvider(createMeetingTranscriptSourceProvider({
				...options.transcriptSource,
				runtime: async () => {
					const resolved = await ensureRuntime();
					if (!resolved.startTranscriptSource || !resolved.stopTranscriptSource) throw new Error(`${options.name} transcript source runtime is unavailable`);
					return resolved;
				}
			}));
			const sendError = (respond, error, code = ErrorCodes.UNAVAILABLE) => {
				const payload = { error: formatErrorMessage(error) };
				respond(false, payload, errorShape(code, payload.error, { details: payload }));
			};
			const sendRequestError = (respond, error) => sendError(respond, error, options.isInvalidRequest(error) ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE);
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.join`, async ({ params, client, respond }) => {
				try {
					const raw = keepTrustedToolContext(asOptionalRecord(params) ?? {}, client);
					respond(true, await (await ensureRuntime()).join(joinRequest(raw)));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.leave`, async ({ params, client, respond }) => {
				try {
					const raw = asOptionalRecord(params) ?? {};
					const agentId = trustedToolAgentId(raw, client);
					const sessionId = requireString(raw.sessionId, "sessionId");
					const rt = await ensureRuntime();
					respond(true, agentId && !rt.ownsSession(agentId, sessionId) ? { found: false } : await rt.leave(sessionId));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.status`, async ({ params, client, respond }) => {
				try {
					const raw = asOptionalRecord(params) ?? {};
					const agentId = trustedToolAgentId(raw, client);
					const rt = await ensureRuntime();
					respond(true, agentId ? await rt.statusForAgent(agentId, normalizeOptionalString(raw.sessionId)) : await rt.status(normalizeOptionalString(raw.sessionId)));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.transcript`, async ({ params, client, respond }) => {
				try {
					const raw = asOptionalRecord(params) ?? {};
					const sessionId = requireString(raw.sessionId, "sessionId");
					const sinceIndex = readSinceIndex(raw);
					const agentId = trustedToolAgentId(raw, client);
					const rt = await ensureRuntime();
					respond(true, agentId && !rt.ownsSession(agentId, sessionId) ? { found: false } : await rt.transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.speak`, async ({ params, client, respond }) => {
				try {
					const raw = asOptionalRecord(params) ?? {};
					const sessionId = requireString(raw.sessionId, "sessionId");
					const agentId = trustedToolAgentId(raw, client);
					const rt = await ensureRuntime();
					respond(true, agentId && !rt.ownsSession(agentId, sessionId) ? {
						found: false,
						spoken: false
					} : await rt.speak(sessionId, normalizeOptionalString(raw.message)));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerGatewayMethod(`${options.gatewayMethodPrefix}.setup`, async ({ params, respond }) => {
				try {
					respond(true, await (await ensureRuntime()).setupStatus({
						mode: normalizeMode(params?.mode),
						transport: normalizeTransport(params?.transport)
					}));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			for (const [method, run] of [[`${options.gatewayMethodPrefix}.testSpeech`, (rt, raw) => rt.testSpeech(joinRequest(raw, { allowTimeout: true }))], [`${options.gatewayMethodPrefix}.testListen`, (rt, raw) => rt.testListen(joinRequest(raw, { allowTimeout: true }))]]) api.registerGatewayMethod(method, async ({ params, client, respond }) => {
				try {
					const raw = keepTrustedToolContext(asOptionalRecord(params) ?? {}, client);
					respond(true, await run(await ensureRuntime(), raw));
				} catch (error) {
					sendRequestError(respond, error);
				}
			});
			api.registerTool((toolContext) => ({
				name: options.toolName,
				label: options.toolLabel,
				description: options.toolDescription,
				parameters: options.toolParameters,
				async execute(_toolCallId, params) {
					const raw = asOptionalRecord(params) ?? {};
					const action = raw.action;
					const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
					const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
					const agentId = options.normalizeToolAgentId(contextAgentId);
					try {
						if (![
							"join",
							"leave",
							"status",
							"transcript",
							"speak"
						].includes(action)) throw new Error(options.unknownActionMessage);
						const runtimeForTool = await options.resolveToolRuntime(api, agentId);
						return jsonResult(await callGatewayFromTool({
							action,
							config,
							raw: {
								...raw,
								...requesterSessionKey ? { requesterSessionKey } : {},
								...runtimeForTool && agentId ? { agentId } : {}
							},
							runtime: runtimeForTool
						}));
					} catch (error) {
						return jsonResult({ error: formatErrorMessage(error) });
					}
				}
			}), { name: options.toolName });
			if (options.registerNodeWhen(config)) {
				api.registerNodeHostCommand({
					command: options.nodeCommand,
					cap: options.cap,
					dangerous: true,
					handle: (paramsJSON) => options.nodeHandler(paramsJSON)
				});
				api.registerNodeInvokePolicy(options.createNodePolicy(config));
			}
			options.registerCli(api, config);
		}
	};
}
//#endregion
//#region src/meeting-bot/agent-consult.ts
function resolveMeetingRealtimeTools(policy) {
	return resolveRealtimeVoiceAgentConsultTools(policy);
}
function resolveMeetingAgentConsultSurface(platform) {
	return {
		id: platform.id,
		provider: platform.id,
		lane: platform.id,
		...platform.agentConsult
	};
}
function createMeetingRealtimeEngineBindings(params) {
	const surface = resolveMeetingAgentConsultSurface(params.platform);
	return {
		platform: {
			displayName: params.platform.displayName,
			logScope: params.platform.logScope,
			sessionIdPrefix: params.platform.id
		},
		consultAgent: async (consult) => await consultMeetingAgent({
			surface,
			config: params.fullConfig,
			runtime: params.runtime,
			logger: params.logger,
			agentId: params.config.realtime.agentId,
			toolPolicy: params.config.realtime.toolPolicy,
			...consult
		}),
		tools: resolveMeetingRealtimeTools(params.config.realtime.toolPolicy),
		handleToolCall: async (call) => {
			const abortSignal = readMeetingRealtimeToolAbortSignal(call.session);
			await handleMeetingRealtimeConsultToolCall({
				surface,
				config: params.fullConfig,
				runtime: params.runtime,
				logger: params.logger,
				agentId: params.config.realtime.agentId,
				toolPolicy: params.config.realtime.toolPolicy,
				abortSignal,
				...call
			});
		}
	};
}
async function submitMeetingConsultWorkingResponse(params) {
	if (params.abortSignal?.aborted || !params.session.bridge.supportsToolResultContinuation) return;
	await params.session.submitToolResult(params.callId, buildRealtimeVoiceAgentConsultWorkingResponse(params.label), { willContinue: true });
}
async function consultMeetingAgent(params) {
	const agentId = params.agentId ? normalizeAgentId(params.agentId) : resolveDefaultAgentId(params.config);
	const requesterSessionKey = normalizeOptionalString(params.requesterSessionKey) ?? `agent:${agentId}:main`;
	const sessionKey = `agent:${agentId}:subagent:${params.surface.id}:${params.meetingSessionId}`;
	return await consultRealtimeVoiceAgent({
		cfg: params.config,
		agentRuntime: params.runtime.agent,
		logger: params.logger,
		agentId,
		sessionKey,
		messageProvider: params.surface.provider,
		lane: params.surface.lane,
		runIdPrefix: `${params.surface.id}:${params.meetingSessionId}`,
		spawnedBy: requesterSessionKey,
		contextMode: "fork",
		args: params.args,
		transcript: params.transcript,
		surface: params.surface.surface,
		userLabel: params.surface.userLabel,
		assistantLabel: params.surface.assistantLabel,
		questionSourceLabel: params.surface.questionSourceLabel,
		toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow(params.toolPolicy),
		extraSystemPrompt: params.surface.extraSystemPrompt,
		abortSignal: params.abortSignal
	});
}
async function handleMeetingRealtimeConsultToolCall(params) {
	const callId = params.event.callId || params.event.itemId;
	if (params.abortSignal?.aborted) return;
	if (params.strategy !== "bidi") {
		const error = `Tool "${params.event.name}" is only available in bidi realtime strategy`;
		await params.session.submitToolResult(callId, { error });
		if (params.abortSignal?.aborted) return;
		params.onTalkEvent?.({
			type: "tool.error",
			callId,
			payload: {
				name: params.event.name,
				error
			},
			final: true
		});
		return;
	}
	if (params.event.name !== "openclaw_agent_consult") {
		const error = `Tool "${params.event.name}" not available`;
		await params.session.submitToolResult(callId, { error });
		if (params.abortSignal?.aborted) return;
		params.onTalkEvent?.({
			type: "tool.error",
			callId,
			payload: {
				name: params.event.name,
				error
			},
			final: true
		});
		return;
	}
	await submitMeetingConsultWorkingResponse({
		session: params.session,
		abortSignal: params.abortSignal,
		callId,
		label: params.surface.workingResponseLabel
	});
	if (params.abortSignal?.aborted) return;
	params.onTalkEvent?.({
		type: "tool.progress",
		callId,
		payload: {
			name: params.event.name,
			status: "working"
		}
	});
	let result;
	try {
		result = await consultMeetingAgent({
			surface: params.surface,
			config: params.config,
			runtime: params.runtime,
			logger: params.logger,
			agentId: params.agentId,
			toolPolicy: params.toolPolicy,
			meetingSessionId: params.meetingSessionId,
			requesterSessionKey: params.requesterSessionKey,
			args: params.event.args,
			transcript: params.transcript,
			abortSignal: params.abortSignal
		});
	} catch (error) {
		if (params.abortSignal?.aborted) return;
		const message = formatErrorMessage(error);
		await params.session.submitToolResult(callId, { error: message });
		if (params.abortSignal?.aborted) return;
		params.onTalkEvent?.({
			type: "tool.error",
			callId,
			payload: {
				name: params.event.name,
				error: message
			},
			final: true
		});
		return;
	}
	if (params.abortSignal?.aborted) return;
	await params.session.submitToolResult(callId, result);
	if (params.abortSignal?.aborted) return;
	params.onTalkEvent?.({
		type: "tool.result",
		callId,
		payload: {
			name: params.event.name,
			result
		},
		final: true
	});
}
//#endregion
//#region src/meeting-bot/node-invoke-policy.ts
function readOutputGeneration(value) {
	return asSafeIntegerInRange(value, { min: 0 });
}
function copyCommand(command) {
	return command && command.length > 0 ? [...command] : void 0;
}
function copyConfiguredAudio(target, start) {
	if (start.audioBackend === "auto" || start.audioBackend === "blackhole-2ch" || start.audioBackend === "pipewire-pulse") target.audioBackend = start.audioBackend;
	if (typeof start.audioBufferBytes === "number" && start.audioBufferBytes > 0) target.audioBufferBytes = start.audioBufferBytes;
	if (start.audioFormat === "pcm16-24khz" || start.audioFormat === "g711-ulaw-8khz") target.audioFormat = start.audioFormat;
	const hasCommandOverrideFields = "audioInputCommandOverride" in start || "audioOutputCommandOverride" in start;
	const audioInputCommand = copyCommand(start.audioInputCommandOverride ?? (hasCommandOverrideFields ? void 0 : start.audioInputCommand));
	const audioOutputCommand = copyCommand(start.audioOutputCommandOverride ?? (hasCommandOverrideFields ? void 0 : start.audioOutputCommand));
	if (audioInputCommand) target.audioInputCommand = audioInputCommand;
	if (audioOutputCommand) target.audioOutputCommand = audioOutputCommand;
	for (const key of [
		"bargeInInputCommand",
		"audioBridgeCommand",
		"audioBridgeHealthCommand"
	]) {
		const command = copyCommand(start[key]);
		if (command) target[key] = command;
	}
}
function denied(options, message) {
	return {
		ok: false,
		code: options.deniedCode,
		message
	};
}
function approved(params) {
	return {
		approved: true,
		params
	};
}
function buildStartParams(params, options) {
	let url;
	try {
		url = options.normalizeUrl(params.url);
	} catch (error) {
		return {
			approved: false,
			result: denied(options, error instanceof Error ? error.message : `${options.commandName} start requires url`)
		};
	}
	const mode = readNonEmptyStringPreservingWhitespace(params.mode);
	if (mode && !options.supportedModes.has(mode)) return {
		approved: false,
		result: denied(options, `${options.commandName} start mode is unsupported: ${mode}`)
	};
	const startParams = {
		action: "start",
		url,
		launch: params.launch === false ? false : options.start.launch,
		browserProfile: options.start.browserProfile,
		joinTimeoutMs: options.start.joinTimeoutMs
	};
	if (mode) startParams.mode = mode;
	copyConfiguredAudio(startParams, options.start);
	return approved(startParams);
}
function denyMissing(options, action, field) {
	return {
		approved: false,
		result: denied(options, `${options.commandName} ${action} requires ${field}`)
	};
}
function buildForwardParams(params, options) {
	const action = readNonEmptyStringPreservingWhitespace(params.action);
	switch (action) {
		case "setup": return approved({ action });
		case "status": {
			const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
			return approved(bridgeId ? {
				action,
				bridgeId
			} : { action });
		}
		case "list": {
			const forwarded = { action };
			const url = readNonEmptyStringPreservingWhitespace(params.url);
			const mode = readNonEmptyStringPreservingWhitespace(params.mode);
			if (url) try {
				forwarded.url = options.normalizeUrl(url);
			} catch (error) {
				return {
					approved: false,
					result: denied(options, error instanceof Error ? error.message : `${options.commandName} list url`)
				};
			}
			if (mode) forwarded.mode = mode;
			return approved(forwarded);
		}
		case "stopByUrl": {
			const forwarded = { action };
			const url = readNonEmptyStringPreservingWhitespace(params.url);
			const mode = readNonEmptyStringPreservingWhitespace(params.mode);
			const exceptBridgeId = readNonEmptyStringPreservingWhitespace(params.exceptBridgeId);
			if (!url) return denyMissing(options, action, "url");
			try {
				forwarded.url = options.normalizeUrl(url);
			} catch (error) {
				return {
					approved: false,
					result: denied(options, error instanceof Error ? error.message : `${options.commandName} stopByUrl url`)
				};
			}
			if (mode) forwarded.mode = mode;
			if (exceptBridgeId) forwarded.exceptBridgeId = exceptBridgeId;
			return approved(forwarded);
		}
		case "pullAudio": {
			const forwarded = { action };
			const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
			const timeoutMs = asPositiveFiniteNumber(params.timeoutMs);
			if (!bridgeId) return denyMissing(options, action, "bridgeId");
			forwarded.bridgeId = bridgeId;
			if (timeoutMs) forwarded.timeoutMs = timeoutMs;
			return approved(forwarded);
		}
		case "pushAudio": {
			const forwarded = { action };
			const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
			const base64 = readNonEmptyStringPreservingWhitespace(params.base64);
			if (!bridgeId) return denyMissing(options, action, "bridgeId");
			if (!base64) return denyMissing(options, action, "base64");
			if (!isMeetingAudioBase64(base64)) return {
				approved: false,
				result: denied(options, "base64 must be a valid audio payload")
			};
			forwarded.bridgeId = bridgeId;
			forwarded.base64 = base64;
			const outputGeneration = readOutputGeneration(params.outputGeneration);
			if (params.outputGeneration !== void 0 && outputGeneration === void 0) return {
				approved: false,
				result: denied(options, "outputGeneration must be a non-negative safe integer")
			};
			if (outputGeneration !== void 0) forwarded.outputGeneration = outputGeneration;
			return approved(forwarded);
		}
		case "clearAudio": {
			const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
			if (!bridgeId) return denyMissing(options, action, "bridgeId");
			const outputGeneration = readOutputGeneration(params.outputGeneration);
			if (params.outputGeneration !== void 0 && outputGeneration === void 0) return {
				approved: false,
				result: denied(options, "outputGeneration must be a non-negative safe integer")
			};
			return approved({
				action,
				bridgeId,
				...outputGeneration !== void 0 ? { outputGeneration } : {}
			});
		}
		case "stop": {
			const bridgeId = readNonEmptyStringPreservingWhitespace(params.bridgeId);
			return approved(bridgeId ? {
				action,
				bridgeId
			} : { action });
		}
		default: return null;
	}
}
function createMeetingBrowserNodeInvokePolicy(options) {
	return {
		commands: [options.commandName],
		dangerous: true,
		async handle(ctx) {
			if (ctx.command !== options.commandName) return denied(options, `unsupported ${options.displayName} node command: ${ctx.command}`);
			const params = asOptionalRecord(ctx.params) ?? {};
			const action = readNonEmptyStringPreservingWhitespace(params.action);
			if (action === "setup" && options.useConfiguredSetupCommands) {
				const setupParams = { action };
				copyConfiguredAudio(setupParams, options.start);
				return await ctx.invokeNode({ params: setupParams });
			}
			const decision = action === "start" ? buildStartParams(params, options) : buildForwardParams(params, options) ?? {
				approved: false,
				result: denied(options, `unsupported ${options.commandName} action`)
			};
			if (!decision.approved) return decision.result;
			return await ctx.invokeNode({ params: decision.params });
		}
	};
}
//#endregion
//#region src/meeting-bot/plugin-shell.ts
function createMeetingPluginNodeHostHandler(options) {
	const platform = options.platform;
	return createMeetingConfiguredNodeHost({
		...options,
		commandName: platform.nodeCommandName,
		displayName: platform.displayName,
		browserLabel: platform.browserLabel,
		bridgeIdPrefix: `${platform.session.idPrefix}_node_`,
		talkBackModes: /* @__PURE__ */ new Set(["agent", "bidi"]),
		agentMode: "agent",
		defaultAudio: {
			backend: "auto",
			bufferBytes: 4096,
			format: "pcm16-24khz"
		},
		normalizeUrl: (value) => platform.urls.validateAndNormalize(value),
		normalizeMeetingKey: (value) => platform.urls.normalizeForReuse(value),
		browser: {
			application: "Google Chrome",
			buildProfileArgs: (profile) => ["--args", `--profile-directory=${profile}`],
			openedStatus: "chrome-opened",
			openedNotes: [`${options.browserPageName} page control is handled by OpenClaw browser automation when using chrome-node.`]
		}
	});
}
function createMeetingPluginNodeInvokePolicy(config, options) {
	const platform = options.platform;
	return createMeetingBrowserNodeInvokePolicy({
		commandName: platform.nodeCommandName,
		displayName: platform.displayName,
		deniedCode: options.deniedCode,
		supportedModes: /* @__PURE__ */ new Set([
			"agent",
			"bidi",
			"transcribe"
		]),
		normalizeUrl: (value) => platform.urls.validateAndNormalize(value),
		useConfiguredSetupCommands: true,
		start: config.chrome
	});
}
function createMeetingChromeRuntimeBindings() {
	return {
		createBindings: createMeetingRealtimeEngineBindings,
		createLocalAudioTransport: createLocalMeetingRealtimeAudioTransport,
		createNodeAudioTransport: createNodeMeetingRealtimeAudioTransport,
		startAgentRealtimeEngine: startMeetingAgentRealtimeEngine,
		startRealtimeEngine: startMeetingRealtimeEngine
	};
}
function createMeetingPluginChromeTransport(options) {
	return createMeetingChromeTransport({
		...options,
		browserNodeAdapter: options.platform,
		isRealtimeRouteReady: isMeetingRealtimeRouteReady,
		isTalkBackMode: isMeetingTalkBackMode,
		nodeCommandName: options.platform.nodeCommandName
	});
}
function createMeetingPluginShellEntry(options) {
	const id = options.platform.id;
	const methodPrefix = id.replaceAll("-", "");
	const toolName = id.replaceAll("-", "_");
	const loadCli = createLazyRuntimeModule(() => options.cli.load());
	return createMeetingPluginEntryOptions({
		...options,
		id,
		name: options.platform.displayName,
		cap: id,
		description: `Join ${options.platform.displayName} as a Chrome browser guest`,
		disabledMessage: `${options.platform.displayName} plugin disabled in plugin config`,
		gatewayMethodPrefix: methodPrefix,
		nodeCommand: options.platform.nodeCommandName,
		normalizeUrl: (value) => options.platform.urls.validateAndNormalize(value),
		toolDescription: `Join and manage ${options.browserGuestLabel} browser guests. Guest admission, tenant sign-in, and media permissions may require manual action in the OpenClaw Chrome profile.`,
		toolLabel: options.platform.displayName,
		toolName,
		transcriptSource: {
			...options.transcriptSource,
			name: options.platform.displayName
		},
		unknownActionMessage: `unknown ${toolName} action`,
		createRuntime: ({ api, config }) => new options.runtime({
			config,
			fullConfig: api.config,
			runtime: api.runtime,
			logger: api.logger
		}),
		registerCli: (api, config) => {
			api.registerCli(async ({ program }) => (await loadCli())({
				program,
				config
			}), {
				commands: [options.cli.descriptor.name],
				descriptors: [options.cli.descriptor]
			});
		}
	});
}
function createMeetingPluginTypes() {
	return null;
}
//#endregion
//#region src/meeting-bot/runtime-facade.ts
const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
function createMeetingRuntimeFacade(options) {
	return class MeetingRuntimeFacade {
		#defaultAgentId;
		#sessions;
		#requesterSessionKeys = /* @__PURE__ */ new Map();
		constructor(params) {
			this.params = params;
			this.#defaultAgentId = normalizeAgentId(params.config.realtime.agentId ?? resolveDefaultAgentId(params.fullConfig));
			this.#sessions = new MeetingSessionRuntime({
				logger: params.logger,
				logScope: options.platform.logScope,
				formatError: formatErrorMessage,
				reuseExistingBrowserTab: params.config.chrome.reuseExistingTab,
				waitForInCallMs: params.config.chrome.waitForInCallMs,
				joinTimeoutMs: params.config.chrome.joinTimeoutMs,
				defaultSpeechInstructions: params.config.realtime.introMessage,
				transientSpeechBlockedReasons: /* @__PURE__ */ new Set([
					"not-in-call",
					"browser-unverified",
					options.messages.sessionRuntime.speech.microphoneMutedReason
				]),
				messages: options.messages.sessionRuntime,
				resolveJoin: (request) => this.#resolveJoin(request),
				createSession: ({ request, resolved, createdAt }) => {
					const session = createMeetingSession({
						platform: options.platform,
						config: params.config,
						resolved,
						createdAt
					});
					if (request.requesterSessionKey) this.#requesterSessionKeys.set(session.id, request.requesterSessionKey);
					return session;
				},
				resolveSpeechInstructions: (request) => request.message ?? params.config.realtime.introMessage,
				isBrowserTransport: () => true,
				isTalkBackMode: (mode) => mode === "agent" || mode === "bidi",
				isTranscribeMode: (mode) => mode === "transcribe",
				sameMeetingUrl: (left, right) => options.platform.urls.isSameMeeting(left, right),
				normalizeMeetingUrlForReuse: (url) => options.platform.urls.normalizeForReuse(url),
				getBrowser: (session) => session.chrome ? {
					launched: session.chrome.launched,
					nodeId: session.chrome.nodeId,
					tab: session.chrome.browserTab,
					health: session.chrome.health,
					hasAudioBridge: options.hooks?.isAudioBridgeActive?.(session) ?? Boolean(session.chrome.audioBridge)
				} : void 0,
				setBrowserTab: (session, tab) => {
					if (session.chrome) session.chrome.browserTab = tab;
				},
				setBrowserHealth: (session, health) => {
					if (session.chrome) session.chrome.health = health;
				},
				joinTransport: async ({ request, session, context }) => await this.#joinTransport(request, session, context),
				releaseBrowserTab: async (session) => await this.#releaseBrowserTab(session),
				refreshBrowserHealth: async (session, refreshOptions) => await this.#refreshBrowserHealth(session, refreshOptions),
				refreshStatus: async (session) => await this.#refreshStatus(session),
				refreshReusableSession: async (session, request) => await options.hooks?.refreshReusableSession?.(session, request, this.#hookContext()),
				ensureRealtimeBridge: async (session) => await this.#ensureRealtimeBridge(session),
				captureTranscript: async (session, captureOptions) => await this.#captureTranscript(session, captureOptions),
				speakViaTransport: async () => void 0,
				durableTranscripts: {
					config: params.fullConfig.transcripts,
					...options.messages.durableTranscripts
				}
			});
		}
		list() {
			return this.#sessions.list();
		}
		async startTranscriptSource(request) {
			return await this.#sessions.startTranscriptSource(request);
		}
		async stopTranscriptSource(request) {
			return await this.#sessions.stopTranscriptSource(request);
		}
		ownsSession(agentId, sessionId) {
			return this.list().some((session) => session.id === sessionId && session.agentId === agentId);
		}
		async join(request) {
			try {
				return await this.#sessions.join(options.hooks?.normalizeJoinRequest?.(request, this.#hookContext()) ?? request);
			} catch (error) {
				const activeIds = new Set(this.list().map((session) => session.id));
				for (const sessionId of this.#requesterSessionKeys.keys()) if (!activeIds.has(sessionId)) this.#requesterSessionKeys.delete(sessionId);
				throw error;
			}
		}
		async leave(sessionId) {
			try {
				return await this.#sessions.leave(sessionId);
			} finally {
				this.#requesterSessionKeys.delete(sessionId);
			}
		}
		async status(sessionId) {
			return await this.#sessions.status(sessionId);
		}
		async statusForAgent(agentId, sessionId) {
			if (sessionId) return this.ownsSession(agentId, sessionId) ? await this.#sessions.status(sessionId) : { found: false };
			const sessions = this.list().filter((session) => session.agentId === agentId);
			await Promise.all(sessions.map((session) => this.#sessions.status(session.id)));
			return {
				found: true,
				sessions
			};
		}
		async transcript(sessionId, transcriptOptions = {}) {
			return await this.#sessions.transcript(sessionId, transcriptOptions);
		}
		async speak(sessionId, instructions) {
			return await this.#sessions.speak(sessionId, instructions);
		}
		async setupStatus(setupOptions) {
			return await options.probes.setupStatus({
				...this.params,
				options: setupOptions
			});
		}
		async testSpeech(request) {
			return await options.probes.testSpeech(this.#probeContext(), request);
		}
		async testListen(request) {
			return await options.probes.testListening(this.#probeContext(), request);
		}
		#resolveJoin(request) {
			return {
				url: options.platform.urls.validateAndNormalize(request.url),
				transport: request.transport ?? (this.params.config.chromeNode.node ? "chrome-node" : "chrome"),
				mode: request.mode ?? this.params.config.defaultMode,
				agentId: normalizeAgentId(request.agentId ?? this.#defaultAgentId)
			};
		}
		#hookContext() {
			return {
				deleteRequesterSessionKey: (sessionId) => this.#requesterSessionKeys.delete(sessionId),
				endSession: async (sessionId, leaveOptions) => {
					await this.#sessions.leave(sessionId, leaveOptions);
					this.#requesterSessionKeys.delete(sessionId);
				},
				noteSession: (session, note) => this.#noteSession(session, note),
				refreshBrowserHealth: async (session, refreshOptions) => await this.#sessions.refreshBrowserHealth(session, refreshOptions),
				resolvedJoin: (request) => this.#resolveJoin(request)
			};
		}
		#probeContext() {
			return {
				config: this.params.config,
				resolveAgentId: (request) => normalizeAgentId(request.agentId ?? this.#defaultAgentId),
				list: () => this.list(),
				join: async (request) => await this.join(request),
				isReusable: (session, resolved) => this.#sessions.isReusableSession(session, resolved),
				hasHealthHandle: (sessionId) => this.#sessions.hasHealthHandle(sessionId),
				refreshHealth: (sessionId) => this.#sessions.refreshHealth(sessionId),
				refreshCaptionHealth: async (session, timeoutMs) => await this.#refreshBrowserHealth(session, { timeoutMs })
			};
		}
		async #joinTransport(request, session, context) {
			const config = this.#withSessionAgentConfig(session.agentId);
			const launchParams = {
				runtime: this.params.runtime,
				config,
				fullConfig: this.params.fullConfig,
				meetingSessionId: session.id,
				requesterSessionKey: request.requesterSessionKey,
				mode: session.mode,
				url: session.url,
				logger: this.params.logger
			};
			const result = session.transport === "chrome-node" ? await options.transport.launchOnNode(launchParams) : await options.transport.launchInChrome(launchParams);
			const nodeId = result.nodeId;
			const tab = context.inheritedBrowserTab({
				session,
				transport: session.transport,
				nodeId,
				meetingUrl: session.url,
				tab: result.tab
			});
			session.chrome = {
				audioBackend: result.audioBackend,
				launched: result.launched,
				nodeId,
				browserProfile: this.params.config.chrome.browserProfile,
				browserTab: tab,
				health: result.browser
			};
			options.hooks?.validateLaunchResult?.(result);
			const handles = this.#attachAudioBridge(session, result.audioBridge);
			if (handles) context.attachRuntimeHandles(session, handles);
			session.notes.push(result.audioBridge ? session.transport === "chrome-node" ? options.messages.joined.node : options.messages.joined.local : session.mode === "transcribe" ? options.messages.joined.transcribe : options.messages.joined.waiting);
			this.#sessions.refreshSpeechReadiness(session);
			return {};
		}
		#attachAudioBridge(session, audioBridge) {
			if (!session.chrome || !audioBridge) return;
			session.chrome.audioBridge = {
				type: audioBridge.type,
				provider: audioBridge.providerId
			};
			options.hooks?.afterAudioBridgeAttached?.(session);
			return {
				stop: audioBridge.stop,
				speak: audioBridge.speak,
				getHealth: audioBridge.getHealth
			};
		}
		async #ensureRealtimeBridge(session) {
			const audioBridgeActive = options.hooks?.isAudioBridgeActive?.(session) ?? Boolean(session.chrome?.audioBridge);
			if (session.mode !== "agent" && session.mode !== "bidi" || session.state !== "active" || !session.chrome || audioBridgeActive || !isMeetingRealtimeRouteReady(session.mode, session.chrome.health)) return;
			if (session.chrome.audioBridge) session.chrome.audioBridge = void 0;
			const config = this.#withSessionAgentConfig(session.agentId);
			const recoveryConfig = {
				...config,
				chrome: {
					...config.chrome,
					launch: false
				},
				chromeNode: { node: session.chrome.nodeId ?? config.chromeNode.node }
			};
			const launchParams = {
				runtime: this.params.runtime,
				config: recoveryConfig,
				fullConfig: this.params.fullConfig,
				meetingSessionId: session.id,
				requesterSessionKey: this.#requesterSessionKeys.get(session.id),
				mode: session.mode,
				trackedTargetId: session.chrome.browserTab?.targetId,
				url: session.url,
				logger: this.params.logger
			};
			const result = session.transport === "chrome-node" ? await options.transport.launchOnNode(launchParams) : await options.transport.launchInChrome(launchParams);
			if (result.tab) {
				const currentTab = session.chrome.browserTab;
				session.chrome.browserTab = {
					...result.tab,
					openedByPlugin: result.tab.targetId === currentTab?.targetId ? currentTab.openedByPlugin : result.tab.openedByPlugin
				};
			}
			if (result.browser) session.chrome.health = {
				...session.chrome.health,
				...result.browser
			};
			session.updatedAt = nowIso();
			return this.#attachAudioBridge(session, result.audioBridge);
		}
		async #refreshStatus(session) {
			await this.#sessions.refreshBrowserHealth(session, {
				force: true,
				readOnly: !(options.hooks?.isAwaitingAdmission?.(session) ?? false)
			});
			await options.hooks?.afterStatusRefresh?.(session, this.#hookContext());
		}
		async #refreshBrowserHealth(session, refreshOptions = {}) {
			try {
				const result = await options.transport.recoverCurrentTab({
					runtime: this.params.runtime,
					config: this.params.config,
					fullConfig: this.params.fullConfig,
					meetingSessionId: session.id,
					mode: session.mode,
					nodeId: session.chrome?.nodeId,
					readOnly: refreshOptions.readOnly,
					trackedMeetingUrl: session.url,
					trackedTargetId: session.chrome?.browserTab?.targetId,
					transport: session.transport,
					timeoutMs: refreshOptions.timeoutMs,
					url: session.url
				});
				if (result.found && session.chrome) {
					if (result.tab?.targetId) {
						const currentTab = session.chrome.browserTab;
						session.chrome.browserTab = {
							targetId: result.tab.targetId,
							openedByPlugin: result.tab.targetId === currentTab?.targetId ? currentTab.openedByPlugin : false
						};
					}
					if (result.browser) session.chrome.health = {
						...session.chrome.health,
						...result.browser
					};
					session.updatedAt = nowIso();
				} else if (session.chrome) options.hooks?.recordBrowserRecoveryFailure?.(session, {
					kind: "missing",
					message: result.message
				});
			} catch (error) {
				const formattedError = formatErrorMessage(error);
				const message = options.messages.browserReadinessFailed?.(formattedError) ?? formattedError;
				if (options.hooks?.recordBrowserRecoveryFailure) {
					this.params.logger.debug?.(`${options.platform.logScope} ${message}`);
					options.hooks.recordBrowserRecoveryFailure(session, {
						kind: "error",
						message
					});
				} else this.params.logger.debug?.(`${options.platform.logScope} browser readiness refresh ignored: ${formatErrorMessage(error)}`);
			}
		}
		async #captureTranscript(session, captureOptions = {}) {
			await this.#sessions.refreshCaptionHealth(session);
			const tab = session.chrome?.browserTab;
			if (!tab) return;
			return await options.transport.readTranscript({
				runtime: this.params.runtime,
				config: this.params.config,
				finalize: captureOptions.finalize,
				meetingUrl: session.url,
				meetingSessionId: session.id,
				nodeId: session.chrome?.nodeId,
				tab
			});
		}
		async #releaseBrowserTab(session) {
			const tab = session.chrome?.browserTab;
			if (!tab) {
				this.#noteSession(session, options.messages.noTrackedTab);
				session.browserLeft = false;
				return false;
			}
			if (this.list().some((other) => other.id !== session.id && other.state === "active" && other.chrome?.browserTab?.targetId === tab.targetId && other.chrome?.nodeId === session.chrome?.nodeId)) {
				this.#noteSession(session, options.messages.sharedTab);
				return;
			}
			try {
				const result = await options.transport.leaveInBrowser({
					runtime: this.params.runtime,
					config: this.params.config,
					meetingSessionId: session.id,
					meetingUrl: session.url,
					nodeId: session.chrome?.nodeId,
					tab
				});
				this.#noteSession(session, result.note);
				if (result.left && session.chrome) {
					session.chrome.browserTab = void 0;
					if (session.chrome.health) session.chrome.health = {
						...session.chrome.health,
						captioning: false,
						audioInputRouted: false,
						audioOutputRouted: false,
						providerConnected: false,
						realtimeReady: false,
						audioInputActive: false,
						audioOutputActive: false
					};
				}
				session.browserLeft = result.left;
				return result.left;
			} catch (error) {
				this.#noteSession(session, options.messages.leaveFailed(formatErrorMessage(error)));
				session.browserLeft = false;
				return false;
			}
		}
		#withSessionAgentConfig(agentId) {
			return this.params.config.realtime.agentId === agentId ? this.params.config : {
				...this.params.config,
				realtime: {
					...this.params.config.realtime,
					agentId
				}
			};
		}
		#noteSession(session, note) {
			session.notes = [...session.notes.filter((item) => item !== note), note];
		}
	};
}
//#endregion
//#region src/meeting-bot/runtime-probes.ts
function resolveMeetingProbeTimeoutMs(input, fallback, invalidRequest = (message) => new Error(message)) {
	if (input === void 0) return Math.min(Math.max(fallback, 1), 12e4);
	if (!Number.isFinite(input) || input <= 0) throw invalidRequest("timeoutMs must be a positive number");
	return Math.min(Math.trunc(input), 12e4);
}
function createMeetingRuntimeProbes(options) {
	const testSpeech = async (context, request) => {
		const requestMode = options.resolveRequestMode?.(request.mode, context.config) ?? request.mode;
		if (requestMode === "transcribe") throw options.invalidRequest(options.speechModeError ?? "test_speech requires mode: agent or bidi");
		const requestedMode = requestMode ?? context.config.defaultMode;
		const mode = options.talkBackMode(requestedMode) ? requestedMode : "agent";
		const resolved = {
			url: options.normalizeUrl?.(request.url) ?? request.url,
			transport: request.transport ?? options.defaultTransport?.(context.config) ?? (context.config.chromeNode.node ? "chrome-node" : "chrome"),
			mode,
			agentId: context.resolveAgentId(request)
		};
		const beforeSessions = context.list();
		const before = new Set(beforeSessions.map((session) => session.id));
		const existing = beforeSessions.find((session) => context.isReusable(session, resolved));
		const existingBaseline = {
			outputBytes: existing?.chrome?.health?.lastOutputBytes ?? 0,
			outputGeneration: existing?.chrome?.health?.outputGeneration ?? 0
		};
		const result = await context.join({
			...request,
			...resolved,
			message: request.message ?? options.defaultSpeechMessage
		});
		const baseline = existing?.id === result.session.id ? existingBaseline : {
			outputBytes: 0,
			outputGeneration: 0
		};
		let health = result.session.chrome?.health;
		const verified = () => (health?.lastOutputBytes ?? 0) > baseline.outputBytes && (health?.outputGeneration ?? 0) > baseline.outputGeneration && health?.verifiedOutputGeneration === health?.outputGeneration;
		const shouldWait = result.spoken === true && health?.manualAction === void 0 && context.hasHealthHandle(result.session.id);
		if (shouldWait && !verified()) {
			const deadline = Date.now() + (options.resolveSpeechTimeoutMs?.(request, context.config) ?? options.resolveTimeoutMs(request.timeoutMs, context.config.chrome.joinTimeoutMs));
			while (Date.now() < deadline && !verified()) {
				await sleep(100);
				context.refreshHealth(result.session.id);
				health = result.session.chrome?.health;
			}
		}
		const speechOutputVerified = verified();
		return {
			createdSession: !before.has(result.session.id),
			inCall: health?.inCall,
			manualAction: health?.manualAction,
			spoken: result.spoken ?? false,
			speechOutputVerified,
			speechOutputTimedOut: shouldWait && !speechOutputVerified,
			speechReady: health?.speechReady,
			speechBlockedReason: health?.speechBlockedReason,
			speechBlockedMessage: health?.speechBlockedMessage,
			audioOutputActive: health?.audioOutputActive,
			lastOutputBytes: health?.lastOutputBytes,
			outputLoopbackSignalBytes: health?.outputLoopbackSignalBytes,
			lastOutputLoopbackAt: health?.lastOutputLoopbackAt,
			lastOutputLoopbackCorrelation: health?.lastOutputLoopbackCorrelation,
			lastOutputLoopbackRms: health?.lastOutputLoopbackRms,
			lastOutputLoopbackPeak: health?.lastOutputLoopbackPeak,
			outputGeneration: health?.outputGeneration,
			verifiedOutputGeneration: health?.verifiedOutputGeneration,
			session: result.session
		};
	};
	const testListening = async (context, request) => {
		const requestMode = options.resolveRequestMode?.(request.mode, context.config) ?? request.mode;
		if (requestMode && requestMode !== "transcribe") throw options.invalidRequest(options.listeningModeError ?? "test_listen requires mode: transcribe");
		const resolved = {
			url: options.normalizeUrl?.(request.url) ?? request.url,
			transport: request.transport ?? options.defaultTransport?.(context.config) ?? (context.config.chromeNode.node ? "chrome-node" : "chrome"),
			mode: "transcribe",
			agentId: context.resolveAgentId(request)
		};
		options.validateListeningTransport?.(resolved.transport);
		const beforeSessions = context.list();
		const before = new Set(beforeSessions.map((session) => session.id));
		const existing = beforeSessions.find((session) => context.isReusable(session, resolved));
		const start = {
			lines: existing?.chrome?.health?.transcriptLines ?? 0,
			at: existing?.chrome?.health?.lastCaptionAt,
			text: existing?.chrome?.health?.lastCaptionText
		};
		const result = await context.join({
			...request,
			...resolved,
			message: void 0
		});
		let health = result.session.chrome?.health;
		const advanced = () => (health?.transcriptLines ?? 0) > (existing?.id === result.session.id ? start.lines : 0) || Boolean(health?.lastCaptionAt && health.lastCaptionAt !== start.at) || Boolean(health?.lastCaptionText && health.lastCaptionText !== start.text);
		const shouldWait = health?.manualAction === void 0 && options.shouldWaitForListening(result.session);
		let listenVerified = advanced();
		if (shouldWait && !listenVerified) {
			const deadline = Date.now() + options.resolveTimeoutMs(request.timeoutMs, context.config.chrome.joinTimeoutMs);
			while (Date.now() < deadline) {
				const remainingMs = deadline - Date.now();
				if (remainingMs <= 0) break;
				let deadlineTimer;
				const deadlineReached = new Promise((resolve) => {
					deadlineTimer = setTimeout(() => resolve(false), remainingMs);
				});
				if (!await Promise.race([(options.refreshCaptionHealth?.(context, result.session, remainingMs) ?? context.refreshCaptionHealth(result.session, remainingMs)).then(() => true), deadlineReached]).finally(() => {
					if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
				})) break;
				health = result.session.chrome?.health;
				if (Date.now() >= deadline) break;
				if (advanced()) listenVerified = true;
				if (listenVerified || health?.manualAction) break;
				const retryDelayMs = deadline - Date.now();
				if (retryDelayMs <= 0) break;
				await sleep(Math.min(250, retryDelayMs));
			}
		}
		return {
			createdSession: !before.has(result.session.id),
			inCall: health?.inCall,
			manualAction: health?.manualAction,
			listenVerified,
			listenTimedOut: shouldWait && !listenVerified && health?.manualAction === void 0,
			captioning: health?.captioning,
			captionsEnabledAttempted: health?.captionsEnabledAttempted,
			transcriptLines: health?.transcriptLines,
			lastCaptionAt: health?.lastCaptionAt,
			lastCaptionSpeaker: health?.lastCaptionSpeaker,
			lastCaptionText: health?.lastCaptionText,
			recentTranscript: health?.recentTranscript,
			session: result.session
		};
	};
	return {
		testListening,
		testSpeech
	};
}
//#endregion
//#region src/meeting-bot/setup-checks.ts
function createMeetingSetupStatus(checks) {
	return {
		ok: checks.every((check) => check.ok),
		checks
	};
}
function addMeetingSetupCheck(status, check) {
	return createMeetingSetupStatus([...status.checks, check]);
}
//#endregion
//#region src/meeting-bot/runtime-setup.ts
async function commandExists(runtime, command) {
	return (await runtime.system.runCommandWithTimeout([
		"/bin/sh",
		"-lc",
		"command -v \"$1\" >/dev/null 2>&1",
		"sh",
		command
	], { timeoutMs: 5e3 })).code === 0;
}
function createMeetingRuntimeSetup(options) {
	return async (params) => {
		const mode = params.options?.mode ?? params.config.defaultMode;
		const transport = params.options?.transport ?? (params.config.chromeNode.node ? "chrome-node" : "chrome");
		const talkBack = isMeetingTalkBackMode(mode);
		const guestJoin = options.guestJoinCheck(params.config);
		let status = createMeetingSetupStatus([
			{
				id: "chrome-profile",
				ok: true,
				message: params.config.chrome.browserProfile ? `Chrome node profile configured: ${params.config.chrome.browserProfile}` : "Local Chrome uses the configured OpenClaw browser profile"
			},
			{
				id: "guest-join",
				...guestJoin
			},
			{
				id: "captions",
				ok: true,
				message: options.captionsMessage(mode)
			}
		]);
		if (transport === "chrome-node") try {
			const node = await resolveMeetingBrowserNodeInfo({
				runtime: params.runtime,
				requestedNode: params.config.chromeNode.node,
				adapter: options.nodeAdapter
			});
			status = addMeetingSetupCheck(status, {
				id: "chrome-node-connected",
				ok: true,
				message: options.connectedNodeMessage(node.displayName ?? node.remoteIp ?? node.nodeId)
			});
			if (talkBack) {
				if (!node.nodeId) throw new Error(options.missingNodeIdMessage);
				await params.runtime.nodes.invoke({
					nodeId: node.nodeId,
					command: options.nodeAdapter.nodeCommandName,
					params: {
						action: "setup",
						audioBackend: params.config.chrome.audioBackend,
						audioFormat: params.config.chrome.audioFormat,
						audioBufferBytes: params.config.chrome.audioBufferBytes,
						...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
						...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {},
						...params.config.chrome.bargeInInputCommand ? { bargeInInputCommand: params.config.chrome.bargeInInputCommand } : {}
					},
					timeoutMs: 12e3
				});
				status = addMeetingSetupCheck(status, {
					id: "chrome-node-audio-prerequisites",
					ok: true,
					message: "Remote virtual audio backend and command-pair prerequisites are ready"
				});
			}
		} catch (error) {
			const connected = status.checks.some((check) => check.id === "chrome-node-connected" && check.ok);
			status = addMeetingSetupCheck(status, {
				id: connected ? "chrome-node-audio-prerequisites" : "chrome-node-connected",
				ok: false,
				message: formatErrorMessage(error)
			});
		}
		if (!talkBack) return status;
		status = addMeetingSetupCheck(status, {
			id: "audio-bridge",
			ok: true,
			message: `Command-pair audio bridge configured (${params.config.chrome.audioFormat})`
		});
		if (transport === "chrome-node") return status;
		let audio;
		try {
			audio = resolveMeetingAudioRuntimeForFormat({
				backend: params.config.chrome.audioBackend,
				bufferBytes: params.config.chrome.audioBufferBytes,
				format: params.config.chrome.audioFormat,
				inputCommand: params.config.chrome.audioInputCommandOverride,
				outputCommand: params.config.chrome.audioOutputCommandOverride
			});
			await options.assertAudioDeviceAvailable({
				config: params.config,
				runtime: params.runtime,
				timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
			});
			status = addMeetingSetupCheck(status, {
				id: "chrome-local-audio-device",
				ok: true,
				message: "Virtual meeting audio backend is ready"
			});
		} catch (error) {
			status = addMeetingSetupCheck(status, {
				id: "chrome-local-audio-device",
				ok: false,
				message: formatErrorMessage(error)
			});
			if (!audio) return status;
		}
		const commands = uniqueStrings([
			audio.inputCommand[0],
			audio.outputCommand[0],
			params.config.chrome.bargeInInputCommand?.[0]
		].filter((value) => Boolean(value?.trim())));
		const missing = [];
		for (const command of commands) if (!await commandExists(params.runtime, command).catch(() => false)) missing.push(command);
		return addMeetingSetupCheck(status, {
			id: "chrome-local-audio-commands",
			ok: missing.length === 0,
			message: missing.length === 0 ? "Configured Chrome audio commands are available" : `Chrome audio commands missing: ${missing.join(", ")}`
		});
	};
}
//#endregion
//#region src/meeting-bot/status-call-source.ts
function createMeetingStatusCallSource(options) {
	const captionSettleMs = options.captionSettleMs ?? 1e3;
	const audioOutputsGlobal = JSON.stringify(options.platform.globals.audioOutputs);
	const captionsGlobal = JSON.stringify(options.platform.globals.captions);
	const meetingGlobal = JSON.stringify(options.platform.globals.meeting);
	const transcriptMaxLines = options.transcriptMaxLines ?? 500;
	return `  let audioOutputRouted;
  let audioOutputDeviceLabel;
  let audioOutputRouteError;
  let audioOutputRouteRetryable = false;
  if (inCall && allowMicrophone && navigator.mediaDevices?.enumerateDevices) {
    const media = [...document.querySelectorAll("audio, video")].filter(
      (element) =>
        typeof element.setSinkId === "function" &&
        !String(element.id || "").startsWith(${JSON.stringify(options.platform.audioOutputElementIdPrefix)}),
    );
    if (media.length > 0) {
      try {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const output = devices.find(
          (device) => device.kind === "audiooutput" && isVirtualAudioDevice(device.label)
        );
        if (output?.deviceId) {
          const routeErrors = [];
          const liveStream = (element) =>
            element.srcObject?.getAudioTracks?.().some((track) => track.readyState === "live")
              ? element.srcObject
              : undefined;
          const allBridgeEntries = Array.isArray(window[${audioOutputsGlobal}])
            ? window[${audioOutputsGlobal}]
            : [];
          const retainedBridgeEntries = allBridgeEntries.filter((entry) => !bridgeOwnedBySession(entry));
          const previousBridgeEntries = allBridgeEntries.filter(bridgeOwnedBySession);
          const originalMuteBySource = new Map(previousBridgeEntries.flatMap((entry) =>
            bridgeSources(entry).flatMap((source) =>
              source?.element ? [[source.element, Boolean(source.muted)]] : []
            )
          ));
          const bridgedElements = new Set(previousBridgeEntries.flatMap((entry) =>
            bridgeSources(entry).map((source) => source?.element).filter(Boolean)
          ));
          const routeCandidates = media
            .map((element) => ({ element, stream: liveStream(element) }))
            // Teams mutes local/self-view and intentionally suppressed playback. Preserve
            // that product decision; only our own already-bridged source stays eligible.
            .filter((entry) => !entry.element.muted || bridgedElements.has(entry.element));
          // The self-view often exists before Teams attaches remote playback. With the
          // required output present, an all-filtered list is still a transient DOM state.
          if (routeCandidates.length === 0) audioOutputRouteRetryable = true;
          if (canMutateSession) {
            for (const { element } of routeCandidates) {
              if (!originalMuteBySource.has(element)) {
                originalMuteBySource.set(element, Boolean(element.muted));
              }
              // Sink changes are asynchronous. Silence the physical output until either
              // the source or its fallback bridge is confirmed on the virtual device.
              element.muted = true;
            }
          }
          const currentSources = new Set(routeCandidates.map((entry) => entry.element));
          const bridgeEntries = previousBridgeEntries.filter((entry) =>
            entry?.source &&
            entry?.stream === liveStream(entry.source) &&
            entry?.bridge?.isConnected &&
            currentSources.has(entry.source)
          );
          const suspendedBySource = new Map();
          for (const entry of previousBridgeEntries) {
            if (bridgeEntries.includes(entry)) continue;
            for (const source of bridgeSources(entry)) {
              if (
                !source?.element ||
                source.muted ||
                !bridgeSourceMatches(source.element, source)
              ) continue;
              const sourceStillPresent = currentSources.has(source.element);
              const detachedLiveSource = !sourceStillPresent && Boolean(liveStream(source.element));
              if (!sourceStillPresent && !detachedLiveSource) continue;
              suspendedBySource.set(source.element, {
                detached: detachedLiveSource,
                sessionId: entry.sessionId || sessionId,
                source: source.element,
                sourceMuted: false,
                sourceUrl: mediaSourceUrl(source.element) || source.url,
                stream: source.element.srcObject,
                suspended: true,
              });
            }
          }
          if (canMutateSession) {
            // One bridge owns one Teams playback element. Stream or element replacement
            // retires that bridge so it cannot keep playing or satisfy route verification.
            previousBridgeEntries.filter((entry) => !bridgeEntries.includes(entry)).forEach((entry) => {
              for (const source of bridgeSources(entry)) {
                if (
                  !source?.element ||
                  suspendedBySource.has(source.element) ||
                  currentSources.has(source.element)
                ) continue;
                restoreAudioBridgeSource(source);
              }
              // Reused current elements stay silent until this pass confirms their
              // replacement source; unrelated exact sources were restored above.
              retireAudioBridge(entry, false);
            });
          }
          const routed = [];
          for (const { element, stream } of routeCandidates) {
            let entry = bridgeEntries.find((candidate) => candidate.source === element);
            let elementRouted = element.sinkId === output.deviceId;
            let directRouteError;
            if (canMutateSession && !elementRouted) {
              try {
                await element.setSinkId(output.deviceId);
                elementRouted = element.sinkId === output.deviceId;
              } catch (error) {
                directRouteError = {
                  message: error?.message || String(error),
                  retryable: error?.name === "AbortError",
                };
              }
            }
            if (elementRouted && entry && canMutateSession) {
              const bridgedIndex = bridgeEntries.indexOf(entry);
              if (bridgedIndex >= 0) {
                const [bridged] = bridgeEntries.splice(bridgedIndex, 1);
                retireAudioBridge(bridged);
                entry = undefined;
              }
            }
            // Direct sink routing is valid for src/MediaSource and pre-attachment elements.
            // A live MediaStream is required only when the hidden bridge fallback is needed.
            if (elementRouted) {
              if (canMutateSession && originalMuteBySource.has(element)) {
                element.muted = originalMuteBySource.get(element);
              }
              suspendedBySource.delete(element);
              routed.push(true);
              continue;
            }
            if (!stream) {
              const hasLoadedPlaybackSource = Number(element.readyState) > 0;
              routed.push(false);
              if (hasLoadedPlaybackSource && directRouteError) routeErrors.push(directRouteError);
              if (!hasLoadedPlaybackSource) audioOutputRouteRetryable = true;
              if (canMutateSession && originalMuteBySource.get(element) === false) {
                // Teams may attach the remote MediaStream after creating its media element.
                // Keep it silent until a later serialized status poll routes that source.
                suspendedBySource.set(element, {
                  sessionId,
                  pending: true,
                  source: element,
                  sourceMuted: false,
                  sourceUrl: mediaSourceUrl(element),
                  stream: element.srcObject,
                  suspended: true,
                });
              }
              continue;
            }
            if (!elementRouted && stream) {
              if (!entry && canMutateSession) {
                const bridge = document.createElement("audio");
                bridge.id = ${JSON.stringify(options.platform.audioOutputElementIdPrefix)} + bridgeEntries.length;
                bridge.autoplay = false;
                bridge.hidden = true;
                bridge.srcObject = stream;
                document.body.appendChild(bridge);
                entry = {
                  bridge,
                  playing: false,
                  sessionId,
                  source: element,
                  sourceMuted: originalMuteBySource.has(element)
                    ? originalMuteBySource.get(element)
                    : Boolean(element.muted),
                  sourceUrl: mediaSourceUrl(element),
                  stream,
                };
                bridgeEntries.push(entry);
                suspendedBySource.delete(element);
              }
              if (entry?.bridge) {
                try {
                  if (canMutateSession) {
                    if (entry.bridge.sinkId !== output.deviceId) {
                      await entry.bridge.setSinkId(output.deviceId);
                    }
                    await entry.bridge.play();
                    entry.playing = true;
                  }
                  elementRouted =
                    entry.bridge.sinkId === output.deviceId && entry.playing === true;
                  if (elementRouted) {
                    suspendedBySource.delete(element);
                    if (canMutateSession && !entry.sourceMuted) element.muted = true;
                  }
                } catch (error) {
                  entry.playing = false;
                  if (canMutateSession) retireAudioBridge(entry, false);
                  routeErrors.push({
                    message: error?.message || String(error),
                    retryable: error?.name === "AbortError",
                  });
                }
              }
            }
            routed.push(elementRouted);
          }
          if (canMutateSession) {
            const nextBridgeEntries = [
              ...retainedBridgeEntries,
              ...bridgeEntries,
              ...suspendedBySource.values(),
            ];
            if (nextBridgeEntries.length > 0) {
              window[${audioOutputsGlobal}] = nextBridgeEntries;
            } else {
              delete window[${audioOutputsGlobal}];
            }
          }
          audioOutputRouted = routed.length > 0 && routed.every(Boolean);
          if (canMutateSession && !audioOutputRouted) suspendOwnedAudioBridges();
          if (audioOutputRouted && bridgeEntries.length > 0) {
            notes.push(
              "Routed ${options.platform.displayName} remote audio to " +
              (output.label || "the virtual audio device") +
              " through MediaStream bridges."
            );
          }
          audioOutputDeviceLabel = output.label || "Virtual audio device";
          // An unloaded Teams media element can reject setSinkId before its stream
          // arrives. Keep that state retryable; loaded-source failures are terminal.
          if (!audioOutputRouted && routed.length > 0 && routeErrors.length > 0) {
            audioOutputRouteError = routeErrors[routeErrors.length - 1]?.message;
            audioOutputRouteRetryable = routeErrors.every((error) => error.retryable === true);
          }
        } else {
          audioOutputRouted = false;
          if (canMutateSession) suspendOwnedAudioBridges();
          notes.push("The OpenClaw virtual audio speaker output was not visible to ${options.platform.displayName}.");
        }
      } catch (error) {
        audioOutputRouted = false;
        audioOutputRouteError = error?.message || String(error);
        if (canMutateSession) suspendOwnedAudioBridges();
      }
      if (!audioOutputRouted && audioOutputRouteError) {
        notes.push("Could not route ${options.platform.displayName} speaker output to the OpenClaw virtual audio device: " + audioOutputRouteError);
      }
    } else {
      audioOutputRouted = false;
      try {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const output = devices.find(
          (device) => device.kind === "audiooutput" && isVirtualAudioDevice(device.label)
        );
        if (output?.deviceId) {
          // Teams can briefly remove every media element during an in-call rerender.
          // Retry only after proving the required output still exists.
          audioOutputRouteRetryable = true;
          audioOutputDeviceLabel = output.label || "Virtual audio device";
        } else {
          notes.push("The OpenClaw virtual audio speaker output was not visible to ${options.platform.displayName}.");
        }
      } catch (error) {
        audioOutputRouteError = error?.message || String(error);
        notes.push("Could not inspect ${options.platform.displayName} speaker outputs: " + audioOutputRouteError);
      }
      // Suspend ownership until the source returns; call teardown retires it.
      if (canMutateSession) suspendOwnedAudioBridges();
    }
  } else if (inCall && allowMicrophone) {
    audioOutputRouted = false;
    if (canMutateSession) retireOwnedAudioBridges();
  }
  let captioning = false;
  let captionsEnabledAttempted = false;
  let transcriptLines = 0;
  let lastCaptionAt;
  let lastCaptionSpeaker;
  let lastCaptionText;
  let recentTranscript = [];
  const captionState = (() => {
    let active = window[${captionsGlobal}];
    const activeOwnedByRequest = Boolean(
      !active || (sessionId && (!active.sessionId || active.sessionId === sessionId))
    );
    if (!identityVerified) {
      if (identityAwaitingRerender && activeOwnedByRequest) return active;
      if (canMutateSession && activeOwnedByRequest) finalizeOwnedCaptions();
      return undefined;
    }
    if (!activeOwnedByRequest) {
      const replacedPriorOwner = Boolean(
        canMutateSession &&
        active?.sessionId &&
        active.sessionId !== sessionId
      );
      if (replacedPriorOwner) {
        if (priorMeeting?.sessionId === active.sessionId) {
          active.identity ||= priorMeeting.identity;
        }
        finalizeCaptionState(active);
      }
      else if (!canMutateSession || !captureCaptions || active?.finalized !== true) return undefined;
      archiveFinalizedCaptions(active);
      if (active.settleTimer !== undefined) clearTimeout(active.settleTimer);
      active.observer?.disconnect?.();
      delete window[${captionsGlobal}];
      active = undefined;
    }
    if (!captureCaptions) {
      if (!canMutateSession) return undefined;
      if (active?.settleTimer !== undefined) clearTimeout(active.settleTimer);
      active?.observer?.disconnect?.();
      if (active) delete window[${captionsGlobal}];
      return undefined;
    }
    if (!inCall && !active) return undefined;
    if (!active && !canMutateSession) return undefined;
    if (!active) {
      if (active?.settleTimer !== undefined) clearTimeout(active.settleTimer);
      active?.observer?.disconnect?.();
      window[${captionsGlobal}] = {
        sessionId,
        identity: expectedIdentity,
        epoch: crypto.randomUUID(),
        enabledAttempted: false,
        observerInstalled: false,
        observer: undefined,
        droppedLines: 0,
        lines: [],
        settled: [],
        settleTimer: undefined,
        visible: [],
      };
    }
    return window[${captionsGlobal}];
  })();
  const normalizeCaption = (speaker, captionText) => {
    if (!captionState) return undefined;
    const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
    const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
    if (!clean) return undefined;
    return { speaker: cleanSpeaker || undefined, text: clean };
  };
  const captionRowIdentity = (row) =>
    // aria-posinset identifies the logical caption item across virtual-list
    // rerenders. DOM ids and data indexes can belong to the recycled element.
    ["aria-posinset"]
      .map((name) => {
        const value = row?.getAttribute?.(name);
        return typeof value === "string" && value.trim()
          ? name + ":" + value.trim()
          : undefined;
      })
      .find(Boolean);
  const sameCaptionUtterance = (prior, current) => {
    if (prior.rowIdentity || current.rowIdentity) {
      return Boolean(
        prior.rowIdentity &&
        current.rowIdentity &&
        prior.rowIdentity === current.rowIdentity
      );
    }
    if (prior.speaker && current.speaker && prior.speaker !== current.speaker) return false;
    return prior.node === current.node;
  };
  const commitCaptionLines = (state, entries) => {
    state.lines.push(...entries.map((entry) => {
      entry.utteranceId ||= crypto.randomUUID();
      return {
        at: entry.at,
        speaker: entry.speaker,
        text: entry.text,
        utteranceId: entry.utteranceId,
      };
    }));
    const excess = state.lines.length - ${transcriptMaxLines};
    if (excess > 0) {
      state.lines.splice(0, excess);
      state.droppedLines = (state.droppedLines || 0) + excess;
    }
  };
  const sameCaptionRow = (left, right) =>
    right.rowIdentity
      ? left.rowIdentity === right.rowIdentity
      : left.node === right.node;
  const retainSettledCaptionLines = (state, entries) => {
    const settled = [...state.settled];
    for (const entry of entries) {
      const priorIndex = settled.findIndex((candidate) => sameCaptionRow(candidate, entry));
      if (priorIndex >= 0) settled.splice(priorIndex, 1, { ...entry });
      else settled.push({ ...entry });
    }
    const retainedLineIds = new Set(state.lines.map((entry) => entry.utteranceId));
    state.settled = settled.filter((entry) => retainedLineIds.has(entry.utteranceId));
  };
  const scheduleCaptionSettle = () => {
    if (!captionState || captionState.visible.length === 0) return;
    if (captionState.settleTimer !== undefined) clearTimeout(captionState.settleTimer);
    const pendingState = captionState;
    pendingState.settleTimer = setTimeout(() => {
      if (window[${captionsGlobal}] !== pendingState) return;
      commitCaptionLines(pendingState, pendingState.visible);
      retainSettledCaptionLines(pendingState, pendingState.visible);
      pendingState.visible = [];
      pendingState.settleTimer = undefined;
    }, ${captionSettleMs});
  };
  const captionCaptureMatchesCurrentMeeting = () => {
    if (
      !captionState ||
      captionState.finalized === true ||
      window[${captionsGlobal}] !== captionState
    ) return false;
    const observedIdentity = meetingIdentity(location.href);
    const observedMeeting = window[${meetingGlobal}];
    const identityConflicts = Boolean(
      observedIdentity && expectedIdentity && observedIdentity !== expectedIdentity
    );
    const sessionConflicts = Boolean(
      observedMeeting?.sessionId && sessionId && observedMeeting.sessionId !== sessionId
    );
    if (identityConflicts || sessionConflicts) {
      // The observer outlives Teams SPA navigation. Freeze the old buffer before
      // any caption nodes from the replacement meeting can be attributed to it.
      finalizeOwnedCaptions();
      return false;
    }
    if (observedIdentity === expectedIdentity) return true;
    const observedMarkerAgeMs = Date.now() - (observedMeeting?.verifiedAt || 0);
    const observedAwaitingRerender = Boolean(
      !observedIdentity &&
      observedMeeting?.identity === expectedIdentity &&
      (!observedMeeting.sessionId || !sessionId || observedMeeting.sessionId === sessionId) &&
      observedMeeting.inCallControl?.isConnected === false &&
      observedMeeting.inCallUrl === location.href &&
      observedMarkerAgeMs >= 0 &&
      observedMarkerAgeMs < 5_000
    );
    if (observedAwaitingRerender) return true;
    return Boolean(
      observedMeeting?.identity === expectedIdentity &&
      (!observedMeeting.sessionId || !sessionId || observedMeeting.sessionId === sessionId) &&
      observedMeeting.inCallControl?.isConnected !== false &&
      observedMeeting.inCallUrl === location.href
    );
  };
  const scrapeCaptions = (mutations = []) => {
    if (!captionCaptureMatchesCurrentMeeting()) return;
    const content = firstRaw(selectors.captionContent);
    const rows = content
      ? selectors.captionRows.flatMap((selector) => [...content.querySelectorAll(selector)])
      : [];
    captionState.settled = Array.isArray(captionState.settled) ? captionState.settled : [];
    const removedNodes = mutations.flatMap((mutation) => [...(mutation.removedNodes || [])]);
    const rowWasRemoved = (entry) => removedNodes.some((node) =>
      node === entry.node || node?.contains?.(entry.node)
    );
    const removedVisible = captionState.visible.filter(rowWasRemoved);
    if (removedVisible.length > 0) {
      if (captionState.settleTimer !== undefined) clearTimeout(captionState.settleTimer);
      captionState.settleTimer = undefined;
      captionState.visible = captionState.visible.filter((entry) => !rowWasRemoved(entry));
      commitCaptionLines(captionState, removedVisible);
      retainSettledCaptionLines(captionState, removedVisible);
    }
    const retainedLineIds = new Set(captionState.lines.map((entry) => entry.utteranceId));
    captionState.settled = captionState.settled.filter((entry) =>
      entry.rowIdentity
        ? retainedLineIds.has(entry.utteranceId)
        : !rowWasRemoved(entry) && rows.some((row) => sameCaptionRow(entry, {
            node: row,
            rowIdentity: captionRowIdentity(row),
          }))
    );
    const parsedRows = rows.flatMap((row) => {
      const speaker = text(firstWithin(row, selectors.captionAuthor));
      const captionText = text(firstWithin(row, selectors.captionText));
      const parsed = normalizeCaption(speaker, captionText);
      if (!parsed) return [];
      const current = { ...parsed, node: row, rowIdentity: captionRowIdentity(row) };
      const settledIndex = captionState.settled.findIndex((entry) =>
        sameCaptionRow(entry, current)
      );
      const settled = settledIndex >= 0 ? captionState.settled[settledIndex] : undefined;
      if (
        settled &&
        settled.text === current.text &&
        (settled.speaker || "") === (current.speaker || "")
      ) return [];
      if (settled?.rowIdentity && settled.rowIdentity === current.rowIdentity) {
        const committed = captionState.lines.find((entry) =>
          entry.utteranceId === settled.utteranceId
        );
        if (committed) {
          committed.speaker = current.speaker || committed.speaker;
          committed.text = current.text;
        }
        captionState.settled.splice(settledIndex, 1, {
          ...settled,
          ...current,
          speaker: current.speaker || settled.speaker,
        });
        return [];
      }
      if (settledIndex >= 0) captionState.settled.splice(settledIndex, 1);
      return [current];
    });
    if (parsedRows.length === 0) {
      if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
        scheduleCaptionSettle();
      }
      return;
    }
    const unmatchedPrevious = [...captionState.visible];
    const nextVisible = [];
    const now = Date.now();
    let captionChanged = false;
    for (const row of parsedRows) {
      const priorIndex = unmatchedPrevious.findIndex((candidate) =>
        row.rowIdentity
          ? candidate.rowIdentity === row.rowIdentity
          : candidate.node === row.node
      );
      const candidate = priorIndex >= 0 ? unmatchedPrevious[priorIndex] : undefined;
      const prior = candidate && sameCaptionUtterance(candidate, row)
        ? unmatchedPrevious.splice(priorIndex, 1)[0]
        : undefined;
      if (prior) {
        captionChanged ||=
          prior.text !== row.text ||
          prior.speaker !== row.speaker ||
          prior.node !== row.node;
        prior.speaker = row.speaker || prior.speaker;
        prior.text = row.text;
        prior.node = row.node;
        prior.rowIdentity = row.rowIdentity || prior.rowIdentity;
        prior.seenAt = now;
        nextVisible.push(prior);
      } else {
        captionChanged = true;
        nextVisible.push({
          at: new Date().toISOString(),
          node: row.node,
          rowIdentity: row.rowIdentity,
          seenAt: now,
          speaker: row.speaker,
          text: row.text,
        });
      }
    }
    captionChanged ||= unmatchedPrevious.length > 0;
    commitCaptionLines(captionState, unmatchedPrevious);
    retainSettledCaptionLines(captionState, unmatchedPrevious);
    captionState.visible = nextVisible;
    // Identity-less rows stay mutable while rendered; removal is their only
    // reliable utterance boundary. Stable logical rows may settle on quiet.
    if (
      (captionChanged || captionState.settleTimer === undefined) &&
      captionState.visible.every((entry) => entry.rowIdentity)
    ) {
      scheduleCaptionSettle();
    }
  };
  if (captionState) {
    const captionsFinalized = captionState.finalized === true;
    let captionsEnabledNow = captionsFinalized
      ? Boolean(captionState.enabledAttempted)
      : Boolean(firstRaw(selectors.captionRenderer) || firstRaw(selectors.captionsOff));
    ${options.captionEnableSource}
    if (!captionsFinalized && canMutateSession) captionState.enabledAttempted = captionsEnabledNow;
    captionsEnabledAttempted = Boolean(captionState.enabledAttempted);
    if (!captionsFinalized && canMutateSession && inCall && !captionState.observerInstalled) {
      captionState.observerInstalled = true;
      captionState.observer = new MutationObserver(scrapeCaptions);
      captionState.observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true,
      });
      notes.push("Installed ${options.platform.displayName} live-caption observer.");
    }
    if (!captionsFinalized && canMutateSession && inCall) scrapeCaptions();
    const allLines = [...captionState.lines, ...captionState.visible];
    const lines = allLines.slice(-${transcriptMaxLines});
    const last = lines[lines.length - 1];
    captioning = captionsEnabledNow;
    transcriptLines = (captionState.droppedLines || 0) + allLines.length;
    lastCaptionAt = last?.at;
    lastCaptionSpeaker = last?.speaker;
    lastCaptionText = last?.text;
    recentTranscript = lines.slice(-5).map((entry) => ({
      at: entry.at,
      speaker: entry.speaker,
      text: entry.text,
    }));
  }
  if (inCall && allowMicrophone && !manualAction) {
    if (audioInputRouted !== true || audioOutputRouted !== true) {
      manualAction = manualActionFor("${options.platform.manualActionReasonPrefix}-audio-choice-required", "Verify the OpenClaw virtual audio device is selected as both the ${options.platform.displayName} microphone and speaker before starting talk-back.");
    } else if (micMuted !== false) {
      manualAction = manualActionFor("${options.platform.manualActionReasonPrefix}-microphone-required", "Unmute the ${options.platform.displayName} microphone and verify the microphone control shows it is on before starting talk-back.");
    }
  }
  return JSON.stringify({
    clickedContinueInBrowser: Boolean(continueInBrowser),
    clickedJoin,
    inCall,
    ${options.extraResultSource ?? ""}
    micMuted,
    cameraOff,
    lobbyWaiting,
    captionCaptureRequested: captureCaptions,
    captioning,
    captionsEnabledAttempted,
    transcriptLines,
    lastCaptionAt,
    lastCaptionSpeaker,
    lastCaptionText,
    recentTranscript,
    audioInputRouted,
    audioInputDeviceLabel,
    audioInputRouteError,
    audioOutputRouted,
    audioOutputDeviceLabel,
    audioOutputRouteError,
    audioOutputRouteRetryable,
    manualAction,
    title: document.title,
    url: location.href,
    notes,
  });
}`;
}
//#endregion
//#region src/meeting-bot/status-prejoin-source.ts
function createMeetingStatusPreludeSource(params, options) {
	const selectors = params.selectors;
	const expectedIdentity = params.expectedIdentity;
	const toggleStateFunction = params.toggleStateFunction;
	const pageIdentityFunctionSource = () => params.pageIdentitySource;
	const audioOutputsGlobal = JSON.stringify(options.platform.globals.audioOutputs);
	const captionArchiveGlobal = JSON.stringify(options.platform.globals.captionArchive);
	const captionsGlobal = JSON.stringify(options.platform.globals.captions);
	const meetingGlobal = JSON.stringify(options.platform.globals.meeting);
	const transcriptMaxLines = options.transcriptMaxLines ?? 500;
	return `async () => {
  ${pageIdentityFunctionSource()}
  ${options.setupSource ?? ""}
  const parseToggleState = ${toggleStateFunction};
  const selectors = ${selectors};
  const expectedIdentity = ${JSON.stringify(expectedIdentity)};
  const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
  const allowSessionAdoption = ${JSON.stringify(params.allowSessionAdoption)};
  const autoJoin = ${JSON.stringify(params.autoJoin)};
  const captureCaptions = ${JSON.stringify(params.captureCaptions)};
  const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
  const sessionId = ${JSON.stringify(params.meetingSessionId)};
  const identityRetentionMs = ${JSON.stringify(Math.max(3e4, params.waitForInCallMs))};
  const text = (node) => (node?.innerText || node?.textContent || "").trim();
  const label = (node) => [
    node?.getAttribute?.("aria-label"),
    node?.getAttribute?.("title"),
    node?.getAttribute?.("data-tid"),
    text(node),
  ].filter(Boolean).join(" ");
  const manualActionFor = (reason, message) => ({ reason, message });
  const clickable = (node) => node?.matches?.("button")
    ? node
    : node?.querySelector?.("button") || node?.closest?.("button") || node;
  const first = (list) => {
    for (const selector of list) {
      const node = document.querySelector(selector);
      if (node) return clickable(node);
    }
    return undefined;
  };
  const firstRaw = (list) => {
    for (const selector of list) {
      const node = document.querySelector(selector);
      if (node) return node;
    }
    return undefined;
  };
  const firstWithin = (root, list) => {
    if (!root) return undefined;
    for (const selector of list) {
      if (root.matches?.(selector)) return root;
      const node = root.querySelector?.(selector);
      if (node) return node;
    }
    return undefined;
  };
  ${options.controlLookupSource}
  const waitForUi = () => new Promise((resolve) => setTimeout(resolve, 120));
  const bridgeOwnedBySession = (entry) => Boolean(
    sessionId && (!entry?.sessionId || entry.sessionId === sessionId)
  );
  const mediaSourceUrl = (element) => String(element?.currentSrc || element?.src || "");
  const bridgeSources = (entry) => Array.isArray(entry?.sources)
    ? entry.sources
    : entry?.source
      ? [{ element: entry.source, muted: Boolean(entry.sourceMuted), pending: Boolean(entry.pending), stream: entry.stream, url: entry.sourceUrl }]
      : [];
  const bridgeSourceMatches = (element, source) => {
    if (!element) return false;
    if (source?.pending && mediaSourceIsEmpty(element) && !source.stream && !source.url) return true;
    if (source?.stream || element.srcObject) return element.srcObject === source?.stream;
    const currentUrl = mediaSourceUrl(element);
    return Boolean(source?.url && currentUrl && source.url === currentUrl);
  };
  const mediaSourceIsEmpty = (element) => Boolean(
    element && !element.srcObject && !mediaSourceUrl(element)
  );
  const restoreAudioBridgeSource = (source) => {
    const element = source?.element;
    // An empty element may receive a replacement source after cleanup. Keep it
    // silent because there is no source identity that is safe to restore.
    if (mediaSourceIsEmpty(element)) {
      element.muted = true;
      return;
    }
    // Teams reuses media elements across source changes. Restore only the exact
    // source this bridge muted.
    if (!bridgeSourceMatches(element, source)) return;
    const detachedLiveSource = Boolean(
      element.isConnected === false &&
      element.srcObject?.getAudioTracks?.().some((track) => track.readyState === "live")
    );
    if (detachedLiveSource) {
      element.muted = true;
      element.pause?.();
      element.srcObject = null;
      return;
    }
    element.muted = Boolean(source.muted);
  };
  const restoreAudioBridgeSources = (entry) => {
    bridgeSources(entry).forEach(restoreAudioBridgeSource);
  };
  const retireAudioBridge = (entry, restoreSources = true) => {
    if (restoreSources) restoreAudioBridgeSources(entry);
    entry?.bridge?.pause?.();
    if (entry?.bridge) entry.bridge.srcObject = null;
    entry?.bridge?.remove?.();
  };
  const retireOwnedAudioBridges = (restoreSources = true) => {
    const entries = Array.isArray(window[${audioOutputsGlobal}])
      ? window[${audioOutputsGlobal}]
      : [];
    const retained = [];
    for (const entry of entries) {
      if (!bridgeOwnedBySession(entry)) {
        retained.push(entry);
        continue;
      }
      retireAudioBridge(entry, restoreSources);
    }
    if (retained.length > 0) window[${audioOutputsGlobal}] = retained;
    else delete window[${audioOutputsGlobal}];
  };
  const adoptAudioBridgeSourcesForSession = () => {
    const entries = Array.isArray(window[${audioOutputsGlobal}])
      ? window[${audioOutputsGlobal}]
      : [];
    const suspendedBySource = new Map();
    for (const entry of entries) {
      for (const source of bridgeSources(entry)) {
        if (!source?.element || suspendedBySource.has(source.element)) continue;
        if (!bridgeSourceMatches(source.element, source)) {
          restoreAudioBridgeSource(source);
          continue;
        }
        suspendedBySource.set(source.element, {
          sessionId,
          source: source.element,
          sourceMuted: Boolean(source.muted),
          sourceUrl: mediaSourceUrl(source.element) || source.url,
          stream: source.element.srcObject,
          suspended: true,
        });
      }
      retireAudioBridge(entry, false);
    }
    const suspended = [...suspendedBySource.values()];
    if (suspended.length > 0) window[${audioOutputsGlobal}] = suspended;
    else delete window[${audioOutputsGlobal}];
  };
  const suspendOwnedAudioBridges = () => {
    const entries = Array.isArray(window[${audioOutputsGlobal}])
      ? window[${audioOutputsGlobal}]
      : [];
    const retained = [];
    const suspendedBySource = new Map();
    for (const entry of entries) {
      if (!bridgeOwnedBySession(entry)) {
        retained.push(entry);
        continue;
      }
      // This pending entry owns the muted element until a later serialized
      // status poll sees and routes the attached playback source.
      if (
        entry?.pending &&
        bridgeSources(entry).some((source) => bridgeSourceMatches(source?.element, source))
      ) {
        retained.push(entry);
        continue;
      }
      for (const source of bridgeSources(entry)) {
        if (!source?.element || suspendedBySource.has(source.element)) continue;
        if (!bridgeSourceMatches(source.element, source)) {
          restoreAudioBridgeSource(source);
          continue;
        }
        suspendedBySource.set(source.element, {
          sessionId: entry.sessionId || sessionId,
          source: source.element,
          sourceMuted: Boolean(source.muted),
          sourceUrl: source.url,
          stream: source.element.srcObject,
          suspended: true,
        });
      }
      retireAudioBridge(entry, false);
    }
    const next = [...retained, ...suspendedBySource.values()];
    if (next.length > 0) window[${audioOutputsGlobal}] = next;
    else delete window[${audioOutputsGlobal}];
  };
  const retireOwnedCaptions = () => {
    const active = window[${captionsGlobal}];
    const owned = Boolean(
      active && sessionId && (!active.sessionId || active.sessionId === sessionId)
    );
    if (!owned) return;
    if (active.settleTimer !== undefined) clearTimeout(active.settleTimer);
    active.observer?.disconnect?.();
    delete window[${captionsGlobal}];
  };
  const finalizeCaptionState = (active) => {
    if (!active) return;
    if (active.settleTimer !== undefined) clearTimeout(active.settleTimer);
    active.settleTimer = undefined;
    active.observer?.disconnect?.();
    active.observer = undefined;
    active.observerInstalled = false;
    active.lines = Array.isArray(active.lines) ? active.lines : [];
    if (Array.isArray(active.visible) && active.visible.length > 0) {
      active.lines.push(...active.visible.map((entry) => ({
        at: entry.at,
        speaker: entry.speaker,
        text: entry.text,
      })));
      active.visible = [];
    }
    const excess = active.lines.length - ${transcriptMaxLines};
    if (excess > 0) {
      active.lines.splice(0, excess);
      active.droppedLines = (active.droppedLines || 0) + excess;
    }
    active.finalized = true;
    active.finalizedAt = Date.now();
  };
  const archiveFinalizedCaptions = (active) => {
    if (active?.finalized !== true || !active.sessionId) return;
    const archive = window[${captionArchiveGlobal}] &&
        typeof window[${captionArchiveGlobal}] === "object"
      ? window[${captionArchiveGlobal}]
      : {};
    archive[active.sessionId] = active;
    const retained = Object.entries(archive)
      .sort((left, right) => Number(right[1]?.finalizedAt || 0) - Number(left[1]?.finalizedAt || 0))
      .slice(0, 4);
    window[${captionArchiveGlobal}] = Object.fromEntries(retained);
  };
  const finalizeOwnedCaptions = () => {
    const active = window[${captionsGlobal}];
    const owned = Boolean(
      active && sessionId && (!active.sessionId || active.sessionId === sessionId)
    );
    if (owned) {
      active.identity ||= priorMeeting?.identity || expectedIdentity;
      finalizeCaptionState(active);
    }
  };
  const toggleState = (node, kind) => parseToggleState({
    kind,
    ariaPressed: node?.getAttribute?.("aria-pressed"),
    ariaChecked: node?.getAttribute?.("aria-checked"),
    checked: typeof node?.checked === "boolean" ? node.checked : undefined,
    iconClass: node?.querySelector?.("svg")?.getAttribute?.("class"),
    label: label(node),
  });
  const notes = [];
  const currentIdentity = meetingIdentity(location.href);
  const priorMeeting = window[${meetingGlobal}];
  if (expectedIdentity && currentIdentity && currentIdentity !== expectedIdentity) {
    // A confirmed SPA transition must stop resources still owned by this
    // request, while preserving any newer session already committed to the tab.
    retireOwnedAudioBridges();
    finalizeOwnedCaptions();
    const requestOwnsMeeting = Boolean(
      priorMeeting &&
      sessionId &&
      (!priorMeeting.sessionId || priorMeeting.sessionId === sessionId)
    );
    if (requestOwnsMeeting) delete window[${meetingGlobal}];
    return JSON.stringify({
      inCall: false,
      manualAction: manualActionFor("${options.platform.manualActionReasonPrefix}-session-conflict", "The tracked ${options.platform.displayName} tab now shows a different meeting. Return to the requested meeting link, then retry."),
      title: document.title,
      url: location.href,
      notes,
    });
  }
  const meetingOwnerConflict = Boolean(
    priorMeeting?.sessionId && priorMeeting.sessionId !== sessionId
  );
  const captionOwnerConflict = Boolean(
    window[${captionsGlobal}]?.sessionId &&
    window[${captionsGlobal}].sessionId !== sessionId
  );
  const committedOwnerConflict = meetingOwnerConflict || captionOwnerConflict;
  const canRepairCaptionOwner = Boolean(
    !meetingOwnerConflict && priorMeeting?.sessionId === sessionId
  );
  const canMutateSession = Boolean(
    !readOnly &&
    sessionId &&
    (!committedOwnerConflict || canRepairCaptionOwner || allowSessionAdoption)
  );
  const identityMatchedUrl = Boolean(expectedIdentity && currentIdentity === expectedIdentity);
  const identityVerifiedBeforeCall = identityMatchedUrl;
  ${options.lifecycleSource}
  const micMuted = microphoneState === "off" ? true : microphoneState === "on" ? false : undefined;
  const cameraOff = cameraState === "off" ? true : cameraState === "on" ? false : undefined;
  ${options.manualActionSource}
`;
}
//#endregion
//#region src/meeting-bot/platform-adapter.ts
function browserResultString(result) {
	if (!result || typeof result !== "object") return;
	const value = result.result;
	return typeof value === "string" && value.trim() ? value : void 0;
}
const optionalBrowserString = string().optional().catch(void 0);
const optionalBrowserBoolean = boolean().optional().catch(void 0);
const optionalBrowserNumber = number().optional().catch(void 0);
const invalidBrowserArrayItemSchema = unknown().transform(() => null);
const meetingTranscriptLineSchema = object({
	at: optionalBrowserString,
	speaker: optionalBrowserString,
	text: string().refine((value) => value.trim().length > 0)
}).transform(({ at, speaker, text }) => ({
	...at !== void 0 ? { at } : {},
	...speaker !== void 0 ? { speaker } : {},
	text
}));
const meetingBrowserStatusSchema = looseObject({
	inCall: optionalBrowserBoolean,
	micMuted: optionalBrowserBoolean,
	cameraOff: optionalBrowserBoolean,
	lobbyWaiting: optionalBrowserBoolean,
	captionCaptureRequested: optionalBrowserBoolean,
	captioning: optionalBrowserBoolean,
	captionsEnabledAttempted: optionalBrowserBoolean,
	transcriptLines: optionalBrowserNumber,
	lastCaptionAt: optionalBrowserString,
	lastCaptionSpeaker: optionalBrowserString,
	lastCaptionText: optionalBrowserString,
	recentTranscript: array(union([meetingTranscriptLineSchema, invalidBrowserArrayItemSchema])).transform((lines) => lines.filter((line) => line !== null)).optional().catch(void 0),
	audioInputRouted: optionalBrowserBoolean,
	audioInputDeviceLabel: optionalBrowserString,
	audioInputRouteError: optionalBrowserString,
	audioOutputRouted: optionalBrowserBoolean,
	audioOutputDeviceLabel: optionalBrowserString,
	audioOutputRouteError: optionalBrowserString,
	audioOutputRouteRetryable: optionalBrowserBoolean,
	manualAction: object({
		reason: string(),
		message: string()
	}).optional().catch(void 0),
	url: optionalBrowserString,
	title: optionalBrowserString,
	notes: array(union([string(), invalidBrowserArrayItemSchema])).transform((notes) => notes.filter((note) => note !== null)).optional().catch(void 0)
});
function parseMeetingBrowserStatus(result, options) {
	const raw = browserResultString(result);
	if (!raw) return;
	let parsed;
	try {
		parsed = meetingBrowserStatusSchema.parse(JSON.parse(raw));
	} catch {
		throw new Error(options.malformedStatusMessage);
	}
	return {
		inCall: parsed.inCall,
		micMuted: parsed.micMuted,
		cameraOff: parsed.cameraOff,
		lobbyWaiting: parsed.lobbyWaiting,
		captionCaptureRequested: parsed.captionCaptureRequested,
		captioning: parsed.captioning,
		captionsEnabledAttempted: parsed.captionsEnabledAttempted,
		transcriptLines: parsed.transcriptLines,
		lastCaptionAt: parsed.lastCaptionAt,
		lastCaptionSpeaker: parsed.lastCaptionSpeaker,
		lastCaptionText: parsed.lastCaptionText,
		recentTranscript: parsed.recentTranscript,
		audioInputRouted: parsed.audioInputRouted,
		audioInputDeviceLabel: parsed.audioInputDeviceLabel,
		audioInputRouteError: parsed.audioInputRouteError,
		audioOutputRouted: parsed.audioOutputRouted,
		audioOutputDeviceLabel: parsed.audioOutputDeviceLabel,
		audioOutputRouteError: parsed.audioOutputRouteError,
		audioOutputRouteRetryable: parsed.audioOutputRouteRetryable,
		manualAction: parsed.manualAction,
		browserUrl: parsed.url,
		browserTitle: parsed.title,
		status: "browser-control",
		notes: parsed.notes,
		...options.statusFields?.(parsed)
	};
}
function parseMeetingLeaveResult(result) {
	const raw = browserResultString(result);
	if (!raw) return { departed: false };
	try {
		const parsed = JSON.parse(raw);
		const leaveAction = parsed.leaveAction === "leave" || parsed.leaveAction === "confirm" ? parsed.leaveAction : void 0;
		return {
			departed: parsed.departed === true,
			...leaveAction ? { leaveAction } : {},
			...typeof parsed.sessionConflict === "boolean" ? { sessionConflict: parsed.sessionConflict } : {},
			...typeof parsed.sessionMatched === "boolean" ? { sessionMatched: parsed.sessionMatched } : {},
			...typeof parsed.urlMatched === "boolean" ? { urlMatched: parsed.urlMatched } : {}
		};
	} catch {
		return { departed: false };
	}
}
function parseMeetingTranscript(result, options) {
	const raw = browserResultString(result);
	if (!raw) return {
		droppedLines: 0,
		lines: []
	};
	let parsed;
	try {
		parsed = JSON.parse(raw);
	} catch {
		throw new Error(options.malformedTranscriptMessage);
	}
	if (!parsed || typeof parsed !== "object") throw new Error(options.invalidTranscriptMessage);
	const payload = parsed;
	const droppedLines = typeof payload.droppedLines === "number" && Number.isSafeInteger(payload.droppedLines) ? Math.max(0, payload.droppedLines) : 0;
	const lines = Array.isArray(payload.lines) ? payload.lines.flatMap((value) => {
		if (!value || typeof value !== "object") return [];
		const line = value;
		if (typeof line.text !== "string" || !line.text.trim()) return [];
		return [{
			...typeof line.at === "string" ? { at: line.at } : {},
			...typeof line.speaker === "string" ? { speaker: line.speaker } : {},
			text: line.text
		}];
	}) : [];
	return {
		droppedLines,
		...typeof payload.epoch === "string" ? { epoch: payload.epoch } : {},
		lines,
		...typeof payload.urlMatched === "boolean" ? { urlMatched: payload.urlMatched } : {},
		...typeof payload.sessionMatched === "boolean" ? { sessionMatched: payload.sessionMatched } : {}
	};
}
function createMeetingPlatformAdapter(options) {
	const { browser, parsing, ...platform } = options;
	return {
		...platform,
		browser: {
			...browser,
			parseStatus: (result) => parseMeetingBrowserStatus(result, parsing),
			classifyManualAction: (health) => {
				if (!health.manualAction) return;
				return {
					category: parsing.classifyManualActionReason(health.manualAction.reason),
					reason: health.manualAction.reason,
					message: health.manualAction.message
				};
			},
			parseLeaveResult: parseMeetingLeaveResult,
			captions: {
				...browser.captions,
				parseTranscript: (result) => parseMeetingTranscript(result, parsing)
			},
			permissionNotes: browser.permissionNotes ?? (({ allowMicrophone, error, result }) => {
				if (!allowMicrophone) return [`Observe-only mode does not request ${parsing.displayName} microphone access.`];
				if (error) return [`Could not grant ${parsing.displayName} media permissions automatically: ${formatErrorMessage(error)}`];
				const record = result && typeof result === "object" ? result : {};
				const unsupportedPermissions = Array.isArray(record.unsupportedPermissions) ? record.unsupportedPermissions.filter((value) => typeof value === "string") : [];
				const notes = [`Granted ${parsing.displayName} microphone permission through browser control.`];
				if (unsupportedPermissions.includes("speakerSelection")) notes.push(`Chrome did not accept the optional ${parsing.displayName} speaker-selection permission.`);
				return notes;
			})
		}
	};
}
const MeetingPlatformAdapter = {
	create: createMeetingPlatformAdapter,
	createChromeTransport: createMeetingChromeTransport,
	createChromeRuntimeBindings: createMeetingChromeRuntimeBindings,
	createPluginChromeTransport: createMeetingPluginChromeTransport,
	createPluginConfigSchema: createMeetingPluginConfigSchema,
	createPluginNodeHostHandler: createMeetingPluginNodeHostHandler,
	createPluginNodeInvokePolicy: createMeetingPluginNodeInvokePolicy,
	createPluginShellEntry: createMeetingPluginShellEntry,
	createRuntimeFacade: createMeetingRuntimeFacade,
	createRuntimeSetup: createMeetingRuntimeSetup,
	pluginTypes: createMeetingPluginTypes,
	registerPluginCli: registerMeetingPluginCli,
	resolveProbeTimeoutMs: resolveMeetingProbeTimeoutMs,
	createRuntimeProbes: createMeetingRuntimeProbes,
	createNodeHostHandler: createMeetingConfiguredNodeHost,
	createPluginEntry: createMeetingPluginEntryOptions,
	createStatusCallSource: createMeetingStatusCallSource,
	createStatusPreludeSource: createMeetingStatusPreludeSource,
	isRealtimeRouteReady: isMeetingRealtimeRouteReady,
	isTalkBackMode: isMeetingTalkBackMode,
	ensureAudioBackend: ensureMeetingAudioBackend,
	resolveAudioRuntimeForFormat: resolveMeetingAudioRuntimeForFormat
};
//#endregion
//#region src/meeting-bot/voice-call-gateway.ts
function createMeetingVoiceCallGateway(params) {
	if (!params.config.gatewayUrl) return {
		trustedPluginIdentity: true,
		request: (method, requestParams) => params.runtime.gateway.request(method, requestParams, { timeoutMs: params.config.requestTimeoutMs })
	};
	return {
		trustedPluginIdentity: false,
		async request(method, requestParams) {
			const client = await params.connectClient(params);
			try {
				return await client.request(method, requestParams, { timeoutMs: params.config.requestTimeoutMs });
			} finally {
				await client.stopAndWait({ timeoutMs: 1e3 }).catch(() => {});
			}
		}
	};
}
function isMeetingVoiceCallMissingError(error) {
	const message = formatErrorMessage(error).toLowerCase();
	return message.includes("call not found") || message.includes("call is not active");
}
async function joinMeetingViaVoiceCallGateway(params) {
	if (params.agentId && params.agentId !== "main" && !params.gateway.trustedPluginIdentity) throw new Error(`Per-agent Voice Call routing requires the local Gateway runtime. Remove ${params.surface.configPath} or omit agent routing.`);
	params.logger?.info(`${params.surface.logScope} Delegating ${params.surface.providerLabel} join to Voice Call (dtmf=${params.dtmfSequence ? "pre-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`);
	const start = await params.gateway.request("voicecall.start", {
		to: params.dialInNumber,
		mode: "conversation",
		...params.dtmfSequence ? { dtmfSequence: params.dtmfSequence } : {},
		...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
		...params.agentId && params.gateway.trustedPluginIdentity ? { agentId: params.agentId } : {},
		...params.sessionKey ? { sessionKey: params.sessionKey } : {}
	});
	if (!start.callId) throw new Error(start.error || "voicecall.start did not return callId");
	params.logger?.info(`${params.surface.logScope} Voice Call ${params.surface.providerLabel} phone leg started: callId=${start.callId}`);
	const dtmfSent = Boolean(params.dtmfSequence);
	if (dtmfSent) params.logger?.info(`${params.surface.logScope} ${params.surface.meetingLabel} DTMF queued before realtime connect: callId=${start.callId} digits=${params.dtmfSequence?.length ?? 0}`);
	let introSent = false;
	if (params.message) {
		const delayMs = params.dtmfSequence ? params.config.postDtmfSpeechDelayMs : 0;
		if (delayMs > 0) {
			params.logger?.info(`${params.surface.logScope} Waiting ${delayMs}ms after ${params.surface.meetingLabel} DTMF before speaking intro for callId=${start.callId}`);
			await sleep(delayMs);
		}
		let spoken;
		try {
			spoken = await params.gateway.request("voicecall.speak", {
				callId: start.callId,
				allowTwimlFallback: false,
				message: params.message
			});
		} catch (error) {
			params.logger?.warn?.(`${params.surface.logScope} Skipped intro speech because realtime bridge was not ready: ${formatErrorMessage(error)}`);
			spoken = { success: false };
		}
		if (spoken.success === false) params.logger?.warn?.(`${params.surface.logScope} Skipped intro speech because realtime bridge was not ready: ${spoken.error || "voicecall.speak failed"}`);
		else {
			introSent = true;
			params.logger?.info(`${params.surface.logScope} Intro speech requested after ${params.surface.meetingLabel} dial sequence: callId=${start.callId}`);
		}
	}
	return {
		callId: start.callId,
		dtmfSent,
		introSent
	};
}
async function endMeetingVoiceCallGatewayCall(params) {
	try {
		await params.gateway.request("voicecall.end", { callId: params.callId });
	} catch (error) {
		if (!isMeetingVoiceCallMissingError(error)) throw error;
	}
}
async function getMeetingVoiceCallGatewayCall(params) {
	return await params.gateway.request("voicecall.status", { callId: params.callId });
}
async function speakMeetingViaVoiceCallGateway(params) {
	const spoken = await params.gateway.request("voicecall.speak", {
		callId: params.callId,
		message: params.message
	});
	if (spoken.success === false) throw new Error(spoken.error || "voicecall.speak failed");
}
//#endregion
export { MeetingPlatformAdapter, MeetingSessionRuntime, addMeetingSetupCheck, asMeetingBrowserTabs, buildMeetingSoxAudioCommands, callMeetingBrowserProxyOnNode, convertMeetingTtsAudioForBridge, createLocalMeetingRealtimeAudioTransport, createMeetingBrowserNodeInvokePolicy, createMeetingNodeHost, createMeetingRealtimeEngineBindings, createMeetingSession, createMeetingSetupStatus, createMeetingVoiceCallGateway, createNodeMeetingRealtimeAudioTransport, endMeetingVoiceCallGatewayCall, getMeetingVoiceCallGatewayCall, isMeetingVoiceCallMissingError, joinMeetingViaVoiceCallGateway, leaveMeetingWithBrowser, openMeetingWithBrowser, readMeetingBrowserTab, readMeetingTranscriptWithBrowser, recoverMeetingBrowserTab, resolveLocalMeetingBrowserRequest, resolveMeetingBrowserNode, resolveMeetingBrowserNodeInfo, speakMeetingViaVoiceCallGateway, startMeetingAgentRealtimeEngine, startMeetingRealtimeEngine };