UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

3,997 lines 152 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { t as appendRegularFile } from "./regular-file-BD2zl6_l.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import { m as resolveThreadSessionKeys } from "./session-key-B_NoIfpX.js";
import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js";
import { n as formatAllowlistMatchMeta } from "./allowlist-match-B04-UeMC.js";
import { S as buildMediaPayload } from "./reply-payload-D-VMfpYI.js";
import { r as resolveDefaultGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js";
import { t as parseMediaContentLength } from "./content-length-DZY9SBS5.js";
import { o as saveResponseMedia } from "./fetch-2npQWHQh.js";
import { S as resolveChannelStreamingBlockEnabled, a as createChannelProgressDraftGate, c as formatChannelProgressDraftText, f as mergeChannelProgressDraftLine, i as buildChannelProgressDraftLineForEntry, j as resolveChannelStreamingSuppressDefaultToolProgressMessages, k as resolveChannelStreamingPreviewToolProgress, m as resolveChannelPreviewStreamMode, p as normalizeChannelProgressDraftLineIdentity, r as buildChannelProgressDraftLine, u as isChannelProgressDraftWorkToolName, y as resolveChannelProgressDraftMaxLines } from "./streaming-_TVeMLGC.js";
import { a as resolveChannelMediaMaxBytes } from "./media-runtime-fgpmX6eL.js";
import "./number-runtime-DBLVDypr.js";
import "./security-runtime-CQm7DD1u.js";
import { n as filterSupplementalContextItems } from "./context-visibility-C5CaKMWO.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./routing-CQ63ICPX.js";
import { _ as resolveChannelTurnDispatchCounts, h as hasFinalChannelTurnDispatch, y as buildChannelInboundEventContext } from "./kernel-BdraY_qZ.js";
import { n as resolveInboundMentionDecision } from "./mention-gating-3P8aSD7o.js";
import "./history-6XlO5h6s.js";
import { t as createChannelReplyPipeline } from "./reply-pipeline-eJPstCAz.js";
import { t as createChannelHistoryWindow } from "./history-window-B0hyPVKC.js";
import { r as keepHttpServerTaskAlive } from "./channel-lifecycle.core-Bfh0_sXw.js";
import { f as summarizeMapping, u as mergeAllowlist } from "./allow-from-DCaV3hzI.js";
import { n as isDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js";
import "./reply-history-Y9HXucO9.js";
import { t as resolveChannelContextVisibilityMode } from "./context-visibility-D4NM8ac1.js";
import { o as dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch-BpzI7GIp.js";
import "./channel-outbound-B3_Zy-kG.js";
import { n as logInboundDrop, r as logTypingFailure } from "./logging-gUWPKC5g.js";
import { n as DEFAULT_WEBHOOK_MAX_BODY_BYTES } from "./http-body-DASeqdKJ.js";
import "./ssrf-runtime-BOGN5pUi.js";
import { a as resolveInboundSessionEnvelopeContext } from "./channel-inbound-bl7VmTdr.js";
import { i as resolveStableChannelMessageIngress, t as channelIngressRoutes } from "./message-access-mwxdTUGC.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { n as createChannelPairingController } from "./channel-pairing-Bk6_JhTm.js";
import { n as getOptionalMSTeamsRuntime, t as getMSTeamsRuntime } from "./runtime-6g-cPOGH.js";
import "./runtime-api-B9a52KOD.js";
import { A as GRAPH_ROOT, B as isUrlAllowed, D as resolveMSTeamsSdkCloudOptions, E as tryNormalizeBotFrameworkServiceUrl, F as extractHtmlFromAttachment, G as resolveRequestUrl, H as readNestedString, I as extractInlineImageCandidates, J as tryBuildGraphSharesUrlForSharedLink, K as safeFetchWithPolicy, L as inferPlaceholder, M as applyAuthorizationHeaderForUrl, N as encodeGraphShareId, P as estimateBase64DecodedBytes, R as isDownloadableAttachment, S as ensureUserAgentHeader, U as resolveAttachmentFetchPolicy, V as normalizeContentType, W as resolveMediaSsrfPolicy, a as fetchGraphJson, b as loadMSTeamsSdkWithAuth, h as resolveMSTeamsCredentials, j as IMG_SRC_RE, k as ATTACHMENT_TAG_RE, q as safeHostForUrl, v as createMSTeamsExpressAdapter, y as createMSTeamsTokenProvider, z as isLikelyImageAttachment } from "./graph-users-DhhsJ2VT.js";
import { c as resolveMSTeamsUserAllowlist, s as resolveMSTeamsChannelAllowlist } from "./resolve-allowlist-5LIHeNAD.js";
import { i as resolveMSTeamsRouteConfig, r as resolveMSTeamsReplyPolicy, t as resolveMSTeamsAllowlistMatch } from "./policy-DZBX7LmO.js";
import { n as formatMSTeamsSendErrorHint, r as formatUnknownError, t as classifyMSTeamsSendError } from "./errors-CE6fGKB_.js";
import { l as createMSTeamsPollStoreState, u as extractMSTeamsPollVote, v as createMSTeamsConversationStoreState } from "./polls-CojwMnnP.js";
import { i as createMSTeamsSsoTokenStoreFs } from "./sso-token-store-BZ3Je_ef.js";
import { _ as buildFileInfoCard, c as renderReplyPayloadsToMessages, d as withRevokedProxyFallback, f as resolveGraphChatId, g as removePendingUploadFs, h as getPendingUploadFs, l as sendMSTeamsMessages, m as removePendingUpload, p as getPendingUpload, s as buildConversationReference, u as sendMSTeamsActivityWithReference, v as parseFileConsentInvoke, y as uploadToConsentUrl } from "./probe-DhYkw-P_.js";
import path from "node:path";
import crypto from "node:crypto";
//#region extensions/msteams/src/feedback-reflection-prompt.ts
/** Max chars of the thumbed-down response to include in the reflection prompt. */
const MAX_RESPONSE_CHARS = 500;
function buildReflectionPrompt(params) {
	const parts = ["A user indicated your previous response wasn't helpful."];
	if (params.thumbedDownResponse) {
		const truncated = params.thumbedDownResponse.length > MAX_RESPONSE_CHARS ? `${params.thumbedDownResponse.slice(0, MAX_RESPONSE_CHARS)}...` : params.thumbedDownResponse;
		parts.push(`\nYour response was:\n> ${truncated}`);
	}
	if (params.userComment) parts.push(`\nUser's comment: "${params.userComment}"`);
	parts.push("\nBriefly reflect: what could you improve? Consider tone, length, accuracy, relevance, and specificity. Reply with a single JSON object only, no markdown or prose, using this exact shape:\n{\"learning\":\"...\",\"followUp\":false,\"userMessage\":\"\"}\n- learning: a short internal adjustment note (1-2 sentences) for your future behavior in this conversation.\n- followUp: true only if the user needs a direct follow-up message.\n- userMessage: only the exact user-facing message to send; empty string when followUp is false.");
	return parts.join("\n");
}
function parseBooleanLike(value) {
	if (typeof value === "boolean") return value;
	if (typeof value === "string") {
		const normalized = normalizeOptionalLowercaseString(value);
		if (normalized === "true" || normalized === "yes") return true;
		if (normalized === "false" || normalized === "no") return false;
	}
}
function parseStructuredReflectionValue(value) {
	if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
	const candidate = value;
	const learning = typeof candidate.learning === "string" ? candidate.learning.trim() : void 0;
	if (!learning) return null;
	return {
		learning,
		followUp: parseBooleanLike(candidate.followUp) ?? false,
		userMessage: typeof candidate.userMessage === "string" && candidate.userMessage.trim() ? candidate.userMessage.trim() : void 0
	};
}
function parseReflectionResponse(text) {
	const trimmed = text.trim();
	if (!trimmed) return null;
	const candidates = [trimmed, ...trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.slice(1, 2) ?? []];
	for (const candidateText of candidates) {
		const candidate = candidateText.trim();
		if (!candidate) continue;
		try {
			const parsed = parseStructuredReflectionValue(JSON.parse(candidate));
			if (parsed) return parsed;
		} catch {}
	}
	return {
		learning: trimmed,
		followUp: false
	};
}
/** Tracks last reflection time per session to enforce cooldown. */
const lastReflectionBySession = /* @__PURE__ */ new Map();
/** Maximum cooldown entries before pruning expired ones. */
const MAX_COOLDOWN_ENTRIES = 500;
const LEARNINGS_NAMESPACE = "feedback-learnings";
const MAX_LEARNING_ENTRIES = 1e4;
function learningStoreKey(storePath, sessionKey) {
	return crypto.createHash("sha256").update(`${storePath}\0${sessionKey}`, "utf8").digest("hex");
}
function openLearningStore() {
	return getMSTeamsRuntime().state.openKeyedStore({
		namespace: LEARNINGS_NAMESPACE,
		maxEntries: MAX_LEARNING_ENTRIES
	});
}
/** Prune expired cooldown entries to prevent unbounded memory growth. */
function pruneExpiredCooldowns(cooldownMs) {
	if (lastReflectionBySession.size <= MAX_COOLDOWN_ENTRIES) return;
	const now = Date.now();
	for (const [key, time] of lastReflectionBySession) if (now - time >= cooldownMs) lastReflectionBySession.delete(key);
}
/** Check if a reflection is allowed (cooldown not active). */
function isReflectionAllowed(sessionKey, cooldownMs) {
	const cooldown = cooldownMs ?? 3e5;
	const lastTime = lastReflectionBySession.get(sessionKey);
	if (lastTime == null) return true;
	return Date.now() - lastTime >= cooldown;
}
/** Record that a reflection was run for a session. */
function recordReflectionTime(sessionKey, cooldownMs) {
	lastReflectionBySession.set(sessionKey, Date.now());
	pruneExpiredCooldowns(cooldownMs ?? 3e5);
}
/** Store a learning derived from feedback reflection. */
async function storeSessionLearning(params) {
	const store = openLearningStore();
	const key = learningStoreKey(params.storePath, params.sessionKey);
	let learnings = (await store.lookup(key))?.learnings ?? [];
	learnings.push(params.learning);
	if (learnings.length > 10) learnings = learnings.slice(-10);
	await store.register(key, {
		sessionKey: params.sessionKey,
		learnings,
		updatedAt: Date.now()
	});
}
//#endregion
//#region extensions/msteams/src/feedback-reflection.ts
function buildFeedbackEvent(params) {
	return {
		type: "custom",
		event: "feedback",
		ts: Date.now(),
		messageId: params.messageId,
		value: params.value,
		comment: params.comment,
		sessionKey: params.sessionKey,
		agentId: params.agentId,
		conversationId: params.conversationId
	};
}
function buildReflectionContext(params) {
	const core = getMSTeamsRuntime();
	const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
	const body = core.channel.reply.formatAgentEnvelope({
		channel: "Teams",
		from: "system",
		body: params.reflectionPrompt,
		envelope: envelopeOptions
	});
	return { ctxPayload: core.channel.reply.finalizeInboundContext({
		Body: body,
		BodyForAgent: params.reflectionPrompt,
		RawBody: params.reflectionPrompt,
		CommandBody: params.reflectionPrompt,
		From: `msteams:system:${params.conversationId}`,
		To: `conversation:${params.conversationId}`,
		SessionKey: params.sessionKey,
		ChatType: "direct",
		SenderName: "system",
		SenderId: "system",
		Provider: "msteams",
		Surface: "msteams",
		Timestamp: Date.now(),
		WasMentioned: true,
		CommandAuthorized: false,
		OriginatingChannel: "msteams",
		OriginatingTo: `conversation:${params.conversationId}`
	}) };
}
function createReflectionCaptureDispatcher(params) {
	const core = getMSTeamsRuntime();
	let response = "";
	const { dispatcher, replyOptions } = core.channel.reply.createReplyDispatcherWithTyping({
		deliver: async (payload) => {
			if (payload.text) response += (response ? "\n" : "") + payload.text;
		},
		typingCallbacks: {
			onReplyStart: async () => {},
			onIdle: () => {},
			onCleanup: () => {}
		},
		humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
		onError: (err) => {
			params.log.debug?.("reflection reply error", { error: formatUnknownError(err) });
		}
	});
	return {
		dispatcher,
		replyOptions,
		readResponse: () => response
	};
}
async function sendReflectionFollowUp(params) {
	const baseRef = buildConversationReference(params.conversationRef);
	await sendMSTeamsActivityWithReference(params.app, baseRef, {
		type: "message",
		text: params.userMessage
	}, { serviceUrlBoundary: resolveMSTeamsSdkCloudOptions(params.cfg.channels?.msteams) });
}
/**
* Run a background reflection after negative feedback.
* This is designed to be called fire-and-forget (don't await in the invoke handler).
*/
async function runFeedbackReflection(params) {
	const { cfg, log, sessionKey } = params;
	const cooldownMs = cfg.channels?.msteams?.feedbackReflectionCooldownMs ?? 3e5;
	if (!isReflectionAllowed(sessionKey, cooldownMs)) {
		log.debug?.("skipping reflection (cooldown active)", { sessionKey });
		return;
	}
	const reflectionPrompt = buildReflectionPrompt({
		thumbedDownResponse: params.thumbedDownResponse,
		userComment: params.userComment
	});
	const storePath = getMSTeamsRuntime().channel.session.resolveStorePath(cfg.session?.store, { agentId: params.agentId });
	const { ctxPayload } = buildReflectionContext({
		cfg,
		conversationId: params.conversationId,
		sessionKey: params.sessionKey,
		reflectionPrompt
	});
	const capture = createReflectionCaptureDispatcher({
		cfg,
		agentId: params.agentId,
		log
	});
	try {
		await dispatchReplyFromConfigWithSettledDispatcher({
			ctxPayload,
			cfg,
			dispatcher: capture.dispatcher,
			onSettled: () => {},
			replyOptions: capture.replyOptions
		});
	} catch (err) {
		log.error("reflection dispatch failed", { error: formatUnknownError(err) });
		return;
	}
	const reflectionResponse = capture.readResponse().trim();
	if (!reflectionResponse) {
		log.debug?.("reflection produced no output");
		return;
	}
	const parsedReflection = parseReflectionResponse(reflectionResponse);
	if (!parsedReflection) {
		log.debug?.("reflection produced no structured output");
		return;
	}
	recordReflectionTime(sessionKey, cooldownMs);
	log.info("reflection complete", {
		sessionKey,
		responseLength: reflectionResponse.length,
		followUp: parsedReflection.followUp
	});
	try {
		await storeSessionLearning({
			storePath,
			sessionKey: params.sessionKey,
			learning: parsedReflection.learning
		});
	} catch (err) {
		log.debug?.("failed to store reflection learning", { error: formatUnknownError(err) });
	}
	const conversationType = normalizeOptionalLowercaseString(params.conversationRef.conversation?.conversationType);
	if (!(conversationType === "personal" && parsedReflection.followUp && Boolean(parsedReflection.userMessage))) {
		if (parsedReflection.followUp && conversationType !== "personal") log.debug?.("skipping reflection follow-up outside direct message", {
			sessionKey,
			conversationType
		});
		return;
	}
	try {
		await sendReflectionFollowUp({
			cfg,
			app: params.app,
			conversationRef: params.conversationRef,
			userMessage: parsedReflection.userMessage
		});
		log.info("sent reflection follow-up", { sessionKey });
	} catch (err) {
		log.debug?.("failed to send reflection follow-up", { error: formatUnknownError(err) });
	}
}
//#endregion
//#region extensions/msteams/src/inbound.ts
/**
* Decode common HTML entities to plain text.
*/
function decodeHtmlEntities(html) {
	return html.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
}
/**
* Strip HTML tags, preserving text content.
*/
function htmlToPlainText(html) {
	return decodeHtmlEntities(html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim());
}
/**
* Extract quote info from MS Teams HTML reply attachments.
* Teams wraps quoted content in a blockquote with itemtype="http://schema.skype.com/Reply".
*/
function extractMSTeamsQuoteInfo(attachments) {
	for (const att of attachments) {
		let content = "";
		if (typeof att.content === "string") content = att.content;
		else if (typeof att.content === "object" && att.content !== null) {
			const record = att.content;
			content = typeof record.text === "string" ? record.text : typeof record.body === "string" ? record.body : "";
		}
		if (!content) continue;
		if (!content.includes("http://schema.skype.com/Reply")) continue;
		const senderMatch = /<strong[^>]*itemprop=["']mri["'][^>]*>(.*?)<\/strong>/i.exec(content);
		const sender = senderMatch?.[1] ? htmlToPlainText(senderMatch[1]) : void 0;
		const bodyMatch = /<p[^>]*itemprop=["']copy["'][^>]*>(.*?)<\/p>/is.exec(content);
		const body = bodyMatch?.[1] ? htmlToPlainText(bodyMatch[1]) : void 0;
		if (body) return {
			sender: sender ?? "unknown",
			body
		};
	}
}
function normalizeMSTeamsConversationId(raw) {
	return raw.split(";")[0] ?? raw;
}
function extractMSTeamsConversationMessageId(raw) {
	if (!raw) return;
	return (/(?:^|;)messageid=([^;]+)/i.exec(raw)?.[1]?.trim() ?? "") || void 0;
}
function parseMSTeamsActivityTimestamp(value) {
	if (!value) return;
	if (value instanceof Date) return value;
	if (typeof value !== "string") return;
	const date = new Date(value);
	return Number.isNaN(date.getTime()) ? void 0 : date;
}
function stripMSTeamsMentionTags(text) {
	return text.replace(/<at[^>]*>.*?<\/at>/gi, "").trim();
}
/**
* Bot Framework uses 'a:xxx' conversation IDs for personal chats, but Graph API
* requires the '19:{userId}_{botAppId}@unq.gbl.spaces' format.
*
* This is the documented Graph API format for 1:1 chat thread IDs between a user
* and a bot/app. See Microsoft docs "Get chat between user and app":
* https://learn.microsoft.com/en-us/graph/api/userscopeteamsappinstallation-get-chat
*
* The format is only synthesized when the Bot Framework conversation ID starts with
* 'a:' (the opaque format used by BF but not recognized by Graph). If the ID already
* has the '19:...' Graph format, it is passed through unchanged.
*/
function translateMSTeamsDmConversationIdForGraph(params) {
	const { isDirectMessage, conversationId, aadObjectId, appId } = params;
	return isDirectMessage && conversationId.startsWith("a:") && aadObjectId && appId ? `19:${aadObjectId}_${appId}@unq.gbl.spaces` : conversationId;
}
function wasMSTeamsBotMentioned(activity) {
	const botId = activity.recipient?.id;
	if (!botId) return false;
	return (activity.entities ?? []).some((e) => e.type === "mention" && e.mentioned?.id === botId);
}
//#endregion
//#region extensions/msteams/src/monitor-handler/access.ts
const msteamsIngressIdentity = {
	key: "sender-id",
	normalize: normalizeIngressValue,
	aliases: [{
		key: "sender-name",
		kind: "plugin:msteams-sender-name",
		normalizeEntry: normalizeIngressValue,
		normalizeSubject: normalizeIngressValue,
		dangerous: true
	}],
	isWildcardEntry: (entry) => normalizeIngressValue(entry) === "*",
	resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : "id"}`
};
function normalizeIngressValue(value) {
	return normalizeOptionalLowercaseString(value) ?? null;
}
async function resolveMSTeamsSenderAccess(params) {
	const activity = params.activity;
	const msteamsCfg = params.cfg.channels?.msteams;
	const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
	const convType = normalizeOptionalLowercaseString(activity.conversation?.conversationType);
	const isDirectMessage = convType === "personal" || !convType && !activity.conversation?.isGroup;
	const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
	const senderName = activity.from?.name ?? activity.from?.id ?? senderId;
	const pairing = createChannelPairingController({
		core: getMSTeamsRuntime(),
		channel: "msteams",
		accountId: DEFAULT_ACCOUNT_ID
	});
	const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
	const configuredDmAllowFrom = msteamsCfg?.allowFrom ?? [];
	const groupAllowFrom = msteamsCfg?.groupAllowFrom;
	const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
	const groupPolicy = !isDirectMessage && msteamsCfg ? msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist" : "disabled";
	const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
	const channelGate = resolveMSTeamsRouteConfig({
		cfg: msteamsCfg,
		teamId: activity.channelData?.team?.id,
		teamName: activity.channelData?.team?.name,
		conversationId,
		channelName: activity.channelData?.channel?.name,
		allowNameMatching
	});
	return {
		...await resolveStableChannelMessageIngress({
			channelId: "msteams",
			accountId: pairing.accountId,
			identity: msteamsIngressIdentity,
			cfg: params.cfg,
			readStoreAllowFrom: pairing.readAllowFromStore,
			subject: {
				stableId: senderId,
				aliases: { "sender-name": senderName }
			},
			conversation: {
				kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
				id: conversationId,
				parentId: activity.channelData?.team?.id
			},
			route: channelIngressRoutes(!isDirectMessage && channelGate.allowlistConfigured && {
				id: "msteams:team-channel",
				kind: "nestedAllowlist",
				allowed: channelGate.allowed,
				precedence: 0,
				matchId: "msteams-route",
				...channelGate.allowed && groupPolicy === "allowlist" ? {
					senderPolicy: "deny-when-empty",
					senderAllowFromSource: "effective-group"
				} : {}
			}),
			dmPolicy,
			groupPolicy,
			policy: {
				groupAllowFromFallbackToAllowFrom: true,
				mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled"
			},
			allowFrom: configuredDmAllowFrom,
			groupAllowFrom,
			command: {
				allowTextCommands: true,
				hasControlCommand: params.hasControlCommand === true,
				directGroupAllowFrom: isDirectMessage ? "effective" : "none"
			}
		}),
		pairing,
		isDirectMessage,
		conversationId,
		senderId,
		senderName,
		msteamsCfg,
		dmPolicy,
		channelGate,
		allowNameMatching,
		groupPolicy
	};
}
//#endregion
//#region extensions/msteams/src/attachments/bot-framework.ts
/**
* Bot Framework Service token scope for requesting a token used against
* the Bot Connector (v3) REST endpoints such as `/v3/attachments/{id}`.
*/
const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
/**
* Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
* IDs. These identifiers are not recognized by Graph's `/chats/{id}` endpoint, so we
* must fetch media via the Bot Framework v3 attachments endpoint instead.
*
* Graph-compatible IDs start with `19:` and are left untouched by this detector.
*/
function isBotFrameworkPersonalChatId(conversationId) {
	if (typeof conversationId !== "string") return false;
	const trimmed = conversationId.trim();
	return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
}
function normalizeServiceUrl(serviceUrl) {
	return serviceUrl.replace(/\/+$/, "");
}
function buildBotFrameworkAttachmentHeaders(params) {
	const headers = ensureUserAgentHeader();
	applyAuthorizationHeaderForUrl({
		headers,
		url: params.url,
		authAllowHosts: params.policy.authAllowHosts,
		bearerToken: params.accessToken
	});
	return headers;
}
async function fetchBotFrameworkAttachmentInfo(params) {
	const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
	let response;
	try {
		response = await safeFetchWithPolicy({
			url,
			policy: params.policy,
			fetchFn: params.fetchFn,
			fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
			resolveFn: params.resolveFn,
			requestInit: { headers: buildBotFrameworkAttachmentHeaders({
				url,
				accessToken: params.accessToken,
				policy: params.policy
			}) }
		});
	} catch (err) {
		params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
	if (!response.ok) {
		await response.body?.cancel();
		params.logger?.warn?.("msteams botFramework attachmentInfo non-ok", { status: response.status });
		return;
	}
	try {
		return await response.json();
	} catch (err) {
		params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
}
async function saveBotFrameworkAttachmentView(params) {
	const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}/views/${encodeURIComponent(params.viewId)}`;
	let response;
	try {
		response = await safeFetchWithPolicy({
			url,
			policy: params.policy,
			fetchFn: params.fetchFn,
			fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
			resolveFn: params.resolveFn,
			requestInit: { headers: buildBotFrameworkAttachmentHeaders({
				url,
				accessToken: params.accessToken,
				policy: params.policy
			}) }
		});
	} catch (err) {
		params.logger?.warn?.("msteams botFramework attachmentView fetch failed", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
	if (!response.ok) {
		await response.body?.cancel();
		params.logger?.warn?.("msteams botFramework attachmentView non-ok", { status: response.status });
		return;
	}
	let contentLength;
	try {
		contentLength = parseMediaContentLength(response.headers.get("content-length"));
	} catch (err) {
		await response.body?.cancel();
		params.logger?.warn?.("msteams botFramework attachmentView invalid content-length", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
	if (contentLength !== null && contentLength > params.maxBytes) {
		await response.body?.cancel();
		return;
	}
	try {
		return await getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
			sourceUrl: url,
			filePathHint: params.fileNameHint,
			maxBytes: params.maxBytes,
			fallbackContentType: params.contentTypeHint,
			subdir: "inbound",
			originalFilename: params.preserveFilenames ? params.fileNameHint : void 0
		});
	} catch (err) {
		params.logger?.warn?.("msteams botFramework attachmentView save failed", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
}
/**
* Download media for a single attachment via the Bot Framework v3 attachments
* endpoint. Used for personal DM conversations where the Graph `/chats/{id}`
* path is not usable because the Bot Framework conversation ID (`a:...`) is
* not a valid Graph chat identifier.
*/
async function downloadMSTeamsBotFrameworkAttachment(params) {
	if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) return;
	const policy = resolveAttachmentFetchPolicy({
		allowHosts: params.allowHosts,
		authAllowHosts: params.authAllowHosts
	});
	if (!isUrlAllowed(`${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`, policy.allowHosts)) return;
	let accessToken;
	try {
		accessToken = await params.tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE);
	} catch (err) {
		params.logger?.warn?.("msteams botFramework token acquisition failed", { error: err instanceof Error ? err.message : String(err) });
		return;
	}
	if (!accessToken) return;
	const info = await fetchBotFrameworkAttachmentInfo({
		serviceUrl: params.serviceUrl,
		attachmentId: params.attachmentId,
		accessToken,
		policy,
		fetchFn: params.fetchFn,
		fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
		resolveFn: params.resolveFn,
		logger: params.logger
	});
	if (!info) return;
	const views = Array.isArray(info.views) ? info.views : [];
	const candidateView = views.find((view) => view?.viewId === "original") ?? views.find((view) => typeof view?.viewId === "string");
	const viewId = typeof candidateView?.viewId === "string" && candidateView.viewId ? candidateView.viewId : void 0;
	if (!viewId) return;
	if (typeof candidateView?.size === "number" && candidateView.size > 0 && candidateView.size > params.maxBytes) return;
	const fileNameHint = typeof params.fileNameHint === "string" && params.fileNameHint || typeof info.name === "string" && info.name || void 0;
	const contentTypeHint = typeof params.contentTypeHint === "string" && params.contentTypeHint || typeof info.type === "string" && info.type || void 0;
	const saved = await saveBotFrameworkAttachmentView({
		serviceUrl: params.serviceUrl,
		attachmentId: params.attachmentId,
		viewId,
		accessToken,
		maxBytes: params.maxBytes,
		fileNameHint,
		contentTypeHint,
		preserveFilenames: params.preserveFilenames,
		policy,
		fetchFn: params.fetchFn,
		fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
		resolveFn: params.resolveFn,
		logger: params.logger
	});
	if (!saved) return;
	return {
		path: saved.path,
		contentType: saved.contentType,
		placeholder: inferPlaceholder({
			contentType: saved.contentType,
			fileName: fileNameHint
		})
	};
}
/**
* Download media for every attachment referenced by a Bot Framework personal
* chat activity. Returns all successfully fetched media along with diagnostics
* compatible with `downloadMSTeamsGraphMedia`'s result shape so callers can
* reuse the existing logging path.
*/
async function downloadMSTeamsBotFrameworkAttachments(params) {
	const seen = /* @__PURE__ */ new Set();
	const unique = [];
	for (const id of params.attachmentIds ?? []) {
		if (typeof id !== "string") continue;
		const trimmed = id.trim();
		if (!trimmed || seen.has(trimmed)) continue;
		seen.add(trimmed);
		unique.push(trimmed);
	}
	if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) return {
		media: [],
		attachmentCount: unique.length
	};
	const media = [];
	for (const attachmentId of unique) try {
		const item = await downloadMSTeamsBotFrameworkAttachment({
			serviceUrl: params.serviceUrl,
			attachmentId,
			tokenProvider: params.tokenProvider,
			maxBytes: params.maxBytes,
			allowHosts: params.allowHosts,
			authAllowHosts: params.authAllowHosts,
			fetchFn: params.fetchFn,
			fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
			resolveFn: params.resolveFn,
			fileNameHint: params.fileNameHint,
			contentTypeHint: params.contentTypeHint,
			preserveFilenames: params.preserveFilenames,
			logger: params.logger
		});
		if (item) media.push(item);
	} catch (err) {
		params.logger?.warn?.("msteams botFramework attachment download failed", {
			error: err instanceof Error ? err.message : String(err),
			attachmentId
		});
	}
	return {
		media,
		attachmentCount: unique.length
	};
}
//#endregion
//#region extensions/msteams/src/attachments/remote-media.ts
/**
* Direct save path used when the caller supplies the already-guarded fetch
* implementation. This lets Teams-specific auth fallback own the request
* sequence while keeping redirect and DNS pinning inside `safeFetchWithPolicy`.
*/
async function saveRemoteMediaDirect(params) {
	return await saveResponseMedia(await params.fetchImpl(params.url, { redirect: "follow" }), {
		sourceUrl: params.url,
		filePathHint: params.filePathHint,
		maxBytes: params.maxBytes,
		fallbackContentType: params.contentTypeHint,
		originalFilename: params.originalFilename
	});
}
async function downloadAndStoreMSTeamsRemoteMedia(params) {
	const originalFilename = params.preserveFilenames ? params.filePathHint : void 0;
	let saved;
	if (params.useDirectFetch && params.fetchImpl) saved = await saveRemoteMediaDirect({
		url: params.url,
		filePathHint: params.filePathHint,
		fetchImpl: params.fetchImpl,
		maxBytes: params.maxBytes,
		contentTypeHint: params.contentTypeHint,
		originalFilename
	});
	else saved = await getMSTeamsRuntime().channel.media.saveRemoteMedia({
		url: params.url,
		fetchImpl: params.fetchImpl,
		filePathHint: params.filePathHint,
		maxBytes: params.maxBytes,
		ssrfPolicy: params.ssrfPolicy,
		fallbackContentType: params.contentTypeHint,
		originalFilename
	});
	return {
		path: saved.path,
		contentType: saved.contentType,
		placeholder: params.placeholder ?? inferPlaceholder({
			contentType: saved.contentType,
			fileName: params.filePathHint
		})
	};
}
//#endregion
//#region extensions/msteams/src/attachments/download.ts
function resolveDownloadCandidate(att) {
	const contentType = normalizeContentType(att.contentType);
	const name = normalizeOptionalString(att.name) ?? "";
	if (contentType === "application/vnd.microsoft.teams.file.download.info") {
		if (!isRecord(att.content)) return null;
		const downloadUrl = normalizeOptionalString(att.content.downloadUrl) ?? "";
		if (!downloadUrl) return null;
		const fileType = normalizeOptionalString(att.content.fileType) ?? "";
		const uniqueId = normalizeOptionalString(att.content.uniqueId) ?? "";
		const fileName = normalizeOptionalString(att.content.fileName) ?? "";
		const fileHint = name || fileName || (uniqueId && fileType ? `${uniqueId}.${fileType}` : "");
		return {
			url: downloadUrl,
			fileHint: fileHint || void 0,
			contentTypeHint: void 0,
			placeholder: inferPlaceholder({
				contentType,
				fileName: fileHint,
				fileType
			})
		};
	}
	const contentUrl = normalizeOptionalString(att.contentUrl) ?? "";
	if (!contentUrl) return null;
	const sharesUrl = tryBuildGraphSharesUrlForSharedLink(contentUrl);
	return {
		url: sharesUrl ?? contentUrl,
		fileHint: name || void 0,
		contentTypeHint: sharesUrl ? void 0 : contentType,
		placeholder: inferPlaceholder({
			contentType,
			fileName: name
		})
	};
}
function scopeCandidatesForUrl(url) {
	try {
		const host = normalizeLowercaseStringOrEmpty(new URL(url).hostname);
		return host.endsWith("graph.microsoft.com") || host.endsWith("sharepoint.com") || host.endsWith("1drv.ms") || host.includes("sharepoint") ? ["https://graph.microsoft.com", "https://api.botframework.com"] : ["https://api.botframework.com", "https://graph.microsoft.com"];
	} catch {
		return ["https://api.botframework.com", "https://graph.microsoft.com"];
	}
}
function isRedirectStatus(status) {
	return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
}
async function resolveInlineDataImageMime(inline) {
	const mime = normalizeOptionalLowercaseString(await getMSTeamsRuntime().media.detectMime({
		buffer: inline.data,
		headerMime: inline.contentType
	}) ?? inline.contentType);
	return mime?.startsWith("image/") ? mime : void 0;
}
async function fetchWithAuthFallback(params) {
	const firstAttempt = await safeFetchWithPolicy({
		url: params.url,
		policy: params.policy,
		fetchFn: params.fetchFn,
		fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
		requestInit: params.requestInit,
		resolveFn: params.resolveFn
	});
	if (firstAttempt.ok) return firstAttempt;
	if (!params.tokenProvider) return firstAttempt;
	if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt;
	if (!isUrlAllowed(params.url, params.policy.authAllowHosts)) return firstAttempt;
	await firstAttempt.body?.cancel();
	const scopes = scopeCandidatesForUrl(params.url);
	const fetchFn = params.fetchFn ?? fetch;
	for (const scope of scopes) try {
		const token = await params.tokenProvider.getAccessToken(scope);
		const authHeaders = new Headers(params.requestInit?.headers);
		authHeaders.set("Authorization", `Bearer ${token}`);
		const authAttempt = await safeFetchWithPolicy({
			url: params.url,
			policy: params.policy,
			fetchFn,
			fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
			requestInit: {
				...params.requestInit,
				headers: authHeaders
			},
			resolveFn: params.resolveFn
		});
		if (authAttempt.ok) return authAttempt;
		if (isRedirectStatus(authAttempt.status)) return authAttempt;
		if (authAttempt.status !== 401 && authAttempt.status !== 403) {
			await authAttempt.body?.cancel();
			continue;
		}
		await authAttempt.body?.cancel();
	} catch {}
	return firstAttempt;
}
/**
* Download all file attachments from a Teams message (images, documents, etc.).
* Renamed from downloadMSTeamsImageAttachments to support all file types.
*/
async function downloadMSTeamsAttachments(params) {
	const list = Array.isArray(params.attachments) ? params.attachments : [];
	if (list.length === 0) return [];
	const policy = resolveAttachmentFetchPolicy({
		allowHosts: params.allowHosts,
		authAllowHosts: params.authAllowHosts
	});
	const allowHosts = policy.allowHosts;
	const ssrfPolicy = resolveMediaSsrfPolicy(allowHosts);
	const candidates = list.filter(isDownloadableAttachment).map(resolveDownloadCandidate).filter(Boolean);
	const inlineCandidates = extractInlineImageCandidates(list, {
		maxInlineBytes: params.maxBytes,
		maxInlineTotalBytes: params.maxBytes
	});
	const seenUrls = /* @__PURE__ */ new Set();
	for (const inline of inlineCandidates) if (inline.kind === "url") {
		if (!isUrlAllowed(inline.url, allowHosts)) continue;
		if (seenUrls.has(inline.url)) continue;
		seenUrls.add(inline.url);
		candidates.push({
			url: inline.url,
			fileHint: inline.fileHint,
			contentTypeHint: inline.contentType,
			placeholder: inline.placeholder
		});
	}
	if (candidates.length === 0 && inlineCandidates.length === 0) return [];
	const out = [];
	for (const inline of inlineCandidates) {
		if (inline.kind !== "data") continue;
		if (inline.data.byteLength > params.maxBytes) continue;
		try {
			const contentType = await resolveInlineDataImageMime(inline);
			if (!contentType) continue;
			const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(inline.data, contentType, "inbound", params.maxBytes);
			out.push({
				path: saved.path,
				contentType: saved.contentType,
				placeholder: inferPlaceholder({ contentType: saved.contentType ?? contentType })
			});
		} catch (err) {
			params.logger?.warn?.("msteams inline attachment decode failed", { error: err instanceof Error ? err.message : String(err) });
		}
	}
	for (const candidate of candidates) {
		if (!isUrlAllowed(candidate.url, allowHosts)) continue;
		try {
			const media = await downloadAndStoreMSTeamsRemoteMedia({
				url: candidate.url,
				filePathHint: candidate.fileHint ?? candidate.url,
				maxBytes: params.maxBytes,
				contentTypeHint: candidate.contentTypeHint,
				placeholder: candidate.placeholder,
				preserveFilenames: params.preserveFilenames,
				ssrfPolicy,
				useDirectFetch: true,
				fetchImpl: (input, init) => fetchWithAuthFallback({
					url: resolveRequestUrl(input),
					tokenProvider: params.tokenProvider,
					fetchFn: params.fetchFn,
					fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
					requestInit: init,
					resolveFn: params.resolveFn,
					policy
				})
			});
			out.push(media);
		} catch (err) {
			const msg = err instanceof Error ? err.message : String(err);
			params.logger?.warn?.(`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`);
		}
	}
	return out;
}
function safeHostForLog(url) {
	try {
		return new URL(url).host;
	} catch {
		return "invalid-url";
	}
}
//#endregion
//#region extensions/msteams/src/attachments/graph.ts
function buildMSTeamsGraphMessageUrls(params) {
	const conversationType = normalizeLowercaseStringOrEmpty(params.conversationType ?? "");
	const messageIdCandidates = /* @__PURE__ */ new Set();
	const pushCandidate = (value) => {
		const trimmed = normalizeOptionalString(value) ?? "";
		if (trimmed) messageIdCandidates.add(trimmed);
	};
	pushCandidate(params.messageId);
	pushCandidate(params.conversationMessageId);
	pushCandidate(readNestedString(params.channelData, ["messageId"]));
	pushCandidate(readNestedString(params.channelData, ["teamsMessageId"]));
	const replyToId = normalizeOptionalString(params.replyToId) ?? "";
	if (conversationType === "channel") {
		const teamId = readNestedString(params.channelData, ["team", "id"]) ?? readNestedString(params.channelData, ["teamId"]);
		const channelId = readNestedString(params.channelData, ["channel", "id"]) ?? readNestedString(params.channelData, ["channelId"]) ?? readNestedString(params.channelData, ["teamsChannelId"]);
		if (!teamId || !channelId) return [];
		const urls = [];
		if (replyToId) for (const candidate of messageIdCandidates) {
			if (candidate === replyToId) continue;
			urls.push(`${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(replyToId)}/replies/${encodeURIComponent(candidate)}`);
		}
		if (messageIdCandidates.size === 0 && replyToId) messageIdCandidates.add(replyToId);
		for (const candidate of messageIdCandidates) urls.push(`${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(candidate)}`);
		return uniqueStrings(urls);
	}
	const chatId = params.conversationId?.trim() || readNestedString(params.channelData, ["chatId"]);
	if (!chatId) return [];
	if (messageIdCandidates.size === 0 && replyToId) messageIdCandidates.add(replyToId);
	return uniqueStrings(Array.from(messageIdCandidates).map((candidate) => `${GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(candidate)}`));
}
async function fetchGraphCollection(params) {
	const fetchFn = params.fetchFn ?? fetch;
	const { response, release } = await fetchWithSsrFGuard({
		url: params.url,
		fetchImpl: fetchFn,
		init: { headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }) },
		policy: params.ssrfPolicy,
		auditContext: "msteams.graph.collection"
	});
	try {
		const status = response.status;
		if (!response.ok) return {
			status,
			items: []
		};
		try {
			const data = await response.json();
			return {
				status,
				items: Array.isArray(data.value) ? data.value : []
			};
		} catch {
			return {
				status,
				items: []
			};
		}
	} finally {
		await release();
	}
}
function normalizeGraphAttachment(att) {
	let content = att.content;
	if (typeof content === "string") try {
		content = JSON.parse(content);
	} catch {}
	return {
		contentType: normalizeContentType(att.contentType) ?? void 0,
		contentUrl: att.contentUrl ?? void 0,
		name: att.name ?? void 0,
		thumbnailUrl: att.thumbnailUrl ?? void 0,
		content
	};
}
/**
* Download all hosted content from a Teams message (images, documents, etc.).
* Renamed from downloadGraphHostedImages to support all file types.
*/
async function downloadGraphHostedContent(params) {
	const hosted = await fetchGraphCollection({
		url: `${params.messageUrl}/hostedContents`,
		accessToken: params.accessToken,
		fetchFn: params.fetchFn,
		ssrfPolicy: params.ssrfPolicy
	});
	if (hosted.items.length === 0) return {
		media: [],
		status: hosted.status,
		count: 0
	};
	const out = [];
	for (const item of hosted.items) {
		const contentBytes = typeof item.contentBytes === "string" ? item.contentBytes : "";
		let buffer;
		if (contentBytes) {
			if (estimateBase64DecodedBytes(contentBytes) > params.maxBytes) continue;
			try {
				buffer = Buffer.from(contentBytes, "base64");
			} catch (err) {
				params.logger?.warn?.("msteams graph hostedContent base64 decode failed", { error: err instanceof Error ? err.message : String(err) });
				continue;
			}
		} else if (item.id) {
			try {
				const valueUrl = `${params.messageUrl}/hostedContents/${encodeURIComponent(item.id)}/$value`;
				const { response: valRes, release } = await fetchWithSsrFGuard({
					url: valueUrl,
					fetchImpl: params.fetchFn ?? fetch,
					init: { headers: ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }) },
					policy: params.ssrfPolicy,
					auditContext: "msteams.graph.hostedContent.value"
				});
				try {
					if (!valRes.ok) continue;
					const saved = await getMSTeamsRuntime().channel.media.saveResponseMedia(valRes, {
						sourceUrl: valueUrl,
						maxBytes: params.maxBytes,
						fallbackContentType: item.contentType ?? void 0,
						subdir: "inbound"
					});
					out.push({
						path: saved.path,
						contentType: saved.contentType,
						placeholder: inferPlaceholder({ contentType: saved.contentType })
					});
				} finally {
					await release();
				}
			} catch (err) {
				params.logger?.warn?.("msteams graph hostedContent value fetch failed", { error: err instanceof Error ? err.message : String(err) });
				continue;
			}
			continue;
		} else continue;
		if (buffer.byteLength > params.maxBytes) continue;
		const mime = await getMSTeamsRuntime().media.detectMime({
			buffer,
			headerMime: item.contentType ?? void 0
		});
		try {
			const saved = await getMSTeamsRuntime().channel.media.saveMediaBuffer(buffer, mime ?? item.contentType ?? void 0, "inbound", params.maxBytes);
			out.push({
				path: saved.path,
				contentType: saved.contentType,
				placeholder: inferPlaceholder({ contentType: saved.contentType })
			});
		} catch (err) {
			params.logger?.warn?.("msteams graph hostedContent save failed", { error: err instanceof Error ? err.message : String(err) });
		}
	}
	return {
		media: out,
		status: hosted.status,
		count: hosted.items.length
	};
}
async function downloadMSTeamsGraphMedia(params) {
	if (!params.messageUrl || !params.tokenProvider) return { media: [] };
	const policy = resolveAttachmentFetchPolicy({
		allowHosts: params.allowHosts,
		authAllowHosts: params.authAllowHosts
	});
	const ssrfPolicy = resolveMediaSsrfPolicy(policy.allowHosts);
	const messageUrl = params.messageUrl;
	const debugLog = params.log ?? params.logger ?? void 0;
	let accessToken;
	try {
		accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com");
	} catch (err) {
		debugLog?.debug?.("graph media token acquisition failed", {
			messageUrl,
			error: err instanceof Error ? err.message : String(err)
		});
		params.logger?.warn?.("msteams graph token acquisition failed", { error: err instanceof Error ? err.message : String(err) });
		return {
			media: [],
			messageUrl,
			tokenError: true
		};
	}
	const fetchFn = params.fetchFn ?? fetch;
	const sharePointMedia = [];
	const downloadedReferenceUrls = /* @__PURE__ */ new Set();
	let messageAttachments = [];
	let messageStatus;
	try {
		const { response: msgRes, release } = await fetchWithSsrFGuard({
			url: messageUrl,
			fetchImpl: fetchFn,
			init: { headers: ensureUserAgentHeader({ Authorization: `Bearer ${accessToken}` }) },
			policy: ssrfPolicy,
			auditContext: "msteams.graph.message"
		});
		try {
			messageStatus = msgRes.status;
			if (msgRes.ok) {
				let msgData;
				try {
					msgData = await msgRes.json();
				} catch (err) {
					debugLog?.debug?.("graph media message parse failed", {
						messageUrl,
						error: err instanceof Error ? err.message : String(err)
					});
					params.logger?.warn?.("msteams graph message parse failed", {
						error: err instanceof Error ? err.message : String(err),
						messageUrl
					});
					msgData = {};
				}
				messageAttachments = Array.isArray(msgData.attachments) ? msgData.attachments : [];
				const spAttachments = messageAttachments.filter((a) => a.contentType === "reference" && a.contentUrl && a.name);
				for (const att of spAttachments) {
					const name = att.name ?? "file";
					const shareUrl = att.contentUrl ?? "";
					if (!shareUrl) continue;
					try {
						const sharesUrl = `${GRAPH_ROOT}/shares/${encodeGraphShareId(shareUrl)}/driveItem/content`;
						if (!isUrlAllowed(sharesUrl, policy.allowHosts)) {
							debugLog?.debug?.("graph media sharepoint url not in allowHosts", {
								messageUrl,
								sharesUrl
							});
							continue;
						}
						const media = await downloadAndStoreMSTeamsRemoteMedia({
							url: sharesUrl,
							filePathHint: name,
							maxBytes: params.maxBytes,
							contentTypeHint: "application/octet-stream",
							preserveFilenames: params.preserveFilenames,
							ssrfPolicy,
							useDirectFetch: true,
							fetchImpl: async (input, init) => {
								const requestUrl = resolveRequestUrl(input);
								const headers = ensureUserAgentHeader(init?.headers);
								applyAuthorizationHeaderForUrl({
									headers,
									url: requestUrl,
									authAllowHosts: policy.authAllowHosts,
									bearerToken: accessToken
								});
								return await safeFetchWithPolicy({
									url: requestUrl,
									policy,
									fetchFn,
									fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
									requestInit: {
										...init,
										headers
									},
									resolveFn: params.resolveFn
								});
							}
						});
						sharePointMedia.push(media);
						downloadedReferenceUrls.add(shareUrl);
					} catch (err) {
						params.logger?.warn?.("msteams SharePoint reference download failed", {
							error: err instanceof Error ? err.message : String(err),
							name
						});
					}
				}
			} else debugLog?.debug?.("graph media message fetch not ok", {
				messageUrl,
				status: messageStatus
			});
		} finally {
			await release();
		}
	} catch (err) {
		debugLog?.debug?.("graph media message fetch failed", {
			messageUrl,
			error: err instanceof Error ? err.message : String(err)
		});
		params.logger?.warn?.("msteams graph message fetch failed", { error: err instanceof Error ? err.message : String(err) });
	}
	const hosted = await downloadGraphHostedContent({
		accessToken,
		messageUrl,
		maxBytes: params.maxBytes,
		fetchFn: params.fetchFn,
		preserveFilenames: params.preserveFilenames,
		ssrfPolicy,
		logger: params.logger
	});
	const normalizedAttachments = messageAttachments.map(normalizeGraphAttachment);
	const filteredAttachments = sharePointMedia.length > 0 ? normalizedAttachments.filter((att) => {
		if (normalizeOptionalLowercaseString(att.contentType) !== "reference") return true;
		const url = typeof att.contentUrl === "string" ? att.contentUrl : "";
		if (!url) return true;
		return !downloadedReferenceUrls.has(url);
	}) : normalizedAttachments;
	let attachmentMedia = [];
	try {
		attachmentMedia = await downloadMSTeamsAttachments({
			attachments: filteredAttachments,
			maxBytes: params.maxBytes,
			tokenProvider: params.tokenProvider,
			allowHosts: policy.allowHosts,
			authAllowHosts: policy.authAllowHosts,
			fetchFn: params.fetchFn,
			fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
			resolveFn: params.resolveFn,
			preserveFilenames: params.preserveFilenames,
			logger: params.logger
		});
	} catch (err) {
		params.logger?.warn?.("msteams graph attachment download failed", {
			error: err instanceof Error ? err.message : String(err),
			messageUrl
		});
	}
	return {
		media: [
			...sharePointMedia,
			...hosted.media,
			...attachmentMedia
		],
		hostedCount: hosted.count,
		attachmentCount: filteredAttachments.length + sharePointMedia.length,
		hostedStatus: hosted.status,
		attachmentStatus: messageStatus,
		messageUrl
	};
}
//#endregion
//#region extensions/msteams/src/attachments/html.ts
/**
* Extract every `<attachment id="...">` reference from the HTML attachments in
* the inbound activity. Returns the complete (non-sliced) list; callers that
* need a capped diagnostic summary can truncate after calling this helper.
*/
function extractMSTeamsHtmlAttachmentIds(attachments) {
	const list = Array.isArray(attachments) ? attachments : [];
	if (list.length === 0) return [];
	const ids = /* @__PURE__ */ new Set();
	for (const att of list) {
		const html = extractHtmlFromAttachment(att);
		if (!html) continue;
		ATTACHMENT_TAG_RE.lastIndex = 0;
		let match = ATTACHMENT_TAG_RE.exec(html);
		while (match) {
			const id = match[1]?.trim();
			if (id) ids.add(id);
			match = ATTACHMENT_TAG_RE.exec(html);
		}
	}
	return Array.from(ids);
}
function summarizeMSTeamsHtmlAttachments(attachments) {
	const list = Array.isArray(attachments) ? attachments : [];
	if (list.length === 0) return;
	let htmlAttachments = 0;
	let imgTags = 0;
	let dataImages = 0;
	let cidImages = 0;
	const srcHosts = /* @__PURE__ */ new Set();
	let attachmentTags = 0;
	const attachmentIds = /* @__PURE__ */ new Set();
	for (const att of list) {
		const html = extractHtmlFromAttachment(att);
		if (!html) continue;
		htmlAttachments += 1;
		IMG_SRC_RE.lastIndex = 0;
		let match = IMG_SRC_RE.exec(html);
		while (match) {
			imgTags += 1;
			const src = match[1]?.trim();
			if (src) if (src.startsWith("data:")) dataImages += 1;
			else if (src.startsWith("cid:")) cidImages += 1;
			else srcHosts.add(safeHostForUrl(src));
			match = IMG_SRC_RE.exec(html);
		}
		ATTACHMENT_TAG_RE.lastIndex = 0;
		let attachmentMatch = ATTACHMENT_TAG_RE.exec(html);
		while (attachmentMatch) {
			attachmentTags += 1;
			const id = attachmentMatch[1]?.trim();
			if (id) attachmentIds.add(id);
			attachmentMatch = ATTACHMENT_TAG_RE.exec(html);
		}
	}
	if (htmlAttachments === 0) return;
	return {
		htmlAttachments,
		imgTags,
		dataImages,
		cidImages,
		srcHosts: Array.from(srcHosts).slice(0, 5),
		attachmentTags,
		attachmentIds: Array.from(attachmentIds).slice(0, 5)
	};
}
function buildMSTeamsAttachmentPlaceholder(attachments, limits) {
	const list = Array.isArray(attachments) ? attachments : [];
	if (list.length === 0) return "";
	const totalImages = list.filter(isLikelyImageAttachment).length + extractInlineImageCandidates(list, limits).length;
	if (totalImages > 0) return `<media:image>${totalImages > 1 ? ` (${totalImages} images)` : ""}`;
	const count = list.length;
	return `<media:document>${count > 1 ? ` (${count} files)` : ""}`;
}
//#endregion
//#region extensions/msteams/src/attachments/payload.ts
function buildMSTeamsMediaPayload(mediaList) {
	return buildMediaPayload(mediaList, { preserveMediaTypeCardinality: true });
}
//#endregion
//#region extensions/msteams/src/graph-thread.ts
const teamGroupIdCache = /* @__PURE__ */ new Map();
const CACHE_TTL_MS = 600 * 1e3;
function resolveTeamGroupIdCacheExpiresAt(nowRaw = Date.now()) {
	const now = asDateTimestampMs(nowRaw);
	return now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(CACHE_TTL_MS, { nowMs: now });
}
/**
* Strip HTML tags from Teams message content, preserving @mention display names.
* Teams wraps mentions in <at>Name</at> tags.
*/
function stripHtmlFromTeamsMessage(html) {
	let text = html.replace(/<at[^>]*>(.*?)<\/at>/gi, "@$1");
	text = text.replace(/<[^>]*>/g, " ");
	text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
	return text.replace(/\s+/g, " ").trim();
}
/**
* Resolve the Azure AD group GUID for a Teams conversation team ID.
* Results are cached with a TTL to avoid repeated Graph API calls.
*/
async function resolveTeamGroupId(token, conversationTeamId) {
	const cached = teamGroupIdCache.get(conversationTeamId);
	if (cached) {
		const now = asDateTimestampMs(Date.now());
		const expiresAt = asDateTimestampMs(cached.expiresAt);
		if (now !== void 0 && expiresAt !== void 0 && expiresAt > now) return cached.groupId;
		teamGroupIdCache.delete(conversationTeamId);
	}
	try {
		const groupId = (await fetchGraphJson({
			token,
			path: `/teams/${encodeURIComponent(conversationTeamId)}?$select=id`
		})).id ?? conversationTeamId;
		const expiresAt = resolveTeamGroupIdCacheExpiresAt();
		if (expiresAt !== void 0) teamGroupIdCache.set(conversationTeamId, {
			groupId,
			expiresAt
		});
		return groupId;
	} catch {
		return conversationTeamId;
	}
}
/**
* Fetch a single channel message (the parent/root of a thread).
* Returns undefined on error so callers can degrade gracefully.
*/
async function fetchChannelMessage(token, groupId, channelId, messageId) {
	const path = `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}?$select=id,from,body,createdDateTime`;
	try {
		return await fetchGraphJson({
			token,
			path
		});
	} catch {
		return;
	}
}
/**
* Fetch thread replies for a channel message, ordered chronologically.
*
* **Limitation:** The Graph API replies endpoint (`/messages/{id}/replies`) does not
* support `$orderby`, so results are always returned in ascending (oldest-first) order.
* Combined with the `$top` cap of 50, this means only the **oldest 50 replies** are
* returned for long threads — newer replies are silently omitted. There is currently no
* Graph API workaround for this; pagination via `@odata.nextLink` can retrieve more
* replies but still in ascending order only.
*/
async function fetchThreadReplies(token, groupId, channelId, messageId, limit = 50) {
	return (await fetchGraphJson({
		token,
		path: `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies?$top=${Math.min(Math.max(limit, 1), 50)}&$select=id,from,body,createdDateTime`
	})).value ?? [];
}
/**
* Format thread messages into a context string for the agent.
* Skips the current message (by id) and blank messages.
*/
function formatThreadContext(messages, currentMessageId) {
	const lines = [];
	for (const msg of messages) {
		if (msg.id && msg.id === currentMessageId) continue;
		const sender = msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? "unknown";
		const contentType = msg.body?.contentType ?? "text";
		const rawContent = msg.body?.content ?? "";
		const content = contentType === "html" ? stripHtmlFromTeamsMessage(rawContent) : rawContent.trim();
		if (!content) continue;
		lines.push(`${sender}: ${content}`);
	}
	return lines.join("\n");
}
//#endregion
//#region extensions/msteams/src/thread-parent-context.ts
const PARENT_CACHE_TTL_MS = 300 * 1e3;
const PARENT_CACHE_MAX = 100;
const parentCache = /* @__PURE__ */ new Map();
const INJECTED_MAX = 200;
const injectedParents = /* @__PURE__ */ new Map();
function touchLru(map, key, value, max) {
	if (map.has(key)) map.delete(key);
	else if (map.size >= max) {
		const firstKey = map.keys().next().value;
		if (firstKey !== void 0) map.delete(firstKey);
	}
	map.set(key, value);
}
function buildParentCacheKey(groupId, channelId, parentId) {
	return `${groupId}\u0000${channelId}\u0000${parentId}`;
}
function resolveParentCacheExpiresAt(nowRaw) {
	const nowMs = asDateTimestampMs(nowRaw);
	return nowMs === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(PARENT_CACHE_TTL_MS, { nowMs });
}
/**
* Fetch a channel parent message with an LRU+TTL cache.
*
* Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
* tests can swap in a stub without mocking the Graph transport.
*/
async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = fetchChannelMessage) {
	const key = buildParentCacheKey(groupId, channelId, parentId);
	const now = asDateTimestampMs(Date.now());
	const cached = parentCache.get(key);
	const cachedExpiresAt = cached ? asDateTimestampMs(cached.expiresAt) : void 0;
	if (cached && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) {
		parentCache.delete(key);
		parentCache.set(key, cached);
		return cached.message;
	}
	if (cached) parentCache.delete(key);
	const message = await fetchParent(token, groupId, channelId, parentId);
	const expiresAt = resolveParentCacheExpiresAt(Date.now());
	if (expiresAt !== void 0) touchLru(parentCache, key, {
		message,
		expiresAt
	}, PARENT_CACHE_MAX);
	return message;
}
const PARENT_TEXT_MAX_CHARS = 400;
/**
* Extract a compact summary (sender + plain-text body) from a Graph parent
* message. Returns undefined when the parent cannot be summarized (missing
* or blank body).
*/
function summarizeParentMessage(message) {
	if (!message) return;
	const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
	const contentType = message.body?.contentType ?? "text";
	const raw = message.body?.content ?? "";
	const text = contentType === "html" ? stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
	if (!text) return;
	return {
		sender,
		text: text.length > PARENT_TEXT_MAX_CHARS ? `${text.slice(0, PARENT_TEXT_MAX_CHARS - 1)}…` : text
	};
}
/**
* Build the single-line `Replying to @sender: body` system event text.
* Callers should pass this text to `enqueueSystemEvent` together with a
* stable contextKey derived from the parent id.
*/
function formatParentContextEvent(summary) {
	return `Replying to @${summary.sender}: ${summary.text}`;
}
/**
* Decide whether a parent context event should be enqueued for the current
* session. Returns `false` when we already injected the same parent for this
* session recently (prevents re-prepending identical context on every reply
* in the thread).
*/
function shouldInjectParentContext(sessionKey, parentId) {
	const key = sessionKey;
	return injectedParents.get(key) !== parentId;
}
/**
* Record that `parentId` was just injected for `sessionKey` so subsequent
* replies with the same parent can short-circuit via `shouldInjectParentContext`.
*/
function markParentContextInjected(sessionKey, parentId) {
	touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
}
//#endregion
//#region extensions/msteams/src/reply-stream-controller.ts
function isStreamCancelledError(err) {
	return err instanceof Error && err.name === "StreamCancelledError";
}
/**
* Bridges openclaw's reply pipeline callbacks to the SDK's `ctx.stream`.
* Streaming is enabled for personal (DM) conversations only; group/channel
* messages fall through to block delivery.
*
* Streaming modes (resolved from `cfg.channels.msteams.streaming.preview`):
* - "partial" (default): per-token streaming via `stream.emit(text)`. Each
*   chunk goes onto the live preview card in Teams.
* - "progress": no per-token streaming; the preview card carries an
*   informative status that updates as tools run (e.g. "Looking up the
*   schema..." → "Generating SQL..."). When tool-progress streaming is also
*   enabled, raw tool names appear as bullets above the label.
* - "block": disable native streaming entirely; the reply lands as a regular
*   block message. We bypass the controller in that case.
*/
function createTeamsReplyStreamController(params) {
	const isPersonal = normalizeOptionalLowercaseString(params.conversationType) === "personal";
	const streamMode = resolveChannelPreviewStreamMode(params.msteamsConfig, "partial");
	const shouldUseNativeStream = isPersonal && (streamMode === "partial" || streamMode === "progress");
	const shouldStreamPreviewToolProgress = streamMode === "progress" && resolveChannelStreamingPreviewToolProgress(params.msteamsConfig);
	const stream = shouldUseNativeStream ? params.context.stream : void 0;
	let tokensEmitted = false;
	let streamFinalizationPending = false;
	let canceledLocally = false;
	let streamFailed = false;
	let lastInformativeText = "";
	let progressLines = [];
	let pendingFinalPayload;
	let emittedTextLength = 0;
	const wasCanceled = () => canceledLocally || Boolean(stream?.canceled);
	const fallbackPayloadForSuppressedFinal = (payload) => {
		return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
			...payload,
			mediaUrl: void 0,
			mediaUrls: void 0
		} : payload;
	};
	/**
	* Render the current informative status line into the streaming card. Pulls
	* the rotating "Thinking..." label from msteams config (or the plugin-sdk
	* default) and prepends collected tool-progress lines when configured.
	*/
	const renderInformativeUpdate = () => {
		if (!stream || wasCanceled()) return;
		const informativeText = formatChannelProgressDraftText({
			entry: params.msteamsConfig,
			lines: shouldStreamPreviewToolProgress ? progressLines : [],
			seed: params.progressSeed,
			bullet: "-"
		});
		if (!informativeText || informativeText === lastInformativeText) return;
		lastInformativeText = informativeText;
		try {
			stream.update(informativeText);
		} catch (err) {
			if (isStreamCancelledError(err)) {
				canceledLocally = true;
				return;
			}
			params.log?.debug?.(`stream informative update failed: ${err instanceof Error ? err.message : String(err)}`);
		}
	};
	const progressDraftGate = createChannelProgressDraftGate({ onStart: renderInformativeUpdate });
	return {
		async onReplyStart() {},
		onPartialReply(payload) {
			if (!stream || !payload.text || wasCanceled() || streamMode !== "partial" || streamFinalizationPending) return;
			const fullText = payload.text;
			if (fullText.length <= emittedTextLength) return;
			const delta = fullText.slice(emittedTextLength);
			try {
				stream.emit(delta);
				emittedTextLength = fullText.length;
				tokensEmitted = true;
			} catch (err) {
				if (isStreamCancelledError(err)) {
					canceledLocally = true;
					return;
				}
				streamFailed = true;
				params.log?.warn?.(`msteams stream emit failed, falling back to block delivery: ${err instanceof Error ? err.message : String(err)}`);
			}
		},
		/**
		* Note that the agent is working — bumps the progress-draft gate so the
		* informative status starts (or refreshes) on the next render. Called
		* from the reply-dispatcher's typing callbacks.
		*/
		async noteProgressWork(options) {
			if (!stream || streamMode !== "progress") return;
			if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
			const hadStarted = progressDraftGate.hasStarted;
			const progressActive = await progressDraftGate.noteWork();
			if ((hadStarted || progressActive) && progressDraftGate.hasStarted) renderInformativeUpdate();
		},
		/**
		* Append a tool-progress line (e.g. a tool name being invoked) into the
		* preview card's informative status. Only takes effect in "progress" mode
		* with `streaming.previewToolProgress` enabled in config.
		*/
		async pushProgressLine(line, options) {
			if (!stream || streamMode !== "progress") return;
			if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
			if (shouldStreamPreviewToolProgress) {
				const normalized = normalizeChannelProgressDraftLineIdentity(line);
				if (normalized) progressLines = mergeChannelProgressDraftLine(progressLines, typeof line === "object" && line !== void 0 ? line : normalized, { maxLines: resolveChannelProgressDraftMaxLines(params.msteamsConfig) });
			}
			const hadStarted = progressDraftGate.hasStarted;
			const progressActive = await progressDraftGate.noteWork();
			if ((hadStarted || progressActive) && progressDraftGate.hasStarted) renderInformativeUpdate();
		},
		preparePayload(payload) {
			if (!stream) return payload;
			if (wasCanceled()) return;
			if (tokensEmitted && !streamFailed) {
				const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
				pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
				streamFinalizationPending = true;
				tokensEmitted = false;
				return hasMedia ? {
					...payload,
					text: void 0
				} : void 0;
			}
			if (streamMode === "progress" && payload.text) try {
				stream.emit(payload.text);
				pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
				streamFinalizationPending = true;
				return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
					...payload,
					text: void 0
				} : void 0;
			} catch (err) {
				if (isStreamCancelledError(err)) {
					canceledLocally = true;
					return;
				}
				params.log?.debug?.(`progress-mode finalize failed: ${err instanceof Error ? err.message : String(err)}`);
			}
			return payload;
		},
		async finalize() {
			if (!stream || !streamFinalizationPending || wasCanceled()) return;
			const finalEntities = [{
				type: "https://schema.org/Message",
				"@type": "Message",
				"@context": "https://schema.org",
				"@id": "",
				additionalType: ["AIGeneratedContent"]
			}];
			const finalChannelData = params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {};
			try {
				stream.emit({
					type: "message",
					entities: finalEntities,
					channelData: finalChannelData
				});
				const result = await stream.close();
				streamFinalizationPending = false;
				if (!result) {
					const fallback = pendingFinalPayload;
					pendingFinalPayload = void 0;
					return fallback;
				}
				pendingFinalPayload = void 0;
				return;
			} catch (err) {
				if (isStreamCancelledError(err)) {
					canceledLocally = true;
					pendingFinalPayload = void 0;
					streamFinalizationPending = false;
					return;
				}
				streamFailed = true;
				streamFinalizationPending = false;
				params.log?.warn?.(`msteams stream finalize failed: ${err instanceof Error ? err.message : String(err)}`);
				const fallback = pendingFinalPayload;
				pendingFinalPayload = void 0;
				return fallback;
			}
		},
		hasStream() {
			return Boolean(stream);
		},
		isStreamActive() {
			return Boolean(stream) && tokensEmitted && !wasCanceled() && !streamFailed;
		},
		wasCanceled
	};
}
//#endregion
//#region extensions/msteams/src/reply-dispatcher.ts
function createMSTeamsReplyDispatcher(params) {
	const core = getMSTeamsRuntime();
	const msteamsCfg = params.cfg.channels?.msteams;
	const conversationType = normalizeOptionalLowercaseString(params.conversationRef.conversation?.conversationType);
	const isTypingSupported = conversationType === "personal" || conversationType === "groupchat";
	/**
	* Keepalive cadence for the typing indicator while the bot is running
	* (including long tool chains). Bot Framework 1:1 TurnContext proxies
	* expire after ~30s of inactivity; sending a typing activity every 8s
	* keeps the proxy alive so the post-tool reply can still land via the
	* turn context. Sits in the middle of the 5-10s range recommended in
	* #59731.
	*/
	const TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
	/**
	* TTL ceiling for the typing keepalive loop. The default in
	* createTypingCallbacks is 60s, which is too short for the Teams long tool
	* chains described in #59731 (60s+ total runs are common). Give tool
	* chains up to 10 minutes before auto-stopping the keepalive.
	*/
	const TYPING_KEEPALIVE_MAX_DURATION_MS = 10 * 6e4;
	const streamActiveRef = { current: () => false };
	const streamCanceledRef = { current: () => false };
	const rawSendTypingIndicator = async () => {
		await withRevokedProxyFallback({
			run: async () => {
				await params.context.sendActivity({ type: "typing" });
			},
			onRevoked: async () => {
				const baseRef = buildConversationReference(params.conversationRef);
				await sendMSTeamsActivityWithReference(params.app, baseRef, { type: "typing" }, { serviceUrlBoundary: resolveMSTeamsSdkCloudOptions(msteamsCfg) });
			},
			onRevokedLog: () => {
				params.log.debug?.("turn context revoked, sending typing via proactive messaging");
			}
		});
	};
	const sendTypingIndicator = isTypingSupported ? async () => {
		if (streamActiveRef.current()) return;
		if (streamCanceledRef.current()) return;
		await rawSendTypingIndicator();
	} : async () => {};
	const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
		cfg: params.cfg,
		agentId: params.agentId,
		channel: "msteams",
		accountId: params.accountId,
		typing: {
			start: sendTypingIndicator,
			keepaliveIntervalMs: TYPING_KEEPALIVE_INTERVAL_MS,
			maxDurationMs: TYPING_KEEPALIVE_MAX_DURATION_MS,
			onStartError: (err) => {
				logTypingFailure({
					log: (message) => params.log.debug?.(message),
					channel: "msteams",
					action: "start",
					error: err
				});
			}
		}
	});
	const chunkMode = core.channel.text.resolveChunkMode(params.cfg, "msteams");
	const tableMode = core.channel.text.resolveMarkdownTableMode({
		cfg: params.cfg,
		channel: "msteams"
	});
	const mediaMaxBytes = resolveChannelMediaMaxBytes({
		cfg: params.cfg,
		resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb
	});
	const feedbackLoopEnabled = params.cfg.channels?.msteams?.feedbackEnabled !== false;
	const streamController = createTeamsReplyStreamController({
		conversationType,
		context: params.context,
		feedbackLoopEnabled,
		log: params.log,
		msteamsConfig: msteamsCfg,
		progressSeed: `${params.accountId ?? "default"}:${params.conversationRef.conversation?.id ?? ""}`
	});
	streamActiveRef.current = () => streamController.isStreamActive();
	streamCanceledRef.current = () => streamController.wasCanceled();
	const teamsStreamMode = resolveChannelPreviewStreamMode(msteamsCfg, "partial");
	const blockStreamingResolved = teamsStreamMode === "block" ? true : resolveChannelStreamingBlockEnabled(msteamsCfg);
	const blockStreamingEnabled = blockStreamingResolved ?? false;
	const typingIndicatorEnabled = typeof msteamsCfg?.typingIndicator === "boolean" ? msteamsCfg.typingIndicator : true;
	const pendingMessages = [];
	const sendMessages = async (messages) => {
		return sendMSTeamsMessages({
			replyStyle: params.replyStyle,
			app: params.app,
			appId: params.appId,
			conversationRef: params.conversationRef,
			context: params.context,
			messages,
			retry: {},
			onRetry: (event) => {
				params.log.debug?.("retrying send", {
					replyStyle: params.replyStyle,
					...event
				});
			},
			tokenProvider: params.tokenProvider,
			sharePointSiteId: params.sharePointSiteId,
			mediaMaxBytes,
			feedbackLoopEnabled,
			serviceUrlBoundary: resolveMSTeamsSdkCloudOptions(msteamsCfg)
		});
	};
	const queueDeliveryFailureSystemEvent = (failure) => {
		const classification = classifyMSTeamsSendError(failure.error);
		const errorText = formatUnknownError(failure.error);
		const failedAll = failure.failed >= failure.total;
		const sentences = [
			`Microsoft Teams delivery failed: ${failedAll ? "the previous reply was not delivered" : `${failure.failed} of ${failure.total} message blocks were not delivered`}.`,
			`The user may not have received ${failedAll ? "that reply" : "the full reply"}.`,
			`Error: ${errorText}.`,
			classification.statusCode != null ? `Status: ${classification.statusCode}.` : void 0,
			classification.kind === "transient" || classification.kind === "throttled" ? "Retrying later may succeed." : void 0
		].filter(Boolean);
		core.system.enqueueSystemEvent(sentences.join(" "), {
			sessionKey: params.sessionKey,
			contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`
		});
	};
	const queueReplyPayload = (payload) => {
		const messages = renderReplyPayloadsToMessages([payload], {
			textChunkLimit: params.textLimit,
			chunkText: true,
			mediaMode: "split",
			tableMode,
			chunkMode
		});
		pendingMessages.push(...messages);
	};
	const flushPendingMessages = async () => {
		if (pendingMessages.length === 0) return;
		const toSend = pendingMessages.splice(0);
		const total = toSend.length;
		let ids;
		try {
			ids = await sendMessages(toSend);
		} catch (batchError) {
			ids = [];
			let failed = 0;
			let lastFailedError = batchError;
			for (const msg of toSend) try {
				const msgIds = await sendMessages([msg]);
				ids.push(...msgIds);
			} catch (msgError) {
				failed += 1;
				lastFailedError = msgError;
				params.log.debug?.("individual message send failed, continuing with remaining blocks");
			}
			if (failed > 0) {
				params.log.warn?.(`failed to deliver ${failed} of ${total} message blocks`, {
					failed,
					total
				});
				queueDeliveryFailureSystemEvent({
					failed,
					total,
					error: lastFailedError
				});
			}
		}
		if (ids.length > 0) params.onSentMessageIds?.(ids);
	};
	const { dispatcher, replyOptions, markDispatchIdle: baseMarkDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
		...replyPipeline,
		humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
		onReplyStart: async () => {
			await streamController.onReplyStart();
			if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
		},
		typingCallbacks,
		deliver: async (payload) => {
			const preparedPayload = streamController.preparePayload(payload);
			if (!preparedPayload) return;
			queueReplyPayload(preparedPayload);
			if (blockStreamingEnabled) await flushPendingMessages();
		},
		onError: (err, info) => {
			const errMsg = formatUnknownError(err);
			const classification = classifyMSTeamsSendError(err);
			const hint = formatMSTeamsSendErrorHint(classification);
			params.runtime.error?.(`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`);
			params.log.error("reply failed", {
				kind: info.kind,
				error: errMsg,
				classification,
				hint
			});
		}
	});
	const markDispatchIdle = () => {
		return flushPendingMessages().catch((err) => {
			const errMsg = formatUnknownError(err);
			const classification = classifyMSTeamsSendError(err);
			const hint = formatMSTeamsSendErrorHint(classification);
			params.runtime.error?.(`msteams flush reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`);
			params.log.error("flush reply failed", {
				error: errMsg,
				classification,
				hint
			});
		}).then(async () => {
			const fallbackPayload = await streamController.finalize().catch((err) => {
				params.log.debug?.("stream finalize failed", { error: formatUnknownError(err) });
			});
			if (fallbackPayload) {
				queueReplyPayload(fallbackPayload);
				await flushPendingMessages();
			}
		}).finally(() => {
			baseMarkDispatchIdle();
		});
	};
	const previewToolProgressEnabled = resolveChannelStreamingPreviewToolProgress(msteamsCfg);
	const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(msteamsCfg);
	const shouldSuppressDefaultToolProgressMessages = teamsStreamMode === "progress" && suppressDefaultToolProgressMessages && previewToolProgressEnabled;
	const progressCallbacks = streamController.hasStream() ? {
		onReasoningStream: async (payload) => {
			const text = typeof payload?.text === "string" ? payload.text : void 0;
			if (!text) return;
			if (payload?.isReasoningSnapshot !== true) {
				await streamController.pushProgressLine(text);
				return;
			}
			await streamController.pushProgressLine(buildChannelProgressDraftLine({
				event: "item",
				itemId: "reasoning",
				itemKind: "analysis",
				title: "Reasoning",
				progressText: text
			}));
		},
		onToolStart: async (payload) => {
			const name = typeof payload?.name === "string" ? payload.name : void 0;
			const detailMode = typeof payload?.detailMode === "string" ? payload.detailMode : void 0;
			await streamController.pushProgressLine(buildChannelProgressDraftLineForEntry(msteamsCfg, {
				event: "tool",
				...name ? { name } : {},
				...typeof payload?.phase === "string" ? { phase: payload.phase } : {},
				...payload?.args && typeof payload.args === "object" ? { args: payload.args } : {}
			}, detailMode === "explain" || detailMode === "raw" ? { detailMode } : void 0), name ? { toolName: name } : void 0);
		},
		onItemEvent: async (payload) => {
			await streamController.pushProgressLine(buildChannelProgressDraftLineForEntry(msteamsCfg, {
				event: "item",
				...typeof payload?.kind === "string" ? { itemKind: payload.kind } : {},
				...typeof payload?.title === "string" ? { title: payload.title } : {},
				...typeof payload?.name === "string" ? { name: payload.name } : {},
				...typeof payload?.phase === "string" ? { phase: payload.phase } : {},
				...typeof payload?.status === "string" ? { status: payload.status } : {},
				...typeof payload?.summary === "string" ? { summary: payload.summary } : {},
				...typeof payload?.progressText === "string" ? { progressText: payload.progressText } : {},
				...typeof payload?.meta === "string" ? { meta: payload.meta } : {}
			}));
		},
		onPlanUpdate: async (payload) => {
			if (payload?.phase !== "update") return;
			await streamController.pushProgressLine(buildChannelProgressDraftLine({
				event: "plan",
				phase: payload.phase,
				...typeof payload?.title === "string" ? { title: payload.title } : {},
				...typeof payload?.explanation === "string" ? { explanation: payload.explanation } : {},
				...Array.isArray(payload?.steps) && payload.steps.every((s) => typeof s === "string") ? { steps: payload.steps } : {}
			}));
		},
		onApprovalEvent: async (payload) => {
			if (payload?.phase !== "requested") return;
			await streamController.pushProgressLine(buildChannelProgressDraftLine({
				event: "approval",
				phase: payload.phase,
				...typeof payload?.title === "string" ? { title: payload.title } : {},
				...typeof payload?.command === "string" ? { command: payload.command } : {},
				...typeof payload?.reason === "string" ? { reason: payload.reason } : {},
				...typeof payload?.message === "string" ? { message: payload.message } : {}
			}));
		},
		onCommandOutput: async (payload) => {
			if (payload?.phase !== "end") return;
			await streamController.pushProgressLine(buildChannelProgressDraftLine({
				event: "command-output",
				phase: payload.phase,
				...typeof payload?.title === "string" ? { title: payload.title } : {},
				...typeof payload?.name === "string" ? { name: payload.name } : {},
				...typeof payload?.status === "string" ? { status: payload.status } : {},
				...typeof payload?.exitCode === "number" ? { exitCode: payload.exitCode } : {}
			}));
		},
		onPatchSummary: async (payload) => {
			if (payload?.phase !== "end") return;
			await streamController.pushProgressLine(buildChannelProgressDraftLine({
				event: "patch",
				phase: payload.phase,
				...typeof payload?.title === "string" ? { title: payload.title } : {},
				...typeof payload?.name === "string" ? { name: payload.name } : {},
				...Array.isArray(payload?.added) && payload.added.every((s) => typeof s === "string") ? { added: payload.added } : {},
				...Array.isArray(payload?.modified) && payload.modified.every((s) => typeof s === "string") ? { modified: payload.modified } : {},
				...Array.isArray(payload?.deleted) && payload.deleted.every((s) => typeof s === "string") ? { deleted: payload.deleted } : {},
				...typeof payload?.summary === "string" ? { summary: payload.summary } : {}
			}));
		}
	} : {};
	return {
		dispatcher,
		replyOptions: {
			...replyOptions,
			...streamController.hasStream() ? { onPartialReply: (payload) => streamController.onPartialReply(payload) } : {},
			...progressCallbacks,
			...shouldSuppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
			disableBlockStreaming: blockStreamingResolved == null ? void 0 : !blockStreamingResolved,
			onModelSelected
		},
		markDispatchIdle
	};
}
//#endregion
//#region extensions/msteams/src/sent-message-cache.ts
const TTL_MS = 1440 * 60 * 1e3;
const PERSISTENT_MAX_ENTRIES = 1e3;
const PERSISTENT_NAMESPACE = "msteams.sent-messages";
const MSTEAMS_SENT_MESSAGES_KEY = Symbol.for("openclaw.msteamsSentMessages");
let sentMessageCache;
let persistentStore;
let persistentStoreDisabled = false;
function getSentMessageCache() {
	if (!sentMessageCache) {
		const globalStore = globalThis;
		sentMessageCache = globalStore[MSTEAMS_SENT_MESSAGES_KEY] ?? /* @__PURE__ */ new Map();
		globalStore[MSTEAMS_SENT_MESSAGES_KEY] = sentMessageCache;
	}
	return sentMessageCache;
}
function makePersistentKey(conversationId, messageId) {
	return `${conversationId}:${messageId}`;
}
function reportPersistentSentMessageError(error) {
	try {
		getOptionalMSTeamsRuntime()?.logging.getChildLogger({
			plugin: "msteams",
			feature: "sent-message-state"
		}).warn("Microsoft Teams persistent sent-message state failed", { error: String(error) });
	} catch {}
}
function disablePersistentSentMessageStore(error) {
	persistentStoreDisabled = true;
	persistentStore = void 0;
	reportPersistentSentMessageError(error);
}
function getPersistentSentMessageStore() {
	if (persistentStoreDisabled) return;
	if (persistentStore) return persistentStore;
	const runtime = getOptionalMSTeamsRuntime();
	if (!runtime) return;
	try {
		persistentStore = runtime.state.openKeyedStore({
			namespace: PERSISTENT_NAMESPACE,
			maxEntries: PERSISTENT_MAX_ENTRIES,
			defaultTtlMs: TTL_MS
		});
		return persistentStore;
	} catch (error) {
		disablePersistentSentMessageStore(error);
		return;
	}
}
function cleanupExpired(scopeKey, entry, now) {
	for (const [id, timestamp] of entry) if (now - timestamp > TTL_MS) entry.delete(id);
	if (entry.size === 0) getSentMessageCache().delete(scopeKey);
}
function rememberSentMessageInMemory(conversationId, messageId, sentAt) {
	const store = getSentMessageCache();
	let entry = store.get(conversationId);
	if (!entry) {
		entry = /* @__PURE__ */ new Map();
		store.set(conversationId, entry);
	}
	entry.set(messageId, sentAt);
	if (entry.size > 200) cleanupExpired(conversationId, entry, sentAt);
}
function rememberPersistentSentMessage(params) {
	const store = getPersistentSentMessageStore();
	if (!store) return;
	store.register(makePersistentKey(params.conversationId, params.messageId), { sentAt: params.sentAt }).catch(disablePersistentSentMessageStore);
}
async function lookupPersistentSentMessage(params) {
	const store = getPersistentSentMessageStore();
	if (!store) return;
	try {
		return (await store.lookup(makePersistentKey(params.conversationId, params.messageId)))?.sentAt;
	} catch (error) {
		disablePersistentSentMessageStore(error);
		return;
	}
}
function recordMSTeamsSentMessage(conversationId, messageId) {
	if (!conversationId || !messageId) return;
	const now = Date.now();
	rememberSentMessageInMemory(conversationId, messageId, now);
	rememberPersistentSentMessage({
		conversationId,
		messageId,
		sentAt: now
	});
}
function wasMSTeamsMessageSent(conversationId, messageId) {
	const entry = getSentMessageCache().get(conversationId);
	if (!entry) return false;
	cleanupExpired(conversationId, entry, Date.now());
	return entry.has(messageId);
}
async function wasMSTeamsMessageSentWithPersistence(params) {
	if (!params.conversationId || !params.messageId) return false;
	if (wasMSTeamsMessageSent(params.conversationId, params.messageId)) return true;
	const sentAt = await lookupPersistentSentMessage(params);
	if (sentAt == null) return false;
	rememberSentMessageInMemory(params.conversationId, params.messageId, sentAt);
	return wasMSTeamsMessageSent(params.conversationId, params.messageId);
}
//#endregion
//#region extensions/msteams/src/monitor-handler/inbound-media.ts
async function resolveMSTeamsInboundMedia(params) {
	const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, serviceUrl, activity, log, preserveFilenames } = params;
	let mediaList = await downloadMSTeamsAttachments({
		attachments,
		maxBytes,
		tokenProvider,
		allowHosts,
		authAllowHosts: params.authAllowHosts,
		preserveFilenames,
		logger: log
	});
	if (mediaList.length === 0) {
		const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
		const hasHtmlFileAttachment = attachmentIds.length > 0;
		if (hasHtmlFileAttachment && isBotFrameworkPersonalChatId(conversationId)) if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
			conversationType,
			conversationId
		});
		else {
			const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
				serviceUrl,
				attachmentIds,
				tokenProvider,
				maxBytes,
				allowHosts,
				authAllowHosts: params.authAllowHosts,
				preserveFilenames
			});
			if (bfMedia.media.length > 0) mediaList = bfMedia.media;
			else log.debug?.("bot framework attachments fetch empty", {
				conversationType,
				attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
			});
		}
		if (hasHtmlFileAttachment && mediaList.length === 0 && !isBotFrameworkPersonalChatId(conversationId)) {
			const messageUrls = buildMSTeamsGraphMessageUrls({
				conversationType,
				conversationId,
				messageId: activity.id ?? void 0,
				replyToId: activity.replyToId ?? void 0,
				conversationMessageId,
				channelData: activity.channelData
			});
			if (messageUrls.length === 0) log.debug?.("graph message url unavailable", {
				conversationType,
				hasChannelData: Boolean(activity.channelData),
				messageId: activity.id ?? void 0,
				replyToId: activity.replyToId ?? void 0
			});
			else {
				const attempts = [];
				for (const messageUrl of messageUrls) {
					const graphMedia = await downloadMSTeamsGraphMedia({
						messageUrl,
						tokenProvider,
						maxBytes,
						allowHosts,
						authAllowHosts: params.authAllowHosts,
						preserveFilenames,
						log,
						logger: log
					});
					attempts.push({
						url: messageUrl,
						hostedStatus: graphMedia.hostedStatus,
						attachmentStatus: graphMedia.attachmentStatus,
						hostedCount: graphMedia.hostedCount,
						attachmentCount: graphMedia.attachmentCount,
						tokenError: graphMedia.tokenError
					});
					if (graphMedia.media.length > 0) {
						mediaList = graphMedia.media;
						break;
					}
					if (graphMedia.tokenError) break;
				}
				if (mediaList.length === 0) log.debug?.("graph media fetch empty", {
					attempts,
					attachmentIdCount: attachmentIds.length
				});
			}
		}
	}
	if (mediaList.length > 0) log.debug?.("downloaded attachments", { count: mediaList.length });
	else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
		imgTags: htmlSummary.imgTags,
		srcHosts: htmlSummary.srcHosts,
		dataImages: htmlSummary.dataImages,
		cidImages: htmlSummary.cidImages
	});
	return mediaList;
}
//#endregion
//#region extensions/msteams/src/monitor-handler/thread-session.ts
const TRAILING_THREAD_SUFFIX = /(?::thread:[^:]+)+$/;
function resolveMSTeamsRouteSessionKey(params) {
	const channelThreadId = params.isChannel ? params.conversationMessageId ?? params.replyToId ?? void 0 : void 0;
	const cleanBase = params.baseSessionKey.replace(TRAILING_THREAD_SUFFIX, "");
	return resolveThreadSessionKeys({
		baseSessionKey: cleanBase,
		threadId: channelThreadId,
		parentSessionKey: channelThreadId ? cleanBase : void 0
	}).sessionKey;
}
//#endregion
//#region extensions/msteams/src/monitor-handler/message-handler.ts
function extractTextFromHtmlAttachments(attachments) {
	for (const attachment of attachments) {
		if (attachment.contentType !== "text/html") continue;
		const content = attachment.content;
		const raw = typeof content === "string" ? content : isRecord(content) && typeof content.text === "string" ? content.text : isRecord(content) && typeof content.body === "string" ? content.body : "";
		if (!raw) continue;
		const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
		if (text) return text;
	}
	return "";
}
function formatMSTeamsSenderReason(params) {
	switch (params.reasonCode) {
		case "dm_policy_open": return "dmPolicy=open";
		case "dm_policy_disabled": return "dmPolicy=disabled";
		case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
		case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
		case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
		case "group_policy_disabled": return "groupPolicy=disabled";
		case "group_policy_empty_allowlist":
		case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
		case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
		case "group_policy_open": return "groupPolicy=open";
		case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
		default: return params.reasonCode;
	}
}
function buildStoredConversationReference(params) {
	const { activity, conversationId, conversationType, teamId, threadId } = params;
	const from = activity.from;
	const conversation = activity.conversation;
	const agent = activity.recipient;
	const clientInfo = activity.entities?.find((e) => e.type === "clientInfo");
	const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
	const aadObjectId = from?.aadObjectId;
	const serviceUrl = tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
	return {
		activityId: activity.id,
		user: from ? {
			id: from.id,
			name: from.name,
			aadObjectId: from.aadObjectId
		} : void 0,
		agent,
		bot: agent ? {
			id: agent.id,
			name: agent.name
		} : void 0,
		conversation: {
			id: conversationId,
			conversationType,
			tenantId
		},
		...tenantId ? { tenantId } : {},
		...aadObjectId ? { aadObjectId } : {},
		teamId,
		channelId: activity.channelId,
		...serviceUrl ? { serviceUrl } : {},
		locale: activity.locale,
		...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
		...threadId ? { threadId } : {}
	};
}
function createMSTeamsMessageHandler(deps) {
	const { cfg, runtime, appId, app, tokenProvider, textLimit, mediaMaxBytes, conversationStore, pollStore, log } = deps;
	const core = getMSTeamsRuntime();
	const logVerboseMessage = (message) => {
		if (core.logging.shouldLogVerbose()) log.debug?.(message);
	};
	const msteamsCfg = cfg.channels?.msteams;
	const contextVisibilityMode = resolveChannelContextVisibilityMode({
		cfg,
		channel: "msteams"
	});
	const historyLimit = Math.max(0, msteamsCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 50);
	const conversationHistories = /* @__PURE__ */ new Map();
	const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
		cfg,
		channel: "msteams"
	});
	const handleTeamsMessageNow = async (params) => {
		const context = params.context;
		const activity = context.activity;
		const rawText = params.rawText;
		const text = params.text;
		const attachments = params.attachments;
		const attachmentPlaceholder = buildMSTeamsAttachmentPlaceholder(attachments, {
			maxInlineBytes: mediaMaxBytes,
			maxInlineTotalBytes: mediaMaxBytes
		});
		const rawBody = text || attachmentPlaceholder;
		const quoteInfo = extractMSTeamsQuoteInfo(attachments);
		let quoteSenderId;
		let quoteSenderName;
		const from = activity.from;
		const conversation = activity.conversation;
		const attachmentTypes = attachments.map((att) => typeof att.contentType === "string" ? att.contentType : void 0).filter(Boolean).slice(0, 3);
		const htmlSummary = summarizeMSTeamsHtmlAttachments(attachments);
		log.info("received message", {
			rawText: rawText.slice(0, 50),
			text: text.slice(0, 50),
			attachments: attachments.length,
			attachmentTypes,
			from: from?.id,
			conversation: conversation?.id
		});
		if (htmlSummary) log.debug?.("html attachment summary", htmlSummary);
		if (!from?.id) {
			log.debug?.("skipping message without from.id");
			return;
		}
		const rawConversationId = conversation?.id ?? "";
		const conversationId = normalizeMSTeamsConversationId(rawConversationId);
		const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId);
		const conversationType = conversation?.conversationType ?? "personal";
		const teamId = activity.channelData?.team?.id;
		const conversationRef = buildStoredConversationReference({
			activity,
			conversationId,
			conversationType,
			teamId,
			threadId: conversationType === "channel" ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0
		});
		const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
			cfg,
			surface: "msteams"
		});
		const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(text, cfg);
		const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy } = await resolveMSTeamsSenderAccess({
			cfg,
			activity,
			hasControlCommand: isControlCommand
		});
		const commandAuthorized = commandAccess.requested ? commandAccess.authorized : void 0;
		const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
		const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
		const isChannel = conversationType === "channel";
		if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
			if (senderAccess.reasonCode === "dm_policy_disabled") {
				log.info("dropping dm (dms disabled)", {
					sender: senderId,
					label: senderName
				});
				log.debug?.("dropping dm (dms disabled)");
				return;
			}
			const allowMatch = resolveMSTeamsAllowlistMatch({
				allowFrom: effectiveDmAllowFrom,
				senderId,
				senderName,
				allowNameMatching
			});
			if (senderAccess.decision === "pairing") {
				conversationStore.upsert(conversationId, conversationRef).catch((err) => {
					log.debug?.("failed to save conversation reference", { error: formatUnknownError(err) });
				});
				if (await pairing.upsertPairingRequest({
					id: senderId,
					meta: { name: senderName }
				})) log.info("msteams pairing request created", {
					sender: senderId,
					label: senderName
				});
			}
			log.debug?.("dropping dm (not allowlisted)", {
				sender: senderId,
				label: senderName,
				allowlistMatch: formatAllowlistMatchMeta(allowMatch)
			});
			log.info("dropping dm (not allowlisted)", {
				sender: senderId,
				label: senderName,
				dmPolicy,
				reason: formatMSTeamsSenderReason({
					reasonCode: senderAccess.reasonCode,
					dmPolicy,
					groupPolicy
				}),
				allowlistMatch: formatAllowlistMatchMeta(allowMatch)
			});
			return;
		}
		if (!isDirectMessage && msteamsCfg) {
			if (channelGate.allowlistConfigured && !channelGate.allowed) {
				log.info("dropping group message (not in team/channel allowlist)", {
					conversationId,
					teamKey: channelGate.teamKey ?? "none",
					channelKey: channelGate.channelKey ?? "none",
					channelMatchKey: channelGate.channelMatchKey ?? "none",
					channelMatchSource: channelGate.channelMatchSource ?? "none"
				});
				log.debug?.("dropping group message (not in team/channel allowlist)", {
					conversationId,
					teamKey: channelGate.teamKey ?? "none",
					channelKey: channelGate.channelKey ?? "none",
					channelMatchKey: channelGate.channelMatchKey ?? "none",
					channelMatchSource: channelGate.channelMatchSource ?? "none"
				});
				return;
			}
			if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
				log.info("dropping group message (groupPolicy: disabled)", { conversationId });
				log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId });
				return;
			}
			if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
				log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
				log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
				return;
			}
			if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_not_allowlisted") {
				const allowMatch = resolveMSTeamsAllowlistMatch({
					allowFrom: effectiveGroupAllowFrom,
					senderId,
					senderName,
					allowNameMatching
				});
				log.debug?.("dropping group message (not in groupAllowFrom)", {
					sender: senderId,
					label: senderName,
					allowlistMatch: formatAllowlistMatchMeta(allowMatch)
				});
				log.info("dropping group message (not in groupAllowFrom)", {
					sender: senderId,
					label: senderName,
					allowlistMatch: formatAllowlistMatchMeta(allowMatch)
				});
				return;
			}
		}
		if (commandAccess.shouldBlockControlCommand) {
			logInboundDrop({
				log: logVerboseMessage,
				channel: "msteams",
				reason: "control command (unauthorized)",
				target: senderId
			});
			return;
		}
		conversationStore.upsert(conversationId, conversationRef).catch((err) => {
			log.debug?.("failed to save conversation reference", { error: formatUnknownError(err) });
		});
		const pollVote = extractMSTeamsPollVote(activity);
		if (pollVote) {
			try {
				if (!await pollStore.recordVote({
					pollId: pollVote.pollId,
					voterId: senderId,
					selections: pollVote.selections
				})) log.debug?.("poll vote ignored (poll not found)", { pollId: pollVote.pollId });
				else log.info("recorded poll vote", {
					pollId: pollVote.pollId,
					voter: senderId,
					selections: pollVote.selections
				});
			} catch (err) {
				log.error("failed to record poll vote", {
					pollId: pollVote.pollId,
					error: formatUnknownError(err)
				});
			}
			return;
		}
		if (!rawBody) {
			log.debug?.("skipping empty message after stripping mentions");
			return;
		}
		const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
		const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
		const route = core.channel.routing.resolveAgentRoute({
			cfg,
			channel: "msteams",
			teamId,
			peer: {
				kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
				id: isDirectMessage ? senderId : conversationId
			}
		});
		route.sessionKey = resolveMSTeamsRouteSessionKey({
			baseSessionKey: route.sessionKey,
			isChannel,
			conversationMessageId,
			replyToId: activity.replyToId
		});
		const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
		const inboundLabel = isDirectMessage ? `Teams DM from ${senderName}` : `Teams message in ${conversationType} from ${senderName}`;
		const enqueuePrimaryMessageSystemEvent = () => core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
			sessionKey: route.sessionKey,
			contextKey: `msteams:message:${conversationId}:${activity.id ?? "unknown"}`
		});
		const channelId = conversationId;
		const { teamConfig, channelConfig } = channelGate;
		const { requireMention, replyStyle } = resolveMSTeamsReplyPolicy({
			isDirectMessage,
			globalConfig: msteamsCfg,
			teamConfig,
			channelConfig
		});
		const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp);
		const mentionDecision = resolveInboundMentionDecision({
			facts: {
				canDetectMention: true,
				wasMentioned: params.wasMentioned,
				implicitMentionKinds: params.implicitMentionKinds
			},
			policy: {
				isGroup: !isDirectMessage,
				requireMention,
				allowTextCommands,
				hasControlCommand: isControlCommand,
				commandAuthorized: commandAuthorized === true
			}
		});
		if (!isDirectMessage) {
			const mentioned = mentionDecision.effectiveWasMentioned;
			if (requireMention && mentionDecision.shouldSkip) {
				log.debug?.("skipping message (mention required)", {
					teamId,
					channelId,
					requireMention,
					mentioned
				});
				enqueuePrimaryMessageSystemEvent();
				createChannelHistoryWindow({ historyMap: conversationHistories }).record({
					historyKey: conversationId,
					limit: historyLimit,
					entry: {
						sender: senderName,
						body: rawBody,
						timestamp: timestamp?.getTime(),
						messageId: activity.id ?? void 0
					}
				});
				return;
			}
		}
		enqueuePrimaryMessageSystemEvent();
		let graphConversationId = translateMSTeamsDmConversationIdForGraph({
			isDirectMessage,
			conversationId,
			aadObjectId: from.aadObjectId,
			appId
		});
		if (isDirectMessage && conversationId.startsWith("a:")) {
			const cached = await conversationStore.get(conversationId);
			if (cached?.graphChatId) graphConversationId = cached.graphChatId;
			else try {
				const resolved = await resolveGraphChatId({
					botFrameworkConversationId: conversationId,
					userAadObjectId: from.aadObjectId ?? void 0,
					tokenProvider
				});
				if (resolved) {
					graphConversationId = resolved;
					conversationStore.upsert(conversationId, {
						...conversationRef,
						graphChatId: resolved
					}).catch(() => {});
				}
			} catch {
				log.debug?.("failed to resolve Graph chat ID for inbound media", { conversationId });
			}
		}
		const mediaPayload = buildMSTeamsMediaPayload(await resolveMSTeamsInboundMedia({
			attachments,
			htmlSummary: htmlSummary ?? void 0,
			maxBytes: mediaMaxBytes,
			tokenProvider,
			allowHosts: msteamsCfg?.mediaAllowHosts,
			authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
			conversationType,
			conversationId: graphConversationId,
			conversationMessageId: conversationMessageId ?? void 0,
			serviceUrl: activity.serviceUrl,
			activity: {
				id: activity.id,
				replyToId: activity.replyToId,
				channelData: activity.channelData
			},
			log,
			preserveFilenames: cfg.media?.preserveFilenames
		}));
		let threadContext;
		if (activity.replyToId && isChannel && teamId) try {
			const graphToken = await tokenProvider.getAccessToken("https://graph.microsoft.com");
			const groupId = await resolveTeamGroupId(graphToken, teamId);
			const [parentResult, repliesResult] = await Promise.allSettled([fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId), fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId)]);
			const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : void 0;
			const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
			if (parentResult.status === "rejected") log.debug?.("failed to fetch parent message", { error: formatUnknownError(parentResult.reason) });
			if (repliesResult.status === "rejected") log.debug?.("failed to fetch thread replies", { error: formatUnknownError(repliesResult.reason) });
			const isThreadSenderAllowed = (msg) => groupPolicy === "allowlist" ? resolveMSTeamsAllowlistMatch({
				allowFrom: effectiveGroupAllowFrom,
				senderId: msg.from?.user?.id ?? "",
				senderName: msg.from?.user?.displayName,
				allowNameMatching
			}).allowed : true;
			const parentSummary = summarizeParentMessage(parentMsg);
			const visibleParentMessages = parentMsg ? filterSupplementalContextItems({
				items: [parentMsg],
				mode: contextVisibilityMode,
				kind: "thread",
				isSenderAllowed: isThreadSenderAllowed
			}).items : [];
			if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, activity.replyToId)) {
				core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
					sessionKey: route.sessionKey,
					contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`
				});
				markParentContextInjected(route.sessionKey, activity.replyToId);
			}
			const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
			quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
			quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? quoteInfo?.sender;
			const { items: threadMessages } = filterSupplementalContextItems({
				items: allMessages,
				mode: contextVisibilityMode,
				kind: "thread",
				isSenderAllowed: isThreadSenderAllowed
			});
			const formatted = formatThreadContext(threadMessages, activity.id);
			if (formatted) threadContext = formatted;
		} catch (err) {
			log.debug?.("failed to fetch thread history", { error: formatUnknownError(err) });
		}
		quoteSenderName ??= quoteInfo?.sender;
		const envelopeFrom = isDirectMessage ? senderName : conversationType;
		const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
			cfg,
			agentId: route.agentId,
			sessionKey: route.sessionKey
		});
		let combinedBody = core.channel.reply.formatAgentEnvelope({
			channel: "Teams",
			from: envelopeFrom,
			timestamp,
			previousTimestamp,
			envelope: envelopeOptions,
			body: rawBody
		});
		const isRoomish = !isDirectMessage;
		const historyKey = isRoomish ? conversationId : void 0;
		if (isRoomish && historyKey) combinedBody = createChannelHistoryWindow({ historyMap: conversationHistories }).buildPendingContext({
			historyKey,
			limit: historyLimit,
			currentMessage: combinedBody,
			formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
				channel: "Teams",
				from: conversationType,
				timestamp: entry.timestamp,
				body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`,
				envelope: envelopeOptions
			})
		});
		const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? createChannelHistoryWindow({ historyMap: conversationHistories }).buildInboundHistory({
			historyKey,
			limit: historyLimit
		}) : void 0;
		const commandBody = text.trim();
		const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? !isChannel || groupPolicy !== "allowlist" ? true : resolveMSTeamsAllowlistMatch({
			allowFrom: effectiveGroupAllowFrom,
			senderId: quoteSenderId ?? "",
			senderName: quoteSenderName,
			allowNameMatching
		}).allowed : true;
		const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${rawBody}` : rawBody;
		const nativeChannelId = isChannel && teamId ? `${teamId}/${conversationId}` : void 0;
		const ctxPayload = buildChannelInboundEventContext({
			channel: "msteams",
			finalize: core.channel.reply.finalizeInboundContext,
			contextVisibility: contextVisibilityMode,
			supplemental: { quote: quoteInfo ? {
				id: activity.replyToId ?? void 0,
				body: quoteInfo.body,
				sender: quoteInfo.sender,
				senderAllowed: quoteSenderAllowed,
				isQuote: true
			} : void 0 },
			messageId: activity.id,
			timestamp: timestamp?.getTime() ?? Date.now(),
			from: teamsFrom,
			sender: {
				id: senderId,
				name: senderName
			},
			conversation: {
				kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
				id: conversationId,
				label: envelopeFrom,
				spaceId: teamId,
				nativeChannelId
			},
			route: {
				agentId: route.agentId,
				accountId: route.accountId,
				routeSessionKey: route.sessionKey
			},
			reply: {
				to: teamsTo,
				replyToId: activity.replyToId ?? void 0,
				nativeChannelId
			},
			message: {
				body: combinedBody,
				bodyForAgent,
				inboundHistory,
				rawBody,
				commandBody
			},
			access: {
				mentions: {
					canDetectMention: !isDirectMessage,
					wasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned
				},
				commands: { authorized: commandAuthorized }
			},
			extra: {
				GroupSubject: !isDirectMessage ? conversationType : void 0,
				ReplyToIsQuote: quoteInfo ? true : void 0,
				...mediaPayload
			}
		});
		logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
		const sharePointSiteId = msteamsCfg?.sharePointSiteId;
		const { dispatcher, replyOptions, markDispatchIdle } = createMSTeamsReplyDispatcher({
			cfg,
			agentId: route.agentId,
			sessionKey: route.sessionKey,
			accountId: route.accountId,
			runtime,
			log,
			app,
			appId,
			conversationRef,
			context,
			replyStyle,
			textLimit,
			onSentMessageIds: (ids) => {
				for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
			},
			tokenProvider,
			sharePointSiteId
		});
		const senderTimezone = (activity.entities?.find((e) => e.type === "clientInfo"))?.timezone || conversationRef.timezone;
		const configOverride = senderTimezone && !cfg.agents?.defaults?.userTimezone ? { agents: { defaults: {
			...cfg.agents?.defaults,
			userTimezone: senderTimezone
		} } } : void 0;
		log.info("dispatching to agent", { sessionKey: route.sessionKey });
		try {
			const turnResult = await core.channel.inbound.run({
				channel: "msteams",
				accountId: route.accountId,
				raw: context,
				adapter: {
					ingest: () => ({
						id: activity.id ?? `${teamsFrom}:${Date.now()}`,
						timestamp: timestamp?.getTime(),
						rawText: rawBody,
						textForAgent: bodyForAgent,
						textForCommands: commandBody,
						raw: activity
					}),
					resolveTurn: () => ({
						channel: "msteams",
						accountId: route.accountId,
						routeSessionKey: route.sessionKey,
						storePath,
						ctxPayload,
						recordInboundSession: core.channel.session.recordInboundSession,
						record: { onRecordError: (err) => {
							logVerboseMessage(`msteams: failed updating session meta: ${formatUnknownError(err)}`);
						} },
						history: {
							isGroup: isRoomish,
							historyKey,
							historyMap: conversationHistories,
							limit: historyLimit
						},
						onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
							dispatcher,
							onSettled: () => markDispatchIdle()
						}),
						runDispatch: () => dispatchReplyFromConfigWithSettledDispatcher({
							cfg,
							ctxPayload,
							dispatcher,
							onSettled: () => markDispatchIdle(),
							replyOptions,
							configOverride
						})
					})
				}
			});
			const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
			const queuedFinal = dispatchResult?.queuedFinal ?? false;
			const counts = resolveChannelTurnDispatchCounts(dispatchResult);
			const hasFinalResponse = hasFinalChannelTurnDispatch(dispatchResult);
			log.info("dispatch complete", {
				queuedFinal,
				counts
			});
			if (!hasFinalResponse) return;
			const finalCount = counts.final;
			logVerboseMessage(`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`);
		} catch (err) {
			log.error("dispatch failed", { error: formatUnknownError(err) });
			runtime.error(`msteams dispatch failed: ${formatUnknownError(err)}`);
			try {
				await context.sendActivity("⚠️ Something went wrong. Please try again.");
			} catch {}
		}
	};
	const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
		debounceMs: inboundDebounceMs,
		buildKey: (entry) => {
			const conversationId = normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
			const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
			if (!senderId || !conversationId) return null;
			return `msteams:${appId}:${conversationId}:${senderId}`;
		},
		shouldDebounce: (entry) => {
			if (!entry.text.trim()) return false;
			if (entry.attachments.length > 0) return false;
			return !core.channel.commands.isControlCommandMessage(entry.text, cfg);
		},
		onFlush: async (entries) => {
			const last = entries.at(-1);
			if (!last) return;
			if (entries.length === 1) {
				await handleTeamsMessageNow(last);
				return;
			}
			const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
			if (!combinedText.trim()) return;
			const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
			const wasMentioned = entries.some((entry) => entry.wasMentioned);
			const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
			await handleTeamsMessageNow({
				context: last.context,
				rawText: combinedRawText,
				text: combinedText,
				attachments: [],
				wasMentioned,
				implicitMentionKinds
			});
		},
		onError: (err) => {
			runtime.error(`msteams debounce flush failed: ${formatUnknownError(err)}`);
		}
	});
	return async function handleTeamsMessage(context) {
		const activity = context.activity;
		const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
		const rawText = activity.text?.trim() ?? "";
		const htmlText = extractTextFromHtmlAttachments(attachments);
		const text = stripMSTeamsMentionTags(rawText || htmlText);
		const wasMentioned = wasMSTeamsBotMentioned(activity);
		const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
		const replyToId = activity.replyToId ?? void 0;
		const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
			conversationId,
			messageId: replyToId
		}) ? ["reply_to_bot"] : [];
		await inboundDebouncer.enqueue({
			context,
			rawText,
			text,
			attachments,
			wasMentioned,
			implicitMentionKinds
		});
	};
}
//#endregion
//#region extensions/msteams/src/monitor-handler/reaction-handler.ts
/** Teams reaction type names → Unicode emoji. */
const TEAMS_REACTION_EMOJI = {
	like: "👍",
	heart: "❤️",
	laugh: "😆",
	surprised: "😮",
	sad: "😢",
	angry: "😡"
};
/**
* Map a Teams reaction type string to a Unicode emoji.
* Falls back to the raw type if not recognized.
*/
function mapReactionEmoji(reactionType) {
	return TEAMS_REACTION_EMOJI[reactionType] ?? reactionType;
}
/**
* Create a handler for MS Teams reaction activities (reactionsAdded / reactionsRemoved).
* The returned function accepts a turn context and a direction string.
*/
function createMSTeamsReactionHandler(deps) {
	const { cfg, log } = deps;
	const core = getMSTeamsRuntime();
	const msteamsCfg = cfg.channels?.msteams;
	return async function handleReaction(context, direction) {
		const activity = context.activity;
		const reactions = direction === "added" ? activity.reactionsAdded ?? [] : activity.reactionsRemoved ?? [];
		if (reactions.length === 0) {
			log.debug?.("reaction activity has no reactions; skipping");
			return;
		}
		const from = activity.from;
		if (!from?.id) {
			log.debug?.("reaction activity missing from.id; skipping");
			return;
		}
		const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
		const conversationType = activity.conversation?.conversationType ?? "personal";
		const isGroupChat = conversationType === "groupChat" || activity.conversation?.isGroup === true;
		const isChannel = conversationType === "channel";
		const isDirectMessage = !isGroupChat && !isChannel;
		const senderId = from.aadObjectId ?? from.id;
		const senderName = from.name ?? from.id;
		if (msteamsCfg) {
			const senderAccess = await resolveMSTeamsSenderAccess({
				cfg,
				activity
			});
			if (senderAccess.senderAccess.decision !== "allow") {
				log.debug?.("dropping reaction (access denied)", {
					sender: senderId,
					reason: senderAccess.senderAccess.reasonCode
				});
				return;
			}
		}
		const teamId = isDirectMessage ? void 0 : activity.channelData?.team?.id;
		const route = core.channel.routing.resolveAgentRoute({
			cfg,
			channel: "msteams",
			peer: {
				kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
				id: isDirectMessage ? senderId : conversationId
			},
			...teamId ? { teamId } : {}
		});
		const targetMessageId = activity.replyToId ?? "unknown";
		for (const reaction of reactions) {
			const reactionType = reaction.type ?? "unknown";
			const emoji = mapReactionEmoji(reactionType);
			const label = direction === "added" ? `Teams reaction ${emoji} added by ${senderName} on message ${targetMessageId}` : `Teams reaction ${emoji} removed by ${senderName} from message ${targetMessageId}`;
			log.info(`reaction ${direction}`, {
				sender: senderId,
				reactionType,
				emoji,
				targetMessageId,
				conversationId
			});
			core.system.enqueueSystemEvent(label, {
				sessionKey: route.sessionKey,
				contextKey: `msteams:reaction:${conversationId}:${targetMessageId}:${senderId}:${reactionType}:${direction}`
			});
		}
	};
}
//#endregion
//#region extensions/msteams/src/welcome-card.ts
/**
* Builds an Adaptive Card for welcoming users when the bot is added to a conversation.
*/
const DEFAULT_PROMPT_STARTERS = [
	"What can you do?",
	"Summarize my last meeting",
	"Help me draft an email"
];
/**
* Build a welcome Adaptive Card for 1:1 personal chats.
*/
function buildWelcomeCard(options) {
	const botName = options?.botName || "OpenClaw";
	const starters = options?.promptStarters?.length ? options.promptStarters : DEFAULT_PROMPT_STARTERS;
	return {
		type: "AdaptiveCard",
		version: "1.5",
		body: [{
			type: "TextBlock",
			text: `Hi! I'm ${botName}.`,
			weight: "bolder",
			size: "medium"
		}, {
			type: "TextBlock",
			text: "I can help you with questions, tasks, and more. Here are some things to try:",
			wrap: true
		}],
		actions: starters.map((label) => ({
			type: "Action.Submit",
			title: label,
			data: { msteams: {
				type: "imBack",
				value: label
			} }
		}))
	};
}
/**
* Build a brief welcome message for group chats (when the bot is @mentioned).
*/
function buildGroupWelcomeText(botName) {
	const name = botName || "OpenClaw";
	return `Hi! I'm ${name}. Mention me with @${name} to get started.`;
}
//#endregion
//#region extensions/msteams/src/monitor-handler.ts
function serializeAdaptiveCardActionValue(value) {
	if (typeof value === "string") {
		const trimmed = value.trim();
		return trimmed ? trimmed : null;
	}
	if (value === void 0) return null;
	try {
		return JSON.stringify(value);
	} catch {
		return null;
	}
}
async function isInvokeAuthorized(params) {
	const { context, deps, deniedLogs, includeInvokeName = false } = params;
	const resolved = await resolveMSTeamsSenderAccess({
		cfg: deps.cfg,
		activity: context.activity
	});
	const { msteamsCfg, isDirectMessage, conversationId, senderId } = resolved;
	if (!msteamsCfg) return true;
	const maybeInvokeName = includeInvokeName ? { name: context.activity.name } : void 0;
	if (isDirectMessage && resolved.senderAccess.decision !== "allow") {
		deps.log.debug?.(deniedLogs.dm, {
			sender: senderId,
			conversationId,
			...maybeInvokeName
		});
		return false;
	}
	if (!isDirectMessage && resolved.channelGate.allowlistConfigured && !resolved.channelGate.allowed) {
		deps.log.debug?.(deniedLogs.channel, {
			conversationId,
			teamKey: resolved.channelGate.teamKey ?? "none",
			channelKey: resolved.channelGate.channelKey ?? "none",
			...maybeInvokeName
		});
		return false;
	}
	if (!isDirectMessage && !resolved.senderAccess.allowed) {
		deps.log.debug?.(deniedLogs.group, {
			sender: senderId,
			conversationId,
			...maybeInvokeName
		});
		return false;
	}
	return true;
}
async function isFeedbackInvokeAuthorized(context, deps) {
	return isInvokeAuthorized({
		context,
		deps,
		deniedLogs: {
			dm: "dropping feedback invoke (dm sender not allowlisted)",
			channel: "dropping feedback invoke (not in team/channel allowlist)",
			group: "dropping feedback invoke (group sender not allowlisted)"
		}
	});
}
async function isSigninInvokeAuthorized(context, deps) {
	return isInvokeAuthorized({
		context,
		deps,
		deniedLogs: {
			dm: "dropping signin invoke (dm sender not allowlisted)",
			channel: "dropping signin invoke (not in team/channel allowlist)",
			group: "dropping signin invoke (group sender not allowlisted)"
		},
		includeInvokeName: true
	});
}
async function isCardActionInvokeAuthorized(context, deps) {
	return isInvokeAuthorized({
		context,
		deps,
		deniedLogs: {
			dm: "dropping card action invoke (dm sender not allowlisted)",
			channel: "dropping card action invoke (not in team/channel allowlist)",
			group: "dropping card action invoke (group sender not allowlisted)"
		},
		includeInvokeName: true
	});
}
function registerMSTeamsHandlers(handler, deps) {
	const handleTeamsMessage = createMSTeamsMessageHandler(deps);
	const handleReaction = createMSTeamsReactionHandler(deps);
	const originalRun = handler.run;
	if (originalRun) handler.run = async (context) => {
		const ctx = context;
		if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") {
			const text = serializeAdaptiveCardActionValue(ctx.activity?.value);
			if (text) await handleTeamsMessage({
				...ctx,
				activity: {
					...ctx.activity,
					type: "message",
					text
				}
			});
			return;
		}
		return originalRun.call(handler, context);
	};
	handler.onMessage(async (context, next) => {
		try {
			await handleTeamsMessage(context);
		} catch (err) {
			deps.runtime.error(`msteams handler failed: ${formatUnknownError(err)}`);
		}
		await next();
	});
	handler.onMembersAdded(async (context, next) => {
		const ctx = context;
		const membersAdded = ctx.activity?.membersAdded ?? [];
		const botId = ctx.activity?.recipient?.id;
		const msteamsCfg = deps.cfg.channels?.msteams;
		for (const member of membersAdded) if (member.id === botId) {
			const isPersonal = (normalizeOptionalLowercaseString(ctx.activity?.conversation?.conversationType) ?? "personal") === "personal";
			if (isPersonal && msteamsCfg?.welcomeCard !== false) {
				const card = buildWelcomeCard({
					botName: ctx.activity?.recipient?.name ?? void 0,
					promptStarters: msteamsCfg?.promptStarters
				});
				try {
					await ctx.sendActivity({
						type: "message",
						attachments: [{
							contentType: "application/vnd.microsoft.card.adaptive",
							content: card
						}]
					});
					deps.log.info("sent welcome card");
				} catch (err) {
					deps.log.debug?.("failed to send welcome card", { error: formatUnknownError(err) });
				}
			} else if (!isPersonal && msteamsCfg?.groupWelcomeCard === true) {
				const botName = ctx.activity?.recipient?.name ?? void 0;
				try {
					await ctx.sendActivity(buildGroupWelcomeText(botName));
					deps.log.info("sent group welcome message");
				} catch (err) {
					deps.log.debug?.("failed to send group welcome", { error: formatUnknownError(err) });
				}
			} else deps.log.debug?.("skipping welcome (disabled by config or conversation type)");
		} else deps.log.debug?.("member added", { member: member.id });
		await next();
	});
	handler.onReactionsAdded(async (context, next) => {
		try {
			await handleReaction(context, "added");
		} catch (err) {
			deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
		}
		await next();
	});
	handler.onReactionsRemoved(async (context, next) => {
		try {
			await handleReaction(context, "removed");
		} catch (err) {
			deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
		}
		await next();
	});
	return handler;
}
//#endregion
//#region extensions/msteams/src/feedback-invoke.ts
/**
* Run the message-submit (feedback) invoke handler.
*
* Teams delivers feedback (`actionName === "feedback"`) on AI-generated
* messages as a `message/submitAction` invoke. The SDK wraps a void return
* into the HTTP 200 InvokeResponse, so this function intentionally does
* not ack itself — the legacy `ctx.sendActivity({ type: "invokeResponse",
* … })` shape is gone (it became an outbound BF activity on the new SDK
* instead of the HTTP response).
*
* Returns `true` if the invoke matched the feedback shape and was
* consumed (whether or not it was authorized / written / reflected on),
* `false` if the invoke didn't look like feedback at all and the caller
* should fall through to other handlers.
*/
async function runMSTeamsFeedbackInvokeHandler(context, deps) {
	const activity = context.activity;
	const value = activity.value;
	if (!value) return false;
	if (value.actionName !== "feedback") return false;
	const reaction = value.actionValue?.reaction;
	if (reaction !== "like" && reaction !== "dislike") {
		deps.log.debug?.("ignoring feedback with unknown reaction", { reaction });
		return false;
	}
	const msteamsCfg = deps.cfg.channels?.msteams;
	if (msteamsCfg?.feedbackEnabled === false) {
		deps.log.debug?.("feedback handling disabled");
		return true;
	}
	if (!await isFeedbackInvokeAuthorized(context, deps)) return true;
	let userComment;
	if (value.actionValue?.feedback) try {
		userComment = JSON.parse(value.actionValue.feedback).feedbackText || void 0;
	} catch {}
	const rawConversationId = activity.conversation?.id ?? "unknown";
	const conversationId = normalizeMSTeamsConversationId(rawConversationId);
	const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
	const messageId = value.replyToId ?? activity.replyToId ?? "unknown";
	const isNegative = reaction === "dislike";
	const convType = normalizeOptionalLowercaseString(activity.conversation?.conversationType);
	const isDirectMessage = convType === "personal" || !convType && !activity.conversation?.isGroup;
	const isChannel = convType === "channel";
	const core = getMSTeamsRuntime();
	const route = core.channel.routing.resolveAgentRoute({
		cfg: deps.cfg,
		channel: "msteams",
		peer: {
			kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
			id: isDirectMessage ? senderId : conversationId
		}
	});
	const feedbackThreadId = isChannel ? extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
	if (feedbackThreadId) route.sessionKey = resolveThreadSessionKeys({
		baseSessionKey: route.sessionKey,
		threadId: feedbackThreadId,
		parentSessionKey: route.sessionKey
	}).sessionKey;
	const feedbackEvent = buildFeedbackEvent({
		messageId,
		value: isNegative ? "negative" : "positive",
		comment: userComment,
		sessionKey: route.sessionKey,
		agentId: route.agentId,
		conversationId
	});
	deps.log.info("received feedback", {
		value: feedbackEvent.value,
		messageId,
		conversationId,
		hasComment: Boolean(userComment)
	});
	try {
		const storePath = core.channel.session.resolveStorePath(deps.cfg.session?.store, { agentId: route.agentId });
		const safeKey = route.sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
		await appendRegularFile({
			filePath: path.join(storePath, `${safeKey}.jsonl`),
			content: `${JSON.stringify(feedbackEvent)}\n`,
			rejectSymlinkParents: true
		}).catch(() => {});
	} catch {}
	const conversationRef = {
		activityId: activity.id,
		user: {
			id: activity.from?.id,
			name: activity.from?.name,
			aadObjectId: activity.from?.aadObjectId
		},
		agent: activity.recipient ? {
			id: activity.recipient.id,
			name: activity.recipient.name
		} : void 0,
		bot: activity.recipient ? {
			id: activity.recipient.id,
			name: activity.recipient.name
		} : void 0,
		conversation: {
			id: conversationId,
			conversationType: activity.conversation?.conversationType,
			tenantId: activity.conversation?.tenantId
		},
		channelId: activity.channelId ?? "msteams",
		serviceUrl: activity.serviceUrl,
		locale: activity.locale
	};
	if (isNegative && msteamsCfg?.feedbackReflection !== false) runFeedbackReflection({
		cfg: deps.cfg,
		app: deps.app,
		appId: deps.appId,
		conversationRef,
		sessionKey: route.sessionKey,
		agentId: route.agentId,
		conversationId,
		feedbackMessageId: messageId,
		userComment,
		log: deps.log
	}).catch((err) => {
		deps.log.error("feedback reflection failed", { error: formatUnknownError(err) });
	});
	return true;
}
//#endregion
//#region extensions/msteams/src/file-consent-invoke.ts
/**
* Handle fileConsent/invoke activities for large file uploads.
*/
async function handleMSTeamsFileConsentInvoke(context, log) {
	const expiredUploadMessage = "The file upload request has expired. Please try sending the file again.";
	const activity = context.activity;
	if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") return false;
	const consentResponse = parseFileConsentInvoke(activity);
	if (!consentResponse) {
		log.debug?.("invalid file consent invoke", { value: activity.value });
		return false;
	}
	const uploadId = typeof consentResponse.context?.uploadId === "string" ? consentResponse.context.uploadId : void 0;
	const inMemoryFile = getPendingUpload(uploadId);
	const fsFile = inMemoryFile ? void 0 : await getPendingUploadFs(uploadId);
	const pendingFile = inMemoryFile ?? fsFile;
	if (pendingFile) {
		const pendingConversationId = normalizeMSTeamsConversationId(pendingFile.conversationId);
		const invokeConversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
		if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
			log.info("file consent conversation mismatch", {
				uploadId,
				expectedConversationId: pendingConversationId,
				receivedConversationId: invokeConversationId || void 0
			});
			if (consentResponse.action === "accept") await context.sendActivity(expiredUploadMessage);
			return true;
		}
	}
	if (consentResponse.action === "accept" && consentResponse.uploadInfo) if (pendingFile) {
		log.debug?.("user accepted file consent, uploading", {
			uploadId,
			filename: pendingFile.filename,
			size: pendingFile.buffer.length
		});
		try {
			await uploadToConsentUrl({
				url: consentResponse.uploadInfo.uploadUrl,
				buffer: pendingFile.buffer,
				contentType: pendingFile.contentType
			});
			const fileInfoCard = buildFileInfoCard({
				filename: consentResponse.uploadInfo.name,
				contentUrl: consentResponse.uploadInfo.contentUrl,
				uniqueId: consentResponse.uploadInfo.uniqueId,
				fileType: consentResponse.uploadInfo.fileType
			});
			if (!pendingFile.consentCardActivityId) await context.sendActivity({
				type: "message",
				attachments: [fileInfoCard]
			});
			if (pendingFile.consentCardActivityId) try {
				await context.updateActivity({
					id: pendingFile.consentCardActivityId,
					type: "message",
					attachments: [fileInfoCard]
				});
			} catch {
				await context.sendActivity({
					type: "message",
					attachments: [fileInfoCard]
				});
			}
			log.info("file upload complete", {
				uploadId,
				filename: consentResponse.uploadInfo.name,
				uniqueId: consentResponse.uploadInfo.uniqueId
			});
		} catch (err) {
			log.error("file upload failed", {
				uploadId,
				error: formatUnknownError(err)
			});
			await context.sendActivity("File upload failed. Please try again.");
		} finally {
			removePendingUpload(uploadId);
			await removePendingUploadFs(uploadId);
		}
	} else {
		log.debug?.("pending file not found for consent", { uploadId });
		await context.sendActivity(expiredUploadMessage);
	}
	else {
		log.debug?.("user declined file consent", { uploadId });
		removePendingUpload(uploadId);
		await removePendingUploadFs(uploadId);
	}
	return true;
}
/**
* Run the file-consent invoke handler after the SDK route has acknowledged the
* invoke. This intentionally does not send its own invokeResponse; it only does
* the delayed upload/update work.
*/
async function runMSTeamsFileConsentInvokeHandler(context, log) {
	try {
		await withRevokedProxyFallback({
			run: async () => await handleMSTeamsFileConsentInvoke(context, log),
			onRevoked: async () => true,
			onRevokedLog: () => {
				log.debug?.("turn context revoked during file consent invoke; skipping delayed response");
			}
		});
	} catch (err) {
		log.debug?.("file consent handler error", { error: formatUnknownError(err) });
	}
}
//#endregion
//#region extensions/msteams/src/webhook-timeouts.ts
const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 3e4;
const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 3e4;
const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15e3;
function applyMSTeamsWebhookTimeouts(httpServer, opts) {
	const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
	const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
	const headersTimeoutMs = Math.min(opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS, requestTimeoutMs);
	httpServer.setTimeout(inactivityTimeoutMs);
	httpServer.requestTimeout = requestTimeoutMs;
	httpServer.headersTimeout = headersTimeoutMs;
}
//#endregion
//#region extensions/msteams/src/monitor.ts
async function monitorMSTeamsProvider(opts) {
	const core = getMSTeamsRuntime();
	const log = core.logging.getChildLogger({ name: "msteams" });
	let cfg = opts.cfg;
	let msteamsCfg = cfg.channels?.msteams;
	if (!msteamsCfg?.enabled) {
		log.debug?.("msteams provider disabled");
		return {
			app: null,
			shutdown: async () => {}
		};
	}
	const creds = resolveMSTeamsCredentials(msteamsCfg);
	if (!creds) {
		log.error("msteams credentials not configured");
		return {
			app: null,
			shutdown: async () => {}
		};
	}
	const appId = creds.appId;
	const runtime = opts.runtime ?? {
		log: console.log,
		error: console.error,
		exit: (code) => {
			throw new Error(`exit ${code}`);
		}
	};
	let allowFrom = msteamsCfg.allowFrom;
	let groupAllowFrom = msteamsCfg.groupAllowFrom;
	let teamsConfig = msteamsCfg.teams;
	const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
	const cleanAllowEntry = (entry) => entry.replace(/^(msteams|teams):/i, "").replace(/^user:/i, "").trim();
	const isStableUserId = (entry) => /^[0-9a-fA-F-]{16,}$/.test(entry);
	const cleanAllowEntries = (entries) => entries?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
	const mergeStableUserIds = (entries) => {
		const additions = cleanAllowEntries(entries).filter((entry) => isStableUserId(entry));
		return additions.length > 0 ? mergeAllowlist({
			existing: entries,
			additions
		}) : entries;
	};
	const resolveAllowlistUsers = async (label, entries) => {
		if (entries.length === 0) return {
			additions: [],
			unresolved: []
		};
		const resolved = await resolveMSTeamsUserAllowlist({
			cfg,
			entries
		});
		const additions = [];
		const unresolved = [];
		for (const entry of resolved) if (entry.resolved && entry.id) additions.push(entry.id);
		else unresolved.push(entry.input);
		summarizeMapping(label, resolved.filter((entry) => entry.resolved && entry.id).map((entry) => `${entry.input}→${entry.id}`), unresolved, runtime);
		return {
			additions,
			unresolved
		};
	};
	try {
		allowFrom = mergeStableUserIds(allowFrom);
		if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) groupAllowFrom = mergeStableUserIds(groupAllowFrom);
		if (allowNameMatching) {
			const allowEntries = cleanAllowEntries(allowFrom).filter((entry) => !isStableUserId(entry));
			if (allowEntries.length > 0) {
				const { additions } = await resolveAllowlistUsers("msteams users", allowEntries);
				allowFrom = mergeAllowlist({
					existing: allowFrom,
					additions
				});
			}
			if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) {
				const groupEntries = cleanAllowEntries(groupAllowFrom).filter((entry) => !isStableUserId(entry));
				if (groupEntries.length > 0) {
					const { additions } = await resolveAllowlistUsers("msteams group users", groupEntries);
					groupAllowFrom = mergeAllowlist({
						existing: groupAllowFrom,
						additions
					});
				}
			}
		}
		if (teamsConfig && Object.keys(teamsConfig).length > 0) {
			const entries = [];
			for (const [teamKey, teamCfg] of Object.entries(teamsConfig)) {
				if (teamKey === "*") continue;
				const channels = teamCfg?.channels ?? {};
				const channelKeys = Object.keys(channels).filter((key) => key !== "*");
				if (channelKeys.length === 0) {
					entries.push({
						input: teamKey,
						teamKey
					});
					continue;
				}
				for (const channelKey of channelKeys) entries.push({
					input: `${teamKey}/${channelKey}`,
					teamKey,
					channelKey
				});
			}
			if (entries.length > 0) {
				const resolved = await resolveMSTeamsChannelAllowlist({
					cfg,
					entries: entries.map((entry) => entry.input)
				});
				const mapping = [];
				const unresolved = [];
				const nextTeams = { ...teamsConfig };
				resolved.forEach((entry, idx) => {
					const source = entries[idx];
					if (!source) return;
					const sourceTeam = teamsConfig?.[source.teamKey] ?? {};
					if (!entry.resolved || !entry.teamId) {
						unresolved.push(entry.input);
						return;
					}
					mapping.push(entry.channelId ? `${entry.input}→${entry.teamId}/${entry.channelId}` : `${entry.input}→${entry.teamId}`);
					const existing = nextTeams[entry.teamId] ?? {};
					const mergedChannels = {
						...sourceTeam.channels,
						...existing.channels
					};
					const mergedTeam = {
						...sourceTeam,
						...existing,
						channels: mergedChannels
					};
					nextTeams[entry.teamId] = mergedTeam;
					if (source.channelKey && entry.channelId) {
						const sourceChannel = sourceTeam.channels?.[source.channelKey];
						if (sourceChannel) nextTeams[entry.teamId] = {
							...mergedTeam,
							channels: {
								...mergedChannels,
								[entry.channelId]: {
									...sourceChannel,
									...mergedChannels?.[entry.channelId]
								}
							}
						};
					}
				});
				teamsConfig = nextTeams;
				summarizeMapping("msteams channels", mapping, unresolved, runtime);
			}
		}
	} catch (err) {
		runtime.error?.(`msteams resolve failed; falling back to raw config entries — allowlist members resolved via Graph may be missing. ${formatUnknownError(err)}`);
	}
	msteamsCfg = {
		...msteamsCfg,
		allowFrom,
		groupAllowFrom,
		teams: teamsConfig
	};
	cfg = {
		...cfg,
		channels: {
			...cfg.channels,
			msteams: msteamsCfg
		}
	};
	const port = msteamsCfg.webhook?.port ?? 3978;
	const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "msteams");
	const MB = 1024 * 1024;
	const agentDefaults = cfg.agents?.defaults;
	const mediaMaxBytes = typeof agentDefaults?.mediaMaxMb === "number" && agentDefaults.mediaMaxMb > 0 ? Math.floor(agentDefaults.mediaMaxMb * MB) : 8 * MB;
	const conversationStore = opts.conversationStore ?? createMSTeamsConversationStoreState();
	const pollStore = opts.pollStore ?? createMSTeamsPollStoreState();
	log.info(`starting provider (port ${port})`);
	const express = await import("express");
	const expressApp = express.default();
	expressApp.use((req, res, next) => {
		const auth = req.headers.authorization;
		if (!auth || !auth.startsWith("Bearer ")) {
			res.status(401).json({ error: "Unauthorized" });
			return;
		}
		next();
	});
	expressApp.use(express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES }));
	expressApp.use((err, _req, res, next) => {
		if (err && typeof err === "object" && "status" in err && err.status === 413) {
			res.status(413).json({ error: "Payload too large" });
			return;
		}
		next(err);
	});
	const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
	const { app } = await loadMSTeamsSdkWithAuth(creds, {
		...resolveMSTeamsSdkCloudOptions(msteamsCfg),
		httpServerAdapter: await createMSTeamsExpressAdapter(expressApp),
		messagingEndpoint: configuredPath,
		...msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName ? { oauthDefaultConnectionName: msteamsCfg.sso.connectionName } : {}
	});
	if (configuredPath !== "/api/messages") {
		let warnedLegacyMessagesRoute = false;
		expressApp.post("/api/messages", (req, res, next) => {
			if (!warnedLegacyMessagesRoute) {
				warnedLegacyMessagesRoute = true;
				log.warn?.(`received request on /api/messages but webhook.path is ${configuredPath}; update your Azure Bot endpoint — this fallback will be removed in a future release`);
			}
			req.url = configuredPath;
			expressApp(req, res, next);
		});
	}
	const tokenProvider = createMSTeamsTokenProvider(app);
	let ssoDeps;
	if (msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName) {
		ssoDeps = {
			tokenProvider,
			tokenStore: createMSTeamsSsoTokenStoreFs(),
			connectionName: msteamsCfg.sso.connectionName
		};
		log.debug?.("msteams sso enabled", { connectionName: msteamsCfg.sso.connectionName });
	}
	const handler = buildActivityHandler();
	const handlerDeps = {
		cfg,
		runtime,
		appId,
		app,
		tokenProvider,
		textLimit,
		mediaMaxBytes,
		conversationStore,
		pollStore,
		log,
		sso: ssoDeps
	};
	registerMSTeamsHandlers(handler, handlerDeps);
	app.on("card.action", async (ctx) => {
		const adaptedCtx = adaptSdkContext(ctx, app);
		try {
			const activity = adaptedCtx.activity;
			const vote = extractMSTeamsPollVote(activity);
			if (vote) {
				const voterId = activity?.from?.aadObjectId ?? activity?.from?.id ?? "unknown";
				try {
					if (!await isCardActionInvokeAuthorized(adaptedCtx, handlerDeps)) return {
						statusCode: 200,
						type: "application/vnd.microsoft.activity.message",
						value: "Not authorized."
					};
					const existingPoll = await pollStore.getPoll(vote.pollId);
					if (!existingPoll) {
						log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
						return {
							statusCode: 200,
							type: "application/vnd.microsoft.activity.message",
							value: "Poll not found."
						};
					}
					const pollConversationId = existingPoll.conversationId ? normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
					const activityConversationId = normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
					if (pollConversationId && pollConversationId !== activityConversationId) {
						log.info("poll vote ignored (conversation mismatch)", {
							pollId: vote.pollId,
							expectedConversationId: pollConversationId,
							receivedConversationId: activityConversationId || void 0
						});
						return {
							statusCode: 200,
							type: "application/vnd.microsoft.activity.message",
							value: "Poll not found."
						};
					}
					if (await pollStore.recordVote({
						pollId: vote.pollId,
						voterId,
						selections: vote.selections
					})) {
						log.info("recorded poll vote", {
							pollId: vote.pollId,
							voterId
						});
						return {
							statusCode: 200,
							type: "application/vnd.microsoft.activity.message",
							value: "Vote recorded."
						};
					}
					log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
					return {
						statusCode: 200,
						type: "application/vnd.microsoft.activity.message",
						value: "Poll not found."
					};
				} catch (err) {
					log.error("failed to record poll vote", {
						pollId: vote.pollId,
						error: formatUnknownError(err)
					});
					return {
						statusCode: 500,
						type: "application/vnd.microsoft.error",
						value: {
							code: "RECORD_VOTE_FAILED",
							message: "Could not record vote.",
							innerHttpError: {
								statusCode: 500,
								body: null
							}
						}
					};
				}
			}
			handler.run(adaptedCtx).catch((err) => {
				log.error("msteams card.action dispatch failed", { error: formatUnknownError(err) });
			});
			return {
				statusCode: 200,
				type: "application/vnd.microsoft.activity.message",
				value: "OK"
			};
		} catch (err) {
			log.error("msteams card.action failed", { error: formatUnknownError(err) });
			return {
				statusCode: 500,
				type: "application/vnd.microsoft.error",
				value: {
					code: "CARD_ACTION_FAILED",
					message: "Card action failed.",
					innerHttpError: {
						statusCode: 500,
						body: null
					}
				}
			};
		}
	});
	app.on("file.consent.accept", (ctx) => {
		runMSTeamsFileConsentInvokeHandler(adaptSdkContext(ctx, app), log);
	});
	app.on("file.consent.decline", (ctx) => {
		runMSTeamsFileConsentInvokeHandler(adaptSdkContext(ctx, app), log);
	});
	const handleSdkSigninInvoke = async (ctx, delegateName) => {
		const adaptedCtx = adaptSdkContext(ctx, app);
		if (!await isSigninInvokeAuthorized(adaptedCtx, handlerDeps)) return {
			status: 200,
			body: {}
		};
		if (!ssoDeps) {
			log.debug?.("signin invoke received but msteams.sso is not configured", { name: adaptedCtx.activity?.name });
			return {
				status: 200,
				body: {}
			};
		}
		const sdkSigninApp = app;
		const delegate = sdkSigninApp[delegateName];
		if (typeof delegate !== "function") throw new Error(`Teams SDK ${delegateName} handler is unavailable`);
		return delegate.call(sdkSigninApp, ctx);
	};
	app.on("signin.token-exchange", (ctx) => handleSdkSigninInvoke(ctx, "onTokenExchange"));
	app.on("signin.verify-state", (ctx) => handleSdkSigninInvoke(ctx, "onVerifyState"));
	if (ssoDeps) app.event("signin", (ctx) => {
		(async () => {
			if (!await isSigninInvokeAuthorized(adaptSdkContext(ctx, app), handlerDeps)) return;
			const activity = ctx.activity;
			const userIds = Array.from(new Set([activity.from?.id, activity.from?.aadObjectId].filter((id) => Boolean(id))));
			const connectionName = ctx.token.connectionName || ssoDeps.connectionName;
			if (!connectionName || !ctx.token.token || userIds.length === 0) {
				log.warn?.("msteams sso signin event missing token metadata", {
					hasConnectionName: Boolean(connectionName),
					hasToken: Boolean(ctx.token.token),
					hasUser: userIds.length > 0
				});
				return;
			}
			await Promise.all(userIds.map((userId) => ssoDeps.tokenStore.save({
				connectionName,
				userId,
				token: ctx.token.token,
				expiresAt: ctx.token.expiration,
				updatedAt: (/* @__PURE__ */ new Date()).toISOString()
			})));
			log.info("msteams sso token persisted", {
				connectionName,
				userIdCount: userIds.length,
				hasExpiry: Boolean(ctx.token.expiration)
			});
		})().catch((err) => {
			log.error("msteams sso token persistence failed", { error: formatUnknownError(err) });
		});
	});
	app.on("message.submit", async (ctx) => {
		if (!await runMSTeamsFeedbackInvokeHandler(adaptSdkContext(ctx, app), handlerDeps)) await ctx.next?.call(ctx);
	});
	app.on("activity", async (ctx) => {
		try {
			const adaptedCtx = adaptSdkContext(ctx, app);
			const activity = adaptedCtx.activity;
			if (activity?.type === "invoke") {
				if (activity?.name === "adaptiveCard/action") return;
				if (activity?.name === "fileConsent/invoke") return;
				if (activity?.name === "signin/tokenExchange" || activity?.name === "signin/verifyState") return;
			}
			await handler.run(adaptedCtx);
		} catch (err) {
			log.error("msteams webhook failed", { error: formatUnknownError(err) });
		}
	});
	await app.initialize();
	const httpServer = expressApp.listen(port);
	await new Promise((resolve, reject) => {
		const onListening = () => {
			httpServer.off("error", onError);
			log.info(`msteams provider started on port ${port}`);
			resolve();
		};
		const onError = (err) => {
			httpServer.off("listening", onListening);
			log.error("msteams server error", { error: formatUnknownError(err) });
			reject(toLintErrorObject(err, "MSTeams server failed"));
		};
		httpServer.once("listening", onListening);
		httpServer.once("error", onError);
	});
	applyMSTeamsWebhookTimeouts(httpServer);
	httpServer.on("error", (err) => {
		log.error("msteams server error", { error: formatUnknownError(err) });
	});
	const shutdown = async () => {
		log.info("shutting down msteams provider");
		return new Promise((resolve) => {
			httpServer.close((err) => {
				if (err) log.debug?.("msteams server close error", { error: formatUnknownError(err) });
				resolve();
			});
		});
	};
	await keepHttpServerTaskAlive({
		server: httpServer,
		abortSignal: opts.abortSignal,
		onAbort: shutdown
	});
	return {
		app: expressApp,
		shutdown
	};
}
function toLintErrorObject(value, fallbackMessage) {
	if (value instanceof Error) return value;
	if (typeof value === "string") return new Error(value);
	const error = new Error(fallbackMessage, { cause: value });
	if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
	return error;
}
/**
* Build a minimal ActivityHandler-compatible object that supports
* onMessage / onMembersAdded registration and a run() method.
*/
function buildActivityHandler() {
	const messageHandlers = [];
	const membersAddedHandlers = [];
	const reactionsAddedHandlers = [];
	const reactionsRemovedHandlers = [];
	const handler = {
		onMessage(cb) {
			messageHandlers.push(cb);
			return handler;
		},
		onMembersAdded(cb) {
			membersAddedHandlers.push(cb);
			return handler;
		},
		onReactionsAdded(cb) {
			reactionsAddedHandlers.push(cb);
			return handler;
		},
		onReactionsRemoved(cb) {
			reactionsRemovedHandlers.push(cb);
			return handler;
		},
		async run(context) {
			const ctx = context;
			const activityType = ctx?.activity?.type;
			const noop = async () => {};
			if (activityType === "message") for (const h of messageHandlers) await h(context, noop);
			else if (activityType === "conversationUpdate") for (const h of membersAddedHandlers) await h(context, noop);
			else if (activityType === "messageReaction") {
				const activity = ctx?.activity;
				if (activity?.reactionsAdded?.length) for (const h of reactionsAddedHandlers) await h(context, noop);
				if (activity?.reactionsRemoved?.length) for (const h of reactionsRemovedHandlers) await h(context, noop);
			}
		}
	};
	return handler;
}
/**
* Adapt a new @microsoft/teams.apps SDK context to the MSTeamsTurnContext interface
* our handlers expect. The new SDK uses reply()/send() instead of sendActivity().
*/
function adaptSdkContext(ctx, app) {
	const sdkCtx = ctx ?? {};
	if (typeof sdkCtx.reply !== "function" && typeof sdkCtx.send !== "function") return ctx;
	const conversationId = sdkCtx.activity?.conversation?.id ?? "";
	const activityApi = sdkCtx.api ?? app.api;
	const conversationType = (sdkCtx.activity?.conversation?.conversationType ?? "").toLowerCase();
	const isThreadable = conversationType === "channel" || conversationType === "groupchat";
	const sendActivity = (activity) => isThreadable ? sdkCtx.reply(activity) : sdkCtx.send(activity);
	return Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
		sendActivity,
		sendActivities: async (activities) => {
			const results = [];
			for (const a of activities) results.push(await sendActivity(a));
			return results;
		},
		updateActivity: async (activity) => {
			const activityId = activity.id ?? "";
			return activityApi.conversations.activities(conversationId).update(activityId, activity);
		},
		deleteActivity: async (activityId) => {
			return activityApi.conversations.activities(conversationId).delete(activityId);
		},
		stream: sdkCtx.stream
	});
}
//#endregion
export { monitorMSTeamsProvider };