UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,550 lines 97 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { C as resolveExpiresAtMsFromDurationMs, j as resolveTimerTimeoutMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { n as parseTcpPort } from "./tcp-port-DPgvEEt3.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { _ as uniqueStrings, d as normalizeTrimmedStringList, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { m as resolveThreadSessionKeys$1 } from "./session-key-B_NoIfpX.js";
import { Nn as record, Rn as string, Tn as object, Xn as union, Zn as unknown } from "./schemas-6cH6bZ7o.js";
import { n as safeParseWithSchema, t as safeParseJsonWithSchema } from "./zod-parse-Bip-sZi_.js";
import { i as isLoopbackHost } from "./net-DTe7AQiu.js";
import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js";
import { a as getReplyPayloadTtsSupplement, n as buildTtsSupplementMediaPayload } from "./reply-payload-CZJ2cgZc.js";
import { t as countOutboundMedia, u as isReasoningReplyPayload } from "./reply-payload-D-VMfpYI.js";
import { n as createDebugProxyWebSocketAgent, r as resolveDebugProxySettings } from "./env-ckWue_wY.js";
import { a as resolveInboundLastRouteSessionKey } from "./resolve-route-xcIS8NFL.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, n as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js";
import { l as resolvePinnedMainDmOwnerFromAllowlist } from "./dm-policy-shared-BjnswXJt.js";
import { k as resolveChannelStreamingPreviewToolProgress, s as formatChannelProgressDraftLineForEntry } from "./streaming-_TVeMLGC.js";
import { a as deliverWithFinalizableLivePreviewAdapter, r as defineFinalizableLivePreviewAdapter } from "./live-DjttXqqq.js";
import "./error-runtime-C8vbtAJt.js";
import { a as resolveChannelMediaMaxBytes } from "./media-runtime-fgpmX6eL.js";
import { t as buildAgentMediaPayload } from "./agent-media-payload-BXUnfV7N.js";
import "./number-runtime-DBLVDypr.js";
import "./security-runtime-CQm7DD1u.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { n as captureWsEvent } from "./runtime-DrwkIUiy.js";
import "./proxy-capture-x_2F2qw1.js";
import "./routing-CQ63ICPX.js";
import { i as formatInboundFromLabel$1 } from "./envelope-IMcsY0tl.js";
import "./history-6XlO5h6s.js";
import { t as createChannelReplyPipeline } from "./reply-pipeline-eJPstCAz.js";
import { t as createChannelHistoryWindow } from "./history-window-B0hyPVKC.js";
import { t as rawDataToString } from "./ws-C3qhmaFC.js";
import { t as listSkillCommandsForAgents } from "./chat-commands-DKDF6ilZ.js";
import "./channel-outbound-B3_Zy-kG.js";
import { n as createFinalizableDraftLifecycle } from "./draft-stream-controls-C8Kxicze.js";
import { n as logInboundDrop, r as logTypingFailure } from "./logging-gUWPKC5g.js";
import { d as ssrfPolicyFromPrivateNetworkOptIn, o as isPrivateNetworkOptInEnabled } from "./ssrf-policy-sZLyyYht.js";
import "./ssrf-runtime-BOGN5pUi.js";
import { t as registerPluginHttpRoute } from "./http-registry-C6KnnU9A.js";
import "./gateway-runtime-BMyBmMxe.js";
import { t as buildModelsProviderData } from "./commands-models-BhAJq6Rj.js";
import "./extension-shared-B8fkO3TV.js";
import "./channel-inbound-bl7VmTdr.js";
import { n as createChannelPairingController } from "./channel-pairing-Bk6_JhTm.js";
import { t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js";
import "./webhook-ingress-Dgd3Jtks.js";
import { a as resolveMattermostMonitorInboundAccess, r as normalizeMattermostAllowEntry, t as authorizeMattermostCommandInvocation } from "./monitor-auth-DnF21yRY.js";
import { _ as readMattermostError, a as MattermostPostSchema, c as createMattermostPost, f as fetchMattermostMe, g as normalizeMattermostBaseUrl, h as fetchMattermostUserTeams, i as resolveMattermostReplyToMode, l as deleteMattermostPost, o as createMattermostClient, p as fetchMattermostUser, r as resolveMattermostAccount, t as listMattermostAccountIds, u as fetchMattermostChannel, v as sendMattermostTyping, y as updateMattermostPost } from "./accounts-BVsVtEWg.js";
import { t as getMattermostRuntime } from "./runtime-D5T9siJC.js";
import { C as resolveCallbackUrl, S as registerSlashCommands, _ as renderMattermostProviderPickerView, a as sendMessageMattermost, b as cleanupSlashCommands, c as computeInteractionCallbackUrl, d as setInteractionCallbackUrl, f as setInteractionSecret, g as renderMattermostModelsPickerView, h as parseMattermostModelPickerContext, l as createMattermostInteractionHandler, m as buildMattermostAllowedModelRefs, n as deactivateSlashCommands, o as resolveMattermostOpaqueTarget, p as deliverMattermostReplyPayload, r as getSlashCommandState, s as buildButtonProps, t as activateSlashCommands, u as resolveInteractionCallbackPath, v as resolveMattermostModelPickerCurrentModel, w as resolveSlashCommandConfig, x as isSlashCommandsEnabled, y as DEFAULT_COMMAND_SPECS } from "./slash-state-BIUoExh0.js";
import { randomUUID } from "node:crypto";
import WebSocket from "ws";
//#region extensions/mattermost/src/mattermost/directory.ts
function buildClient(params) {
	const account = resolveMattermostAccount({
		cfg: params.cfg,
		accountId: params.accountId
	});
	if (!account.enabled || !account.botToken || !account.baseUrl) return null;
	return createMattermostClient({
		baseUrl: account.baseUrl,
		botToken: account.botToken,
		allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config)
	});
}
/**
* Build clients from ALL enabled accounts (deduplicated by token).
*
* We always scan every account because:
* - Private channels are only visible to bots that are members
* - The requesting agent's account may have an expired/invalid token
*
* This means a single healthy bot token is enough for directory discovery.
*/
function buildClients(params) {
	const accountIds = listMattermostAccountIds(params.cfg);
	const seen = /* @__PURE__ */ new Set();
	const clients = [];
	for (const id of accountIds) {
		const client = buildClient({
			cfg: params.cfg,
			accountId: id
		});
		if (client && !seen.has(client.token)) {
			seen.add(client.token);
			clients.push(client);
		}
	}
	return clients;
}
/**
* List channels (public + private) visible to any configured bot account.
*
* NOTE: Uses per_page=200 which covers most instances. Mattermost does not
* return a "has more" indicator, so very large instances (200+ channels per bot)
* may see incomplete results. Pagination can be added if needed.
*/
async function listMattermostDirectoryGroups(params) {
	const clients = buildClients(params);
	if (!clients.length) return [];
	const q = normalizeLowercaseStringOrEmpty(params.query);
	const seenIds = /* @__PURE__ */ new Set();
	const entries = [];
	for (const client of clients) try {
		const me = await fetchMattermostMe(client);
		const channels = await client.request(`/users/${me.id}/channels?per_page=200`);
		for (const ch of channels) {
			if (ch.type !== "O" && ch.type !== "P") continue;
			if (seenIds.has(ch.id)) continue;
			if (q) {
				const name = normalizeLowercaseStringOrEmpty(ch.name);
				const display = normalizeLowercaseStringOrEmpty(ch.display_name);
				if (!name.includes(q) && !display.includes(q)) continue;
			}
			seenIds.add(ch.id);
			entries.push({
				kind: "group",
				id: `channel:${ch.id}`,
				name: ch.name ?? void 0,
				handle: ch.display_name ?? void 0
			});
		}
	} catch (err) {
		console.debug?.("[mattermost-directory] listGroups: skipping account:", err?.message);
		continue;
	}
	return params.limit && params.limit > 0 ? entries.slice(0, params.limit) : entries;
}
/**
* List team members as peer directory entries.
*
* Uses only the first available client since all bots in a team see the same
* user list (unlike channels where membership varies). Uses the first team
* returned — multi-team setups will only see members from that team.
*
* NOTE: per_page=200 for member listing; same pagination caveat as groups.
*/
async function listMattermostDirectoryPeers(params) {
	const clients = buildClients(params);
	if (!clients.length) return [];
	const client = clients[0];
	try {
		const me = await fetchMattermostMe(client);
		const teams = await client.request("/users/me/teams");
		if (!teams.length) return [];
		const teamId = teams[0].id;
		const q = normalizeLowercaseStringOrEmpty(params.query);
		let users;
		if (q) users = await client.request("/users/search", {
			method: "POST",
			body: JSON.stringify({
				term: q,
				team_id: teamId
			})
		});
		else {
			const userIds = (await client.request(`/teams/${teamId}/members?per_page=200`)).map((m) => m.user_id).filter((id) => id !== me.id);
			if (!userIds.length) return [];
			users = await client.request("/users/ids", {
				method: "POST",
				body: JSON.stringify(userIds)
			});
		}
		const entries = users.filter((u) => u.id !== me.id).map((u) => ({
			kind: "user",
			id: `user:${u.id}`,
			name: u.username ?? void 0,
			handle: [u.first_name, u.last_name].filter(Boolean).join(" ").trim() || u.nickname || void 0
		}));
		return params.limit && params.limit > 0 ? entries.slice(0, params.limit) : entries;
	} catch (err) {
		console.debug?.("[mattermost-directory] listPeers failed:", err?.message);
		return [];
	}
}
//#endregion
//#region extensions/mattermost/src/mattermost/draft-stream.ts
const MATTERMOST_STREAM_MAX_CHARS = 4e3;
const DEFAULT_THROTTLE_MS = 1e3;
function normalizeMattermostDraftText(text, maxChars) {
	const trimmed = text.trim();
	if (!trimmed) return "";
	if (trimmed.length <= maxChars) return trimmed;
	return `${trimmed.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
}
function buildMattermostToolStatusText(params) {
	return formatChannelProgressDraftLineForEntry(params.config, {
		event: "tool",
		name: params.name,
		phase: params.phase,
		args: params.args
	}, params.detailMode ? { detailMode: params.detailMode } : void 0) ?? "Running tool...";
}
function createMattermostDraftStream(params) {
	const maxChars = Math.min(params.maxChars ?? MATTERMOST_STREAM_MAX_CHARS, MATTERMOST_STREAM_MAX_CHARS);
	const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
	const streamState = {
		stopped: false,
		final: false
	};
	let streamPostId;
	let lastSentText = "";
	const sendOrEditStreamMessage = async (text) => {
		if (streamState.stopped && !streamState.final) return false;
		const normalized = normalizeMattermostDraftText(params.renderText?.(text) ?? text, maxChars);
		if (!normalized) return false;
		if (normalized === lastSentText) return true;
		try {
			if (streamPostId) await updateMattermostPost(params.client, streamPostId, { message: normalized });
			else {
				const postId = (await createMattermostPost(params.client, {
					channelId: params.channelId,
					message: normalized,
					rootId: params.rootId
				})).id?.trim();
				if (!postId) {
					streamState.stopped = true;
					params.warn?.("mattermost stream preview stopped (missing post id from create)");
					return false;
				}
				streamPostId = postId;
			}
			lastSentText = normalized;
			return true;
		} catch (err) {
			streamState.stopped = true;
			params.warn?.(`mattermost stream preview failed: ${err instanceof Error ? err.message : String(err)}`);
			return false;
		}
	};
	const { loop, update, stop, clear, discardPending, seal } = createFinalizableDraftLifecycle({
		throttleMs,
		state: streamState,
		sendOrEditStreamMessage,
		readMessageId: () => streamPostId,
		clearMessageId: () => {
			streamPostId = void 0;
		},
		isValidMessageId: (value) => typeof value === "string" && value.length > 0,
		deleteMessage: async (postId) => {
			await deleteMattermostPost(params.client, postId);
		},
		warn: params.warn,
		warnPrefix: "mattermost stream preview cleanup failed"
	});
	const forceNewMessage = () => {
		streamPostId = void 0;
		lastSentText = "";
		loop.resetPending();
		loop.resetThrottleWindow();
	};
	params.log?.(`mattermost stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
	return {
		update,
		flush: loop.flush,
		postId: () => streamPostId,
		clear,
		discardPending,
		seal,
		stop,
		forceNewMessage
	};
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-gating.ts
function mapMattermostChannelTypeToChatType(channelType) {
	const normalized = channelType?.trim().toUpperCase();
	if (!normalized) return "direct";
	if (normalized === "D") return "direct";
	if (normalized === "G" || normalized === "P") return "group";
	return "channel";
}
function resolveMattermostTrustedChatKind(params) {
	const channelType = params.channelType?.trim();
	if (channelType) return mapMattermostChannelTypeToChatType(channelType);
	return params.fallback ?? "direct";
}
function evaluateMattermostMentionGate(params) {
	const shouldRequireMention = params.kind !== "direct" && params.resolveRequireMention({
		cfg: params.cfg,
		channel: "mattermost",
		accountId: params.accountId,
		groupId: params.channelId,
		requireMentionOverride: params.requireMentionOverride
	});
	const shouldBypassMention = params.isControlCommand && shouldRequireMention && !params.wasMentioned && params.commandAuthorized;
	const effectiveWasMentioned = params.wasMentioned || shouldBypassMention || params.oncharTriggered;
	if (params.oncharEnabled && !params.oncharTriggered && !params.wasMentioned && !params.isControlCommand) return {
		shouldRequireMention,
		shouldBypassMention,
		effectiveWasMentioned,
		dropReason: "onchar-not-triggered"
	};
	if (params.kind !== "direct" && shouldRequireMention && params.canDetectMention && !effectiveWasMentioned) return {
		shouldRequireMention,
		shouldBypassMention,
		effectiveWasMentioned,
		dropReason: "missing-mention"
	};
	return {
		shouldRequireMention,
		shouldBypassMention,
		effectiveWasMentioned,
		dropReason: null
	};
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-helpers.ts
const formatInboundFromLabel = formatInboundFromLabel$1;
function resolveThreadSessionKeys(params) {
	return resolveThreadSessionKeys$1({
		...params,
		normalizeThreadId: (threadId) => threadId
	});
}
/**
* Strip bot mention from message text while preserving newlines and
* block-level Markdown formatting (headings, lists, blockquotes).
*/
function normalizeMention(text, mention) {
	if (!mention) return text.trim();
	const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
	const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
	const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
	const normalizedLines = text.split("\n").map((line) => {
		const hadMention = hasMentionRe.test(line);
		const normalizedLine = line.replace(leadingMentionRe, "$1").replace(trailingMentionRe, "").replace(new RegExp(`@${escaped}\\b`, "gi"), "").replace(/(\S)[ \t]{2,}/g, "$1 ");
		return {
			text: normalizedLine,
			mentionOnlyBlank: hadMention && normalizedLine.trim() === ""
		};
	});
	while (normalizedLines[0]?.mentionOnlyBlank) normalizedLines.shift();
	while (normalizedLines.at(-1)?.text.trim() === "") normalizedLines.pop();
	return normalizedLines.map((line) => line.text).join("\n");
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-onchar.ts
const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
function resolveOncharPrefixes(prefixes) {
	const cleaned = prefixes ? normalizeStringEntries(prefixes) : DEFAULT_ONCHAR_PREFIXES;
	return cleaned.length > 0 ? cleaned : DEFAULT_ONCHAR_PREFIXES;
}
function stripOncharPrefix(text, prefixes) {
	const trimmed = text.trimStart();
	for (const prefix of prefixes) {
		if (!prefix) continue;
		if (trimmed.startsWith(prefix)) return {
			triggered: true,
			stripped: trimmed.slice(prefix.length).trimStart()
		};
	}
	return {
		triggered: false,
		stripped: text
	};
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-resources.ts
const CHANNEL_CACHE_TTL_MS = 5 * 6e4;
const USER_CACHE_TTL_MS = 10 * 6e4;
function createMattermostMonitorResources(params) {
	const { accountId, callbackUrl, client, logger, mediaMaxBytes, saveRemoteMedia, mediaKindFromMime } = params;
	const channelCache = /* @__PURE__ */ new Map();
	const userCache = /* @__PURE__ */ new Map();
	const getCachedValue = (cache, key, nowMs) => {
		const cached = cache.get(key);
		if (!cached) return;
		if (nowMs !== void 0 && cached.expiresAt > nowMs) return cached.value;
		cache.delete(key);
	};
	const setCachedValue = (cache, key, value, ttlMs, rawNowMs) => {
		const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs });
		if (expiresAt !== void 0) cache.set(key, {
			value,
			expiresAt
		});
	};
	const resolveMattermostMedia = async (fileIds) => {
		const ids = normalizeStringEntries(fileIds ?? []);
		if (ids.length === 0) return [];
		const out = [];
		for (const fileId of ids) try {
			const saved = await saveRemoteMedia({
				url: `${client.apiBaseUrl}/files/${fileId}`,
				requestInit: { headers: { Authorization: `Bearer ${client.token}` } },
				filePathHint: fileId,
				maxBytes: mediaMaxBytes,
				ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] }
			});
			const contentType = saved.contentType ?? void 0;
			out.push({
				path: saved.path,
				contentType,
				kind: mediaKindFromMime(contentType) ?? "unknown"
			});
		} catch (err) {
			logger.debug?.(`mattermost: failed to download file ${fileId}: ${String(err)}`);
		}
		return out;
	};
	const sendTypingIndicator = async (channelId, parentId) => {
		await sendMattermostTyping(client, {
			channelId,
			parentId
		});
	};
	const resolveChannelInfo = async (channelId) => {
		const rawNow = Date.now();
		const cached = getCachedValue(channelCache, channelId, asDateTimestampMs(rawNow));
		if (cached !== void 0) return cached;
		try {
			const info = await fetchMattermostChannel(client, channelId);
			setCachedValue(channelCache, channelId, info, CHANNEL_CACHE_TTL_MS, rawNow);
			return info;
		} catch (err) {
			logger.debug?.(`mattermost: channel lookup failed: ${String(err)}`);
			setCachedValue(channelCache, channelId, null, CHANNEL_CACHE_TTL_MS, rawNow);
			return null;
		}
	};
	const resolveUserInfo = async (userId) => {
		const rawNow = Date.now();
		const cached = getCachedValue(userCache, userId, asDateTimestampMs(rawNow));
		if (cached !== void 0) return cached;
		try {
			const info = await fetchMattermostUser(client, userId);
			setCachedValue(userCache, userId, info, USER_CACHE_TTL_MS, rawNow);
			return info;
		} catch (err) {
			logger.debug?.(`mattermost: user lookup failed: ${String(err)}`);
			setCachedValue(userCache, userId, null, USER_CACHE_TTL_MS, rawNow);
			return null;
		}
	};
	const buildModelPickerProps = (channelId, buttons) => buildButtonProps({
		callbackUrl,
		accountId,
		channelId,
		buttons
	});
	const updateModelPickerPost = async (paramsLocal) => {
		const props = buildModelPickerProps(paramsLocal.channelId, paramsLocal.buttons ?? []) ?? { attachments: [] };
		await updateMattermostPost(client, paramsLocal.postId, {
			message: paramsLocal.message,
			props
		});
		return {};
	};
	return {
		resolveMattermostMedia,
		sendTypingIndicator,
		resolveChannelInfo,
		resolveUserInfo,
		updateModelPickerPost
	};
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-slash.ts
function buildSlashCommands(params) {
	const commandsToRegister = [...DEFAULT_COMMAND_SPECS];
	if (!params.nativeSkills) return commandsToRegister;
	try {
		const skillCommands = listSkillCommandsForAgents({ cfg: params.cfg });
		for (const spec of skillCommands) {
			const name = typeof spec.name === "string" ? spec.name.trim() : "";
			if (!name) continue;
			const trigger = name.startsWith("oc_") ? name : `oc_${name}`;
			commandsToRegister.push({
				trigger,
				description: spec.description || `Run skill ${name}`,
				autoComplete: true,
				autoCompleteHint: "[args]",
				originalName: name
			});
		}
	} catch (err) {
		params.runtime.error?.(`mattermost: failed to list skill commands: ${String(err)}`);
	}
	return commandsToRegister;
}
function dedupeSlashCommands(commands) {
	const seen = /* @__PURE__ */ new Set();
	return commands.filter((cmd) => {
		const key = cmd.trigger.trim();
		if (!key || seen.has(key)) return false;
		seen.add(key);
		return true;
	});
}
function buildTriggerMap(commands) {
	const triggerMap = /* @__PURE__ */ new Map();
	for (const cmd of commands) if (cmd.originalName) triggerMap.set(cmd.trigger, cmd.originalName);
	return triggerMap;
}
function warnOnSuspiciousCallbackUrl(params) {
	try {
		const mmHost = new URL(normalizeMattermostBaseUrl(params.baseUrl) ?? params.baseUrl).hostname;
		const callbackHost = new URL(params.callbackUrl).hostname;
		if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) params.runtime.error?.(`mattermost: slash commands callbackUrl resolved to ${params.callbackUrl} (loopback) while baseUrl is ${params.baseUrl}. This MAY be unreachable depending on your deployment. If native slash commands don't work, set channels.mattermost.commands.callbackUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`);
	} catch {}
}
async function registerSlashCommandsAcrossTeams(params) {
	const registered = [];
	let teamRegistrationFailures = 0;
	for (const team of params.teams) try {
		const created = await registerSlashCommands({
			client: params.client,
			teamId: team.id,
			creatorUserId: params.botUserId,
			callbackUrl: params.callbackUrl,
			commands: params.commands,
			log: (msg) => params.runtime.log?.(msg)
		});
		registered.push(...created);
	} catch (err) {
		teamRegistrationFailures += 1;
		params.runtime.error?.(`mattermost: failed to register slash commands for team ${team.id}: ${String(err)}`);
	}
	return {
		registered,
		teamRegistrationFailures
	};
}
async function registerMattermostMonitorSlashCommands(params) {
	const commandsRaw = params.account.config.commands;
	const slashConfig = resolveSlashCommandConfig(commandsRaw);
	if (!isSlashCommandsEnabled(slashConfig)) return;
	try {
		const teams = await fetchMattermostUserTeams(params.client, params.botUserId);
		const slashCallbackUrl = resolveCallbackUrl({
			config: slashConfig,
			gatewayPort: parseTcpPort(process.env.OPENCLAW_GATEWAY_PORT) ?? params.cfg.gateway?.port ?? 18789,
			gatewayHost: params.cfg.gateway?.customBindHost ?? void 0
		});
		warnOnSuspiciousCallbackUrl({
			runtime: params.runtime,
			baseUrl: params.baseUrl,
			callbackUrl: slashCallbackUrl
		});
		const dedupedCommands = dedupeSlashCommands(buildSlashCommands({
			cfg: params.cfg,
			runtime: params.runtime,
			nativeSkills: slashConfig.nativeSkills === true
		}));
		const { registered, teamRegistrationFailures } = await registerSlashCommandsAcrossTeams({
			client: params.client,
			teams,
			botUserId: params.botUserId,
			callbackUrl: slashCallbackUrl,
			commands: dedupedCommands,
			runtime: params.runtime
		});
		if (registered.length === 0) {
			params.runtime.error?.("mattermost: native slash commands enabled but no commands could be registered; keeping slash callbacks inactive");
			return;
		}
		if (teamRegistrationFailures > 0) params.runtime.error?.(`mattermost: slash command registration completed with ${teamRegistrationFailures} team error(s)`);
		activateSlashCommands({
			account: params.account,
			commandTokens: registered.map((cmd) => cmd.token).filter(Boolean),
			registeredCommands: registered,
			triggerMap: buildTriggerMap(dedupedCommands),
			api: {
				cfg: params.cfg,
				runtime: params.runtime
			},
			log: (msg) => params.runtime.log?.(msg)
		});
		params.runtime.log?.(`mattermost: slash commands registered (${registered.length} commands across ${teams.length} teams, callback=${slashCallbackUrl})`);
	} catch (err) {
		params.runtime.error?.(`mattermost: failed to register slash commands: ${String(err)}`);
	}
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor-websocket.ts
const MattermostEventPayloadSchema = object({
	event: string().optional(),
	data: object({
		post: union([string(), MattermostPostSchema]).optional(),
		reaction: union([string(), record(string(), unknown())]).optional(),
		channel_id: string().optional(),
		channel_name: string().optional(),
		channel_display_name: string().optional(),
		channel_type: string().optional(),
		sender_name: string().optional(),
		team_id: string().optional()
	}).optional(),
	broadcast: object({
		channel_id: string().optional(),
		team_id: string().optional(),
		user_id: string().optional()
	}).optional()
});
function parseMattermostEventPayload(raw) {
	return safeParseJsonWithSchema(MattermostEventPayloadSchema, raw);
}
function parseMattermostPost(value) {
	if (typeof value === "string") return safeParseJsonWithSchema(MattermostPostSchema, value);
	return safeParseWithSchema(MattermostPostSchema, value);
}
var WebSocketClosedBeforeOpenError = class extends Error {
	constructor(code, reason) {
		super(`websocket closed before open (code ${code})`);
		this.code = code;
		this.reason = reason;
		this.name = "WebSocketClosedBeforeOpenError";
	}
};
const defaultMattermostWebSocketFactory = (url) => {
	const agent = createDebugProxyWebSocketAgent(resolveDebugProxySettings());
	return new WebSocket(url, agent ? { agent } : void 0);
};
function parsePostedPayload(payload) {
	if (payload.event !== "posted") return null;
	const postData = payload.data?.post;
	if (!postData) return null;
	const post = parseMattermostPost(postData);
	if (!post) return null;
	return {
		payload,
		post
	};
}
function createMattermostConnectOnce(opts) {
	const webSocketFactory = opts.webSocketFactory ?? defaultMattermostWebSocketFactory;
	const healthCheckIntervalMs = opts.healthCheckIntervalMs ?? 3e4;
	const pingIntervalMs = opts.pingIntervalMs ?? 3e4;
	const pongTimeoutMs = opts.pongTimeoutMs ?? 1e4;
	return async () => {
		const flowId = randomUUID();
		const ws = webSocketFactory(opts.wsUrl);
		const onAbort = () => ws.terminate();
		opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
		const getBotUpdateAt = opts.getBotUpdateAt;
		try {
			return await new Promise((resolve, reject) => {
				let opened = false;
				let settled = false;
				let healthCheckEnabled = getBotUpdateAt != null;
				let healthCheckInFlight = false;
				let healthCheckTimer;
				let protocolKeepaliveEnabled = true;
				let protocolPingTimer;
				let protocolPongTimer;
				let initialUpdateAt;
				const clearTimers = () => {
					if (healthCheckTimer !== void 0) {
						clearTimeout(healthCheckTimer);
						healthCheckTimer = void 0;
					}
					if (protocolPingTimer !== void 0) {
						clearTimeout(protocolPingTimer);
						protocolPingTimer = void 0;
					}
					if (protocolPongTimer !== void 0) {
						clearTimeout(protocolPongTimer);
						protocolPongTimer = void 0;
					}
				};
				const stopHealthChecks = () => {
					healthCheckEnabled = false;
					protocolKeepaliveEnabled = false;
					clearTimers();
				};
				const sendProtocolPing = () => {
					if (!protocolKeepaliveEnabled || settled) return;
					if (protocolPongTimer !== void 0) clearTimeout(protocolPongTimer);
					protocolPongTimer = setTimeout(() => {
						protocolPongTimer = void 0;
						if (!protocolKeepaliveEnabled || settled) return;
						opts.runtime.error?.("mattermost websocket pong timeout — reconnecting");
						stopHealthChecks();
						ws.terminate();
					}, pongTimeoutMs);
					try {
						ws.ping();
					} catch (err) {
						if (!protocolKeepaliveEnabled || settled) return;
						opts.runtime.error?.(`mattermost websocket ping failed: ${String(err)}`);
						stopHealthChecks();
						ws.terminate();
					}
				};
				const scheduleProtocolPing = () => {
					if (!protocolKeepaliveEnabled || settled || protocolPingTimer !== void 0) return;
					protocolPingTimer = setTimeout(() => {
						protocolPingTimer = void 0;
						sendProtocolPing();
					}, pingIntervalMs);
				};
				const scheduleHealthCheck = () => {
					if (!getBotUpdateAt || !healthCheckEnabled || settled || healthCheckInFlight) return;
					healthCheckTimer = setTimeout(() => {
						healthCheckTimer = void 0;
						runHealthCheck();
					}, healthCheckIntervalMs);
				};
				const runHealthCheck = async () => {
					if (!getBotUpdateAt || !healthCheckEnabled || settled || healthCheckInFlight) return;
					healthCheckInFlight = true;
					try {
						const current = await getBotUpdateAt();
						if (!healthCheckEnabled || settled) return;
						if (initialUpdateAt === void 0) {
							initialUpdateAt = current;
							return;
						}
						if (current !== initialUpdateAt) {
							opts.runtime.log?.(`mattermost: bot account updated (update_at changed: ${initialUpdateAt} → ${current}) — reconnecting`);
							stopHealthChecks();
							ws.terminate();
						}
					} catch (err) {
						if (!healthCheckEnabled || settled) return;
						const label = initialUpdateAt === void 0 ? "mattermost: failed to get initial update_at" : "mattermost: health check error";
						opts.runtime.error?.(`${label}: ${String(err)}`);
					} finally {
						healthCheckInFlight = false;
						scheduleHealthCheck();
					}
				};
				const resolveOnce = () => {
					if (settled) return;
					settled = true;
					stopHealthChecks();
					resolve();
				};
				const rejectOnce = (error) => {
					if (settled) return;
					settled = true;
					stopHealthChecks();
					reject(error);
				};
				ws.on("open", () => {
					opened = true;
					captureWsEvent({
						url: opts.wsUrl,
						direction: "local",
						kind: "ws-open",
						flowId,
						meta: { subsystem: "mattermost-websocket" }
					});
					opts.statusSink?.({
						connected: true,
						lastConnectedAt: Date.now(),
						lastError: null
					});
					const authPayload = JSON.stringify({
						seq: opts.nextSeq(),
						action: "authentication_challenge",
						data: { token: opts.botToken }
					});
					captureWsEvent({
						url: opts.wsUrl,
						direction: "outbound",
						kind: "ws-frame",
						flowId,
						payload: authPayload,
						meta: {
							subsystem: "mattermost-websocket",
							eventType: "authentication_challenge"
						}
					});
					ws.send(authPayload);
					scheduleProtocolPing();
					if (getBotUpdateAt) runHealthCheck();
				});
				ws.on("pong", () => {
					if (protocolPongTimer !== void 0) {
						clearTimeout(protocolPongTimer);
						protocolPongTimer = void 0;
					}
					scheduleProtocolPing();
				});
				ws.on("message", async (data) => {
					captureWsEvent({
						url: opts.wsUrl,
						direction: "inbound",
						kind: "ws-frame",
						flowId,
						payload: Buffer.from(rawDataToString(data)),
						meta: { subsystem: "mattermost-websocket" }
					});
					const payload = parseMattermostEventPayload(rawDataToString(data));
					if (!payload) return;
					if (payload.event === "reaction_added" || payload.event === "reaction_removed") {
						if (!opts.onReaction) return;
						try {
							await opts.onReaction(payload);
						} catch (err) {
							opts.runtime.error?.(`mattermost reaction handler failed: ${String(err)}`);
						}
						return;
					}
					if (payload.event !== "posted") return;
					const parsed = parsePostedPayload(payload);
					if (!parsed) return;
					try {
						await opts.onPosted(parsed.post, parsed.payload);
					} catch (err) {
						opts.runtime.error?.(`mattermost handler failed: ${String(err)}`);
					}
				});
				ws.on("close", (code, reason) => {
					captureWsEvent({
						url: opts.wsUrl,
						direction: "local",
						kind: "ws-close",
						flowId,
						closeCode: code,
						payload: reason,
						meta: { subsystem: "mattermost-websocket" }
					});
					stopHealthChecks();
					const message = reasonToString(reason);
					opts.statusSink?.({
						connected: false,
						lastDisconnect: {
							at: Date.now(),
							status: code,
							error: message || void 0
						}
					});
					if (opened) {
						resolveOnce();
						return;
					}
					rejectOnce(new WebSocketClosedBeforeOpenError(code, message || void 0));
				});
				ws.on("error", (err) => {
					captureWsEvent({
						url: opts.wsUrl,
						direction: "local",
						kind: "error",
						flowId,
						errorText: String(err),
						meta: { subsystem: "mattermost-websocket" }
					});
					opts.runtime.error?.(`mattermost websocket error: ${String(err)}`);
					opts.statusSink?.({ lastError: String(err) });
					try {
						ws.close();
					} catch {}
				});
			});
		} finally {
			opts.abortSignal?.removeEventListener("abort", onAbort);
		}
	};
}
function reasonToString(reason) {
	if (!reason) return "";
	if (typeof reason === "string") return reason;
	return reason.length > 0 ? reason.toString("utf8") : "";
}
//#endregion
//#region extensions/mattermost/src/mattermost/no-visible-reply-diagnostic.ts
/**
* Detects the #80501 symptom: `deliverMattermostReplyPayload` accepted a
* substantive (non-reasoning) payload, called the underlying
* `deliverTextOrMediaReply`, and the outcome was `"empty"` — meaning the
* payload had no text and no media to send, so no Mattermost API call
* happened. The agent's run completes successfully, but no visible
* channel/thread reply ever surfaces to the user.
*
* Returns a structured violation when the outcome is `"empty"` for a payload
* that nominally carried user-facing content (text or media bytes that ended
* up dropped by `resolveSendableOutboundReplyParts`/`sendMediaWithLeadingCaption`).
* Returns `null` for `"reasoning_skipped"` (intentional suppression),
* `"text"`, or `"media"` (successful visible sends).
*/
function evaluateMattermostNoVisibleReply(params) {
	if (params.outcome !== "empty") return null;
	const finalText = typeof params.payload.text === "string" ? params.payload.text.trim() : "";
	const mediaUrlCount = countOutboundMedia(params.payload);
	if (finalText.length === 0 && mediaUrlCount === 0) return null;
	return {
		reason: "no-visible-reply-after-final-delivery",
		outcome: params.outcome,
		finalTextLength: finalText.length,
		mediaUrlCount
	};
}
function formatMattermostNoVisibleReplyLog(params) {
	return `mattermost no-visible-reply: ${params.violation.reason} to=${params.to} accountId=${params.accountId} agentId=${params.agentId ?? "unknown"} outcome=${params.violation.outcome} finalTextLength=${params.violation.finalTextLength} mediaUrlCount=${params.violation.mediaUrlCount}`;
}
//#endregion
//#region extensions/mattermost/src/mattermost/reconnect.ts
/**
* Reconnection loop with exponential backoff.
*
* Calls `connectFn` in a while loop. On normal resolve (connection closed),
* the backoff resets. On thrown error (connection failed), the current delay is
* used, then doubled for the next retry.
* The loop exits when `abortSignal` fires.
*/
async function runWithReconnect(connectFn, opts = {}) {
	const { initialDelayMs = 2e3, maxDelayMs = 6e4 } = opts;
	const jitterRatio = Math.max(0, opts.jitterRatio ?? 0);
	const random = opts.random ?? Math.random;
	const backoff = createReconnectBackoff(initialDelayMs, maxDelayMs);
	let attempt = 0;
	while (!opts.abortSignal?.aborted) {
		let outcome = "resolved";
		let error;
		try {
			await connectFn();
			backoff.reset();
		} catch (err) {
			if (opts.abortSignal?.aborted) return;
			outcome = "rejected";
			error = err;
			opts.onError?.(err);
		}
		if (opts.abortSignal?.aborted) return;
		const delayMs = withJitter(backoff.current(), jitterRatio, random);
		if (!(opts.shouldReconnect?.({
			attempt,
			delayMs,
			outcome,
			error
		}) ?? true)) return;
		opts.onReconnect?.(delayMs);
		await sleepAbortable(delayMs, opts.abortSignal);
		if (outcome === "rejected") backoff.increase();
		attempt++;
	}
}
function createReconnectBackoff(initialDelayMs, maxDelayMs) {
	let retryDelay = initialDelayMs;
	return {
		current: () => retryDelay,
		reset: () => {
			retryDelay = initialDelayMs;
		},
		increase: () => {
			retryDelay = Math.min(retryDelay * 2, maxDelayMs);
		}
	};
}
function withJitter(baseMs, jitterRatio, random) {
	if (jitterRatio <= 0) return baseMs;
	const normalized = Math.max(0, Math.min(1, random()));
	const spread = baseMs * jitterRatio;
	return Math.max(1, Math.round(baseMs - spread + normalized * spread * 2));
}
function sleepAbortable(ms, signal) {
	return new Promise((resolve) => {
		if (signal?.aborted) {
			resolve();
			return;
		}
		const onAbort = () => {
			clearTimeout(timer);
			resolve();
		};
		const timer = setTimeout(() => {
			signal?.removeEventListener("abort", onAbort);
			resolve();
		}, ms);
		signal?.addEventListener("abort", onAbort, { once: true });
	});
}
//#endregion
//#region extensions/mattermost/src/mattermost/monitor.ts
function shouldUpdateMattermostDraftToolProgress(account) {
	return account.streamingMode !== "off" && resolveChannelStreamingPreviewToolProgress(account.config);
}
function shouldSuppressMattermostDefaultToolProgressMessages(account) {
	return account.streamingMode !== "off";
}
const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 6e4;
const RECENT_MATTERMOST_MESSAGE_MAX = 2e3;
function normalizeInteractionSourceIps(values) {
	return normalizeTrimmedStringList(values);
}
const recentInboundMessages = createClaimableDedupe({
	ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
	memoryMaxSize: RECENT_MATTERMOST_MESSAGE_MAX
});
var MattermostRetryableInboundError = class extends Error {
	constructor(message, options) {
		super(message, options);
		this.name = "MattermostRetryableInboundError";
	}
};
function buildMattermostModelPickerSelectMessageSid(params) {
	const provider = normalizeLowercaseStringOrEmpty(params.provider);
	const model = normalizeLowercaseStringOrEmpty(params.model);
	return `interaction:${params.postId}:select:${provider}/${model}`;
}
function buildMattermostInboundReplayKeys(params) {
	return uniqueStrings(params.messageIds.map((id) => `${params.accountId}:${id.trim()}`)).filter((key) => !key.endsWith(":"));
}
async function processMattermostReplayGuardedPost(params) {
	const replayGuard = params.replayGuard ?? recentInboundMessages;
	const replayKeys = buildMattermostInboundReplayKeys({
		accountId: params.accountId,
		messageIds: params.messageIds
	});
	if (replayKeys.length === 0) {
		await params.handlePost();
		return "processed";
	}
	const claimedKeys = [];
	for (const replayKey of replayKeys) if ((await replayGuard.claim(replayKey)).kind === "claimed") claimedKeys.push(replayKey);
	if (claimedKeys.length === 0) return "duplicate";
	try {
		await params.handlePost();
		await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
		return "processed";
	} catch (error) {
		if (error instanceof MattermostRetryableInboundError) claimedKeys.forEach((replayKey) => replayGuard.release(replayKey, { error }));
		else await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
		throw error;
	}
}
function resolveRuntime(opts) {
	return opts.runtime ?? {
		log: console.log,
		error: console.error,
		exit: (code) => {
			throw new Error(`exit ${code}`);
		}
	};
}
function isSystemPost(post) {
	return normalizeOptionalString(post.type) !== void 0;
}
function channelChatType(kind) {
	if (kind === "direct") return "direct";
	if (kind === "group") return "group";
	return "channel";
}
function resolveMattermostReplyRootId(params) {
	if (params.kind === "direct") return;
	const threadRootId = normalizeOptionalString(params.threadRootId);
	if (threadRootId) return threadRootId;
	return normalizeOptionalString(params.replyToId);
}
function canFinalizeMattermostPreviewInPlace(params) {
	return resolveMattermostReplyRootId({
		kind: params.kind,
		threadRootId: params.threadRootId,
		replyToId: params.replyToId
	}) === params.previewRootId?.trim();
}
function createDisabledMattermostDraftStream() {
	const noopAsync = async () => {};
	return {
		update: () => {},
		flush: noopAsync,
		postId: () => void 0,
		clear: noopAsync,
		discardPending: noopAsync,
		seal: noopAsync,
		stop: noopAsync,
		forceNewMessage: () => {}
	};
}
async function deliverMattermostReplyWithDraftPreview(params) {
	if (isReasoningReplyPayload(params.payload)) return;
	await deliverWithFinalizableLivePreviewAdapter({
		kind: params.info.kind,
		payload: params.payload,
		adapter: defineFinalizableLivePreviewAdapter({
			draft: {
				flush: params.draftStream.flush,
				clear: params.draftStream.clear,
				discardPending: params.draftStream.discardPending,
				seal: params.draftStream.seal,
				id: params.draftStream.postId
			},
			buildFinalEdit: (payload) => {
				const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
				const ttsSupplement = getReplyPayloadTtsSupplement(payload);
				const previewFinalText = params.resolvePreviewFinalText(payload.text ?? ttsSupplement?.spokenText);
				if (hasMedia && !ttsSupplement || typeof previewFinalText !== "string" || payload.isError || !canFinalizeMattermostPreviewInPlace({
					kind: params.kind,
					previewRootId: params.effectiveReplyToId,
					threadRootId: params.effectiveReplyToId,
					replyToId: payload.replyToId
				})) return;
				return { message: previewFinalText };
			},
			editFinal: async (previewPostId, edit) => {
				await updateMattermostPost(params.client, previewPostId, edit);
			},
			onPreviewFinalized: () => {
				params.previewState.finalizedViaPreviewPost = true;
			},
			buildSupplementalPayload: (payload) => getReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : void 0,
			deliverSupplemental: async (payload) => {
				await params.deliverPayload(payload);
			},
			logPreviewEditFailure: (err) => {
				params.logVerboseMessage(`mattermost preview final edit failed; falling back to normal send (${String(err)})`);
			}
		}),
		deliverNormally: async (payload) => {
			const supplement = getReplyPayloadTtsSupplement(payload);
			await params.deliverPayload(supplement && !payload.text?.trim() && supplement.visibleTextAlreadyDelivered !== true ? {
				...payload,
				text: supplement.spokenText
			} : payload);
		}
	});
}
function formatMattermostFinalDeliveryOutcomeLog(params) {
	const violation = evaluateMattermostNoVisibleReply({
		outcome: params.outcome,
		payload: params.payload
	});
	if (violation) return formatMattermostNoVisibleReplyLog({
		violation,
		to: params.to,
		accountId: params.accountId,
		agentId: params.agentId
	});
	if (params.outcome === "text" || params.outcome === "media") return `delivered reply to ${params.to}`;
}
function resolveMattermostEffectiveReplyToId(params) {
	if (params.kind === "direct") return;
	const threadRootId = normalizeOptionalString(params.threadRootId);
	if (threadRootId) return threadRootId;
	const postId = normalizeOptionalString(params.postId);
	if (!postId) return;
	return params.replyToMode === "all" || params.replyToMode === "first" || params.replyToMode === "batched" ? postId : void 0;
}
function resolveMattermostThreadSessionContext(params) {
	const effectiveReplyToId = resolveMattermostEffectiveReplyToId({
		kind: params.kind,
		postId: params.postId,
		replyToMode: params.replyToMode,
		threadRootId: params.threadRootId
	});
	const threadKeys = resolveThreadSessionKeys({
		baseSessionKey: params.baseSessionKey,
		threadId: effectiveReplyToId,
		parentSessionKey: effectiveReplyToId ? params.baseSessionKey : void 0
	});
	return {
		effectiveReplyToId,
		sessionKey: threadKeys.sessionKey,
		parentSessionKey: threadKeys.parentSessionKey
	};
}
function resolveMattermostReactionChannelId(payload) {
	return normalizeOptionalString(payload.broadcast?.channel_id) ?? normalizeOptionalString(payload.data?.channel_id);
}
function buildMattermostAttachmentPlaceholder(mediaList) {
	if (mediaList.length === 0) return "";
	if (mediaList.length === 1) return `<media:${mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind}>`;
	const allImages = mediaList.every((media) => media.kind === "image");
	const label = allImages ? "image" : "file";
	const suffix = mediaList.length === 1 ? label : `${label}s`;
	return `${allImages ? "<media:image>" : "<media:document>"} (${mediaList.length} ${suffix})`;
}
function buildMattermostWsUrl(baseUrl) {
	const normalized = normalizeMattermostBaseUrl(baseUrl);
	if (!normalized) throw new Error("Mattermost baseUrl is required");
	return `${normalized.replace(/^http/i, "ws")}/api/v4/websocket`;
}
async function monitorMattermostProvider(opts = {}) {
	const core = getMattermostRuntime();
	const runtime = resolveRuntime(opts);
	const cfg = opts.config ?? core.config.current();
	const account = resolveMattermostAccount({
		cfg,
		accountId: opts.accountId
	});
	const pairing = createChannelPairingController({
		core,
		channel: "mattermost",
		accountId: account.accountId
	});
	const botToken = normalizeOptionalString(opts.botToken) ?? normalizeOptionalString(account.botToken);
	if (!botToken) throw new Error(`Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`);
	const baseUrl = normalizeMattermostBaseUrl(opts.baseUrl ?? account.baseUrl);
	if (!baseUrl) throw new Error(`Mattermost baseUrl missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.baseUrl or MATTERMOST_URL for default).`);
	const client = createMattermostClient({
		baseUrl,
		botToken,
		allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config)
	});
	let botUser;
	await runWithReconnect(async () => {
		botUser = await fetchMattermostMe(client);
	}, {
		abortSignal: opts.abortSignal,
		jitterRatio: .2,
		shouldReconnect: ({ outcome }) => outcome === "rejected",
		onError: (err) => {
			runtime.error?.(`mattermost: API auth failed: ${String(err)}`);
			opts.statusSink?.({
				lastError: String(err),
				connected: false
			});
		},
		onReconnect: (delayMs) => {
			runtime.log?.(`mattermost: API not accessible, retrying in ${Math.round(delayMs / 1e3)}s`);
		}
	});
	if (opts.abortSignal?.aborted) return;
	const botUserId = botUser.id;
	const botUsername = normalizeOptionalString(botUser.username);
	runtime.log?.(`mattermost connected as ${botUsername ? `@${botUsername}` : botUserId}`);
	await registerMattermostMonitorSlashCommands({
		client,
		cfg,
		runtime,
		account,
		baseUrl,
		botUserId
	});
	const slashEnabled = getSlashCommandState(account.accountId) != null;
	setInteractionSecret(account.accountId, botToken);
	const interactionPath = resolveInteractionCallbackPath(account.accountId);
	const callbackUrl = computeInteractionCallbackUrl(account.accountId, {
		gateway: cfg.gateway,
		interactions: account.config.interactions
	});
	setInteractionCallbackUrl(account.accountId, callbackUrl);
	const allowedInteractionSourceIps = normalizeInteractionSourceIps(account.config.interactions?.allowedSourceIps);
	try {
		const mmHost = new URL(baseUrl).hostname;
		const callbackHost = new URL(callbackUrl).hostname;
		if (isLoopbackHost(callbackHost) && !isLoopbackHost(mmHost)) runtime.error?.(`mattermost: interactions callbackUrl resolved to ${callbackUrl} (loopback) while baseUrl is ${baseUrl}. This MAY be unreachable depending on your deployment. If button clicks don't work, set channels.mattermost.interactions.callbackBaseUrl to a URL reachable from the Mattermost server (e.g. your public reverse proxy URL).`);
		if (!isLoopbackHost(callbackHost) && allowedInteractionSourceIps.length === 0) runtime.error?.(`mattermost: interactions callbackUrl resolved to ${callbackUrl} without channels.mattermost.interactions.allowedSourceIps. For safety, non-loopback callback sources will be rejected until you allowlist the Mattermost server or trusted ingress IPs.`);
	} catch {}
	const effectiveInteractionSourceIps = allowedInteractionSourceIps.length > 0 ? allowedInteractionSourceIps : ["127.0.0.1", "::1"];
	const unregisterInteractions = registerPluginHttpRoute({
		path: interactionPath,
		fallbackPath: "/mattermost/interactions/default",
		auth: "plugin",
		handler: createMattermostInteractionHandler({
			client,
			botUserId,
			accountId: account.accountId,
			allowedSourceIps: effectiveInteractionSourceIps,
			trustedProxies: cfg.gateway?.trustedProxies,
			allowRealIpFallback: cfg.gateway?.allowRealIpFallback === true,
			handleInteraction: handleModelPickerInteraction,
			authorizeButtonClick: async ({ payload, post }) => {
				const channelInfo = await resolveChannelInfo(payload.channel_id);
				const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
					cfg,
					surface: "mattermost"
				});
				const decision = await authorizeMattermostCommandInvocation({
					account,
					cfg,
					senderId: payload.user_id,
					senderName: payload.user_name ?? "",
					channelId: payload.channel_id,
					channelInfo,
					readStoreAllowFrom: pairing.readAllowFromStore,
					allowTextCommands,
					hasControlCommand: false
				});
				if (decision.ok) return { ok: true };
				return {
					ok: false,
					response: {
						update: {
							message: post.message ?? "",
							props: post.props ?? void 0
						},
						ephemeral_text: `OpenClaw ignored this action for ${decision.roomLabel}.`
					}
				};
			},
			resolveSessionKey: async ({ channelId, userId, post }) => {
				const channelInfo = await resolveChannelInfo(channelId);
				if (!channelInfo?.type) {
					logVerboseMessage(`mattermost: drop interaction session event (cannot resolve channel type for ${channelId})`);
					throw new Error("Mattermost channel type could not be resolved");
				}
				const kind = mapMattermostChannelTypeToChatType(channelInfo.type);
				const teamId = channelInfo?.team_id ?? void 0;
				const route = core.channel.routing.resolveAgentRoute({
					cfg,
					channel: "mattermost",
					accountId: account.accountId,
					teamId,
					peer: {
						kind,
						id: kind === "direct" ? userId : channelId
					}
				});
				const replyToMode = resolveMattermostReplyToMode(account, kind);
				return resolveMattermostThreadSessionContext({
					baseSessionKey: route.sessionKey,
					kind,
					postId: post.id || void 0,
					replyToMode,
					threadRootId: post.root_id
				}).sessionKey;
			},
			dispatchButtonClick: async (optsLocal) => {
				const channelInfo = await resolveChannelInfo(optsLocal.channelId);
				if (!channelInfo?.type) {
					logVerboseMessage(`mattermost: drop interaction dispatch (cannot resolve channel type for ${optsLocal.channelId})`);
					return;
				}
				const kind = mapMattermostChannelTypeToChatType(channelInfo.type);
				const chatType = channelChatType(kind);
				const teamId = channelInfo?.team_id ?? void 0;
				const channelName = channelInfo?.name ?? void 0;
				const channelDisplay = channelInfo?.display_name ?? channelName ?? optsLocal.channelId;
				const route = core.channel.routing.resolveAgentRoute({
					cfg,
					channel: "mattermost",
					accountId: account.accountId,
					teamId,
					peer: {
						kind,
						id: kind === "direct" ? optsLocal.userId : optsLocal.channelId
					}
				});
				const replyToMode = resolveMattermostReplyToMode(account, kind);
				const threadContext = resolveMattermostThreadSessionContext({
					baseSessionKey: route.sessionKey,
					kind,
					postId: optsLocal.post.id || optsLocal.postId,
					replyToMode,
					threadRootId: optsLocal.post.root_id
				});
				const to = kind === "direct" ? `user:${optsLocal.userId}` : `channel:${optsLocal.channelId}`;
				const bodyText = `[Button click: user @${optsLocal.userName} selected "${optsLocal.actionName}"]`;
				const ctxPayload = core.channel.reply.finalizeInboundContext({
					Body: bodyText,
					BodyForAgent: bodyText,
					RawBody: bodyText,
					CommandBody: bodyText,
					From: kind === "direct" ? `mattermost:${optsLocal.userId}` : kind === "group" ? `mattermost:group:${optsLocal.channelId}` : `mattermost:channel:${optsLocal.channelId}`,
					To: to,
					SessionKey: threadContext.sessionKey,
					ParentSessionKey: threadContext.parentSessionKey,
					AccountId: route.accountId,
					ChatType: chatType,
					ConversationLabel: `mattermost:${optsLocal.userName}`,
					GroupSubject: kind !== "direct" ? channelDisplay : void 0,
					GroupChannel: channelName ? `#${channelName}` : void 0,
					GroupSpace: teamId,
					SenderName: optsLocal.userName,
					SenderId: optsLocal.userId,
					Provider: "mattermost",
					Surface: "mattermost",
					MessageSid: `interaction:${optsLocal.postId}:${optsLocal.actionId}`,
					ReplyToId: threadContext.effectiveReplyToId,
					MessageThreadId: threadContext.effectiveReplyToId,
					WasMentioned: true,
					CommandAuthorized: false,
					OriginatingChannel: "mattermost",
					OriginatingTo: to
				});
				const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
				const tableMode = core.channel.text.resolveMarkdownTableMode({
					cfg,
					channel: "mattermost",
					accountId: account.accountId
				});
				const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
					cfg,
					agentId: route.agentId,
					channel: "mattermost",
					accountId: account.accountId,
					typing: {
						start: () => sendTypingIndicator(optsLocal.channelId, threadContext.effectiveReplyToId),
						onStartError: (err) => {
							logTypingFailure({
								log: (message) => logger.debug?.(message),
								channel: "mattermost",
								target: optsLocal.channelId,
								error: err
							});
						}
					}
				});
				const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
					...replyPipeline,
					humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
					deliver: async (payload) => {
						await deliverMattermostReplyPayload({
							core,
							cfg,
							payload,
							to,
							accountId: account.accountId,
							agentId: route.agentId,
							replyToId: resolveMattermostReplyRootId({
								kind,
								threadRootId: threadContext.effectiveReplyToId,
								replyToId: payload.replyToId
							}),
							textLimit,
							tableMode,
							sendMessage: sendMessageMattermost
						});
						runtime.log?.(`delivered button-click reply to ${to}`);
					},
					onError: (err, info) => {
						runtime.error?.(`mattermost button-click ${info.kind} reply failed: ${String(err)}`);
					},
					onReplyStart: typingCallbacks?.onReplyStart
				});
				await core.channel.reply.dispatchReplyFromConfig({
					ctx: ctxPayload,
					cfg,
					dispatcher,
					replyOptions: {
						...replyOptions,
						disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
						onModelSelected
					}
				});
				markDispatchIdle();
			},
			log: (msg) => runtime.log?.(msg)
		}),
		pluginId: "mattermost",
		source: "mattermost-interactions",
		accountId: account.accountId,
		log: (msg) => runtime.log?.(msg)
	});
	const logger = core.logging.getChildLogger({ module: "mattermost" });
	const logVerboseMessage = (message) => {
		if (!core.logging.shouldLogVerbose()) return;
		logger.debug?.(message);
	};
	const mediaMaxBytes = resolveChannelMediaMaxBytes({
		cfg,
		resolveChannelLimitMb: () => void 0,
		accountId: account.accountId
	}) ?? 8 * 1024 * 1024;
	const historyLimit = Math.max(0, cfg.messages?.groupChat?.historyLimit ?? 50);
	const channelHistories = /* @__PURE__ */ new Map();
	const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
	const dmPolicy = account.config.dmPolicy ?? "pairing";
	const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
		providerConfigPresent: cfg.channels?.mattermost !== void 0,
		groupPolicy: account.config.groupPolicy,
		defaultGroupPolicy
	});
	warnMissingProviderGroupPolicyFallbackOnce({
		providerMissingFallbackApplied,
		providerKey: "mattermost",
		accountId: account.accountId,
		log: (message) => logVerboseMessage(message)
	});
	const { resolveMattermostMedia, sendTypingIndicator, resolveChannelInfo, resolveUserInfo, updateModelPickerPost } = createMattermostMonitorResources({
		accountId: account.accountId,
		callbackUrl,
		client,
		logger: { debug: (message) => logger.debug?.(String(message)) },
		mediaMaxBytes,
		saveRemoteMedia: (params) => core.channel.media.saveRemoteMedia(params),
		mediaKindFromMime: (contentType) => core.media.mediaKindFromMime(contentType)
	});
	const runModelPickerCommand = async (params) => {
		const to = params.kind === "direct" ? `user:${params.senderId}` : `channel:${params.channelId}`;
		const fromLabel = params.kind === "direct" ? `Mattermost DM from ${params.senderName}` : `Mattermost message in ${params.roomLabel} from ${params.senderName}`;
		const ctxPayload = core.channel.reply.finalizeInboundContext({
			Body: params.commandText,
			BodyForAgent: params.commandText,
			RawBody: params.commandText,
			CommandBody: params.commandText,
			From: params.kind === "direct" ? `mattermost:${params.senderId}` : params.kind === "group" ? `mattermost:group:${params.channelId}` : `mattermost:channel:${params.channelId}`,
			To: to,
			SessionKey: params.sessionKey,
			ParentSessionKey: params.parentSessionKey,
			AccountId: params.route.accountId,
			ChatType: params.chatType,
			ConversationLabel: fromLabel,
			GroupSubject: params.kind !== "direct" ? params.channelDisplay || params.roomLabel : void 0,
			GroupChannel: params.channelName ? `#${params.channelName}` : void 0,
			GroupSpace: params.teamId,
			SenderName: params.senderName,
			SenderId: params.senderId,
			Provider: "mattermost",
			Surface: "mattermost",
			MessageSid: params.messageSid ?? `interaction:${params.postId}:${Date.now()}`,
			ReplyToId: params.effectiveReplyToId,
			MessageThreadId: params.effectiveReplyToId,
			Timestamp: Date.now(),
			WasMentioned: true,
			CommandAuthorized: params.commandAuthorized,
			CommandSource: "native",
			OriginatingChannel: "mattermost",
			OriginatingTo: to
		});
		const tableMode = core.channel.text.resolveMarkdownTableMode({
			cfg,
			channel: "mattermost",
			accountId: account.accountId
		});
		const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
		const shouldDeliverReplies = params.deliverReplies === true;
		const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
			cfg,
			agentId: params.route.agentId,
			channel: "mattermost",
			accountId: account.accountId,
			typing: shouldDeliverReplies ? {
				start: () => sendTypingIndicator(params.channelId, params.effectiveReplyToId),
				onStartError: (err) => {
					logTypingFailure({
						log: (message) => logger.debug?.(message),
						channel: "mattermost",
						target: params.channelId,
						error: err
					});
				}
			} : void 0
		});
		const capturedTexts = [];
		const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
			...replyPipeline,
			deliver: async (payload) => {
				const trimmedPayload = {
					...payload,
					text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode).trim()
				};
				if (!shouldDeliverReplies) {
					if (trimmedPayload.text) capturedTexts.push(trimmedPayload.text);
					return;
				}
				await deliverMattermostReplyPayload({
					core,
					cfg,
					payload: trimmedPayload,
					to,
					accountId: account.accountId,
					agentId: params.route.agentId,
					replyToId: resolveMattermostReplyRootId({
						kind: params.kind,
						threadRootId: params.effectiveReplyToId,
						replyToId: trimmedPayload.replyToId
					}),
					textLimit,
					tableMode: "off",
					sendMessage: sendMessageMattermost
				});
			},
			onError: (err, info) => {
				runtime.error?.(`mattermost model picker ${info.kind} reply failed: ${String(err)}`);
			},
			onReplyStart: typingCallbacks?.onReplyStart
		});
		await core.channel.reply.withReplyDispatcher({
			dispatcher,
			onSettled: () => {
				markDispatchIdle();
			},
			run: () => core.channel.reply.dispatchReplyFromConfig({
				ctx: ctxPayload,
				cfg,
				dispatcher,
				replyOptions: {
					...replyOptions,
					disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
					onModelSelected
				}
			})
		});
		return capturedTexts.join("\n\n").trim();
	};
	async function handleModelPickerInteraction(params) {
		const pickerState = parseMattermostModelPickerContext(params.context);
		if (!pickerState) return null;
		if (pickerState.ownerUserId !== params.payload.user_id) return { ephemeral_text: "Only the person who opened this picker can use it." };
		const channelInfo = await resolveChannelInfo(params.payload.channel_id);
		const pickerCommandText = pickerState.action === "select" ? `/model ${pickerState.provider}/${pickerState.model}` : pickerState.action === "list" ? `/models ${pickerState.provider}` : "/models";
		const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
			cfg,
			surface: "mattermost"
		});
		const hasControlCommand = core.channel.text.hasControlCommand(pickerCommandText, cfg);
		const auth = await authorizeMattermostCommandInvocation({
			account,
			cfg,
			senderId: params.payload.user_id,
			senderName: params.userName,
			channelId: params.payload.channel_id,
			channelInfo,
			readStoreAllowFrom: pairing.readAllowFromStore,
			allowTextCommands,
			hasControlCommand
		});
		if (!auth.ok) {
			if (auth.denyReason === "dm-pairing") {
				const { code } = await pairing.upsertPairingRequest({
					id: params.payload.user_id,
					meta: { name: params.userName }
				});
				return { ephemeral_text: core.channel.pairing.buildPairingReply({
					channel: "mattermost",
					idLine: `Your Mattermost user id: ${params.payload.user_id}`,
					code
				}) };
			}
			return { ephemeral_text: auth.denyReason === "unknown-channel" ? "Temporary error: unable to determine channel type. Please try again." : auth.denyReason === "dm-disabled" ? "This bot is not accepting direct messages." : auth.denyReason === "channels-disabled" ? "Model picker actions are disabled in channels." : auth.denyReason === "channel-no-allowlist" ? "Model picker actions are not configured for this channel." : "Unauthorized." };
		}
		const kind = auth.kind;
		const chatType = auth.chatType;
		const teamId = auth.channelInfo.team_id ?? params.payload.team_id ?? void 0;
		const channelName = auth.channelName || void 0;
		const channelDisplay = auth.channelDisplay || auth.channelName || params.payload.channel_id;
		const roomLabel = auth.roomLabel;
		const route = core.channel.routing.resolveAgentRoute({
			cfg,
			channel: "mattermost",
			accountId: account.accountId,
			teamId,
			peer: {
				kind,
				id: kind === "direct" ? params.payload.user_id : params.payload.channel_id
			}
		});
		const replyToMode = resolveMattermostReplyToMode(account, kind);
		const threadContext = resolveMattermostThreadSessionContext({
			baseSessionKey: route.sessionKey,
			kind,
			postId: params.post.id || params.payload.post_id,
			replyToMode,
			threadRootId: params.post.root_id
		});
		const modelSessionRoute = {
			agentId: route.agentId,
			sessionKey: threadContext.sessionKey
		};
		const data = await buildModelsProviderData(cfg, route.agentId);
		if (data.providers.length === 0) return await updateModelPickerPost({
			channelId: params.payload.channel_id,
			postId: params.payload.post_id,
			message: "No models available."
		});
		if (pickerState.action === "providers" || pickerState.action === "back") {
			const currentModel = resolveMattermostModelPickerCurrentModel({
				cfg,
				route: modelSessionRoute,
				data
			});
			const view = renderMattermostProviderPickerView({
				ownerUserId: pickerState.ownerUserId,
				data,
				currentModel
			});
			return await updateModelPickerPost({
				channelId: params.payload.channel_id,
				postId: params.payload.post_id,
				message: view.text,
				buttons: view.buttons
			});
		}
		if (pickerState.action === "list") {
			const currentModel = resolveMattermostModelPickerCurrentModel({
				cfg,
				route: modelSessionRoute,
				data
			});
			const view = renderMattermostModelsPickerView({
				ownerUserId: pickerState.ownerUserId,
				data,
				provider: pickerState.provider,
				page: pickerState.page,
				currentModel
			});
			return await updateModelPickerPost({
				channelId: params.payload.channel_id,
				postId: params.payload.post_id,
				message: view.text,
				buttons: view.buttons
			});
		}
		const targetModelRef = `${pickerState.provider}/${pickerState.model}`;
		if (!buildMattermostAllowedModelRefs(data).has(targetModelRef)) return { ephemeral_text: `That model is no longer available: ${targetModelRef}` };
		(async () => {
			try {
				await runModelPickerCommand({
					commandText: `/model ${targetModelRef}`,
					commandAuthorized: auth.commandAuthorized,
					route,
					sessionKey: threadContext.sessionKey,
					parentSessionKey: threadContext.parentSessionKey,
					channelId: params.payload.channel_id,
					senderId: params.payload.user_id,
					senderName: params.userName,
					kind,
					chatType,
					channelName,
					channelDisplay,
					roomLabel,
					teamId,
					postId: params.payload.post_id,
					messageSid: buildMattermostModelPickerSelectMessageSid({
						postId: params.payload.post_id,
						provider: pickerState.provider,
						model: pickerState.model
					}),
					effectiveReplyToId: threadContext.effectiveReplyToId,
					deliverReplies: true
				});
				const updatedModel = resolveMattermostModelPickerCurrentModel({
					cfg,
					route: modelSessionRoute,
					data,
					skipCache: true
				});
				const view = renderMattermostModelsPickerView({
					ownerUserId: pickerState.ownerUserId,
					data,
					provider: pickerState.provider,
					page: pickerState.page,
					currentModel: updatedModel
				});
				await updateModelPickerPost({
					channelId: params.payload.channel_id,
					postId: params.payload.post_id,
					message: view.text,
					buttons: view.buttons
				});
			} catch (err) {
				runtime.error?.(`mattermost model picker select failed: ${String(err)}`);
			}
		})();
		return {};
	}
	const handlePost = async (post, payload, messageIds) => {
		const channelId = post.channel_id ?? payload.data?.channel_id ?? payload.broadcast?.channel_id;
		if (!channelId) {
			logVerboseMessage("mattermost: drop post (missing channel id)");
			return;
		}
		const allMessageIds = messageIds?.length ? messageIds : post.id ? [post.id] : [];
		if (allMessageIds.length === 0) {
			logVerboseMessage("mattermost: drop post (missing message id)");
			return;
		}
		if (await processMattermostReplayGuardedPost({
			accountId: account.accountId,
			messageIds: allMessageIds,
			handlePost: async () => {
				const senderId = post.user_id ?? payload.broadcast?.user_id;
				if (!senderId) {
					logVerboseMessage("mattermost: drop post (missing sender id)");
					return;
				}
				if (senderId === botUserId) {
					logVerboseMessage(`mattermost: drop post (self sender=${senderId})`);
					return;
				}
				if (isSystemPost(post)) {
					logVerboseMessage(`mattermost: drop post (system post type=${post.type ?? "unknown"})`);
					return;
				}
				const channelInfo = await resolveChannelInfo(channelId);
				const channelType = normalizeOptionalString(channelInfo?.type) ?? normalizeOptionalString(payload.data?.channel_type);
				if (!channelType) {
					logVerboseMessage(`mattermost: drop post (cannot resolve channel type for ${channelId})`);
					return;
				}
				const kind = resolveMattermostTrustedChatKind({ channelType });
				const chatType = channelChatType(kind);
				const senderName = normalizeOptionalString(payload.data?.sender_name) ?? normalizeOptionalString((await resolveUserInfo(senderId))?.username) ?? senderId;
				const rawText = normalizeOptionalString(post.message) ?? "";
				const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
					cfg,
					surface: "mattermost"
				});
				const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(rawText, cfg);
				const accessDecision = await resolveMattermostMonitorInboundAccess({
					account,
					cfg,
					senderId,
					senderName,
					channelId,
					kind,
					groupPolicy,
					readStoreAllowFrom: pairing.readAllowFromStore,
					allowTextCommands,
					hasControlCommand: isControlCommand,
					eventKind: "message",
					mayPair: true
				});
				const commandAuthorized = accessDecision.commandAccess.authorized;
				if (accessDecision.ingress.decision !== "allow") {
					if (kind === "direct") {
						if (accessDecision.ingress.reasonCode === "dm_policy_disabled") {
							logVerboseMessage(`mattermost: drop dm (dmPolicy=disabled sender=${senderId})`);
							return;
						}
						if (accessDecision.ingress.decision === "pairing") {
							const { code, created } = await pairing.upsertPairingRequest({
								id: senderId,
								meta: { name: senderName }
							});
							logVerboseMessage(`mattermost: pairing request sender=${senderId} created=${created}`);
							if (created) try {
								await sendMessageMattermost(`user:${senderId}`, core.channel.pairing.buildPairingReply({
									channel: "mattermost",
									idLine: `Your Mattermost user id: ${senderId}`,
									code
								}), {
									cfg,
									accountId: account.accountId
								});
								opts.statusSink?.({ lastOutboundAt: Date.now() });
							} catch (err) {
								logVerboseMessage(`mattermost: pairing reply failed for ${senderId}: ${String(err)}`);
							}
							return;
						}
						logVerboseMessage(`mattermost: drop dm sender=${senderId} (dmPolicy=${dmPolicy})`);
						return;
					}
					if (accessDecision.ingress.reasonCode === "group_policy_disabled") {
						logVerboseMessage("mattermost: drop group message (groupPolicy=disabled)");
						return;
					}
					if (accessDecision.ingress.reasonCode === "group_policy_empty_allowlist") {
						logVerboseMessage("mattermost: drop group message (no group allowlist)");
						return;
					}
					if (accessDecision.ingress.reasonCode === "group_policy_not_allowlisted") {
						logVerboseMessage(`mattermost: drop group sender=${senderId} (not in groupAllowFrom)`);
						return;
					}
					logVerboseMessage(`mattermost: drop group message (groupPolicy=${groupPolicy} reason=${accessDecision.senderAccess.reasonCode})`);
					return;
				}
				if (kind !== "direct" && accessDecision.commandAccess.shouldBlockControlCommand) {
					logInboundDrop({
						log: logVerboseMessage,
						channel: "mattermost",
						reason: "control command (unauthorized)",
						target: senderId
					});
					return;
				}
				const teamId = payload.data?.team_id ?? channelInfo?.team_id ?? void 0;
				const channelName = payload.data?.channel_name ?? channelInfo?.name ?? "";
				const channelDisplay = payload.data?.channel_display_name ?? channelInfo?.display_name ?? channelName;
				const roomLabel = channelName ? `#${channelName}` : channelDisplay || `#${channelId}`;
				const route = core.channel.routing.resolveAgentRoute({
					cfg,
					channel: "mattermost",
					accountId: account.accountId,
					teamId,
					peer: {
						kind,
						id: kind === "direct" ? senderId : channelId
					}
				});
				const baseSessionKey = route.sessionKey;
				const threadRootId = normalizeOptionalString(post.root_id);
				const replyToMode = resolveMattermostReplyToMode(account, kind);
				const { effectiveReplyToId, sessionKey, parentSessionKey } = resolveMattermostThreadSessionContext({
					baseSessionKey,
					kind,
					postId: post.id,
					replyToMode,
					threadRootId
				});
				const historyKey = kind === "direct" ? null : sessionKey;
				const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId);
				const wasMentioned = kind !== "direct" && ((botUsername ? normalizeLowercaseStringOrEmpty(rawText).includes(`@${normalizeLowercaseStringOrEmpty(botUsername)}`) : false) || core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes));
				const pendingBody = rawText || (post.file_ids?.length ? `[Mattermost ${post.file_ids.length === 1 ? "file" : "files"}]` : "");
				const pendingSender = senderName;
				const recordPendingHistory = () => {
					const trimmed = pendingBody.trim();
					createChannelHistoryWindow({ historyMap: channelHistories }).record({
						limit: historyLimit,
						historyKey: historyKey ?? "",
						entry: historyKey && trimmed ? {
							sender: pendingSender,
							body: trimmed,
							timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
							messageId: post.id ?? void 0
						} : null
					});
				};
				const oncharEnabled = account.chatmode === "onchar" && kind !== "direct";
				const oncharPrefixes = oncharEnabled ? resolveOncharPrefixes(account.oncharPrefixes) : [];
				const oncharResult = oncharEnabled ? stripOncharPrefix(rawText, oncharPrefixes) : {
					triggered: false,
					stripped: rawText
				};
				const oncharTriggered = oncharResult.triggered;
				const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
				const mentionDecision = evaluateMattermostMentionGate({
					kind,
					cfg,
					accountId: account.accountId,
					channelId,
					threadRootId,
					requireMentionOverride: account.requireMention,
					resolveRequireMention: core.channel.groups.resolveRequireMention,
					wasMentioned,
					isControlCommand,
					commandAuthorized,
					oncharEnabled,
					oncharTriggered,
					canDetectMention
				});
				const { shouldRequireMention, shouldBypassMention } = mentionDecision;
				if (mentionDecision.dropReason === "onchar-not-triggered") {
					logVerboseMessage(`mattermost: drop group message (onchar not triggered channel=${channelId} sender=${senderId})`);
					recordPendingHistory();
					return;
				}
				if (mentionDecision.dropReason === "missing-mention") {
					logVerboseMessage(`mattermost: drop group message (missing mention channel=${channelId} sender=${senderId} requireMention=${shouldRequireMention} bypass=${shouldBypassMention} canDetectMention=${canDetectMention})`);
					recordPendingHistory();
					return;
				}
				const mediaList = await resolveMattermostMedia(post.file_ids);
				const mediaPlaceholder = buildMattermostAttachmentPlaceholder(mediaList);
				const bodyText = normalizeMention([oncharTriggered ? oncharResult.stripped : rawText, mediaPlaceholder].filter(Boolean).join("\n").trim(), botUsername);
				if (!bodyText) {
					logVerboseMessage(`mattermost: drop group message (empty body after normalization channel=${channelId} sender=${senderId})`);
					return;
				}
				core.channel.activity.record({
					channel: "mattermost",
					accountId: account.accountId,
					direction: "inbound"
				});
				const fromLabel = formatInboundFromLabel({
					isGroup: kind !== "direct",
					groupLabel: channelDisplay || roomLabel,
					groupId: channelId,
					groupFallback: roomLabel || "Channel",
					directLabel: senderName,
					directId: senderId
				});
				const textWithId = `${bodyText}\n[mattermost message id: ${post.id ?? "unknown"} channel: ${channelId}]`;
				let combinedBody = core.channel.reply.formatInboundEnvelope({
					channel: "Mattermost",
					from: fromLabel,
					timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
					body: textWithId,
					chatType,
					sender: {
						name: senderName,
						id: senderId
					}
				});
				if (historyKey) combinedBody = createChannelHistoryWindow({ historyMap: channelHistories }).buildPendingContext({
					historyKey,
					limit: historyLimit,
					currentMessage: combinedBody,
					formatEntry: (entry) => core.channel.reply.formatInboundEnvelope({
						channel: "Mattermost",
						from: fromLabel,
						timestamp: entry.timestamp,
						body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId} channel:${channelId}]` : ""}`,
						chatType,
						senderLabel: entry.sender
					})
				});
				const to = kind === "direct" ? `user:${senderId}` : `channel:${channelId}`;
				const mediaPayload = buildAgentMediaPayload(mediaList);
				const commandBody = rawText.trim();
				const inboundHistory = historyKey && historyLimit > 0 ? createChannelHistoryWindow({ historyMap: channelHistories }).buildInboundHistory({
					historyKey,
					limit: historyLimit
				}) : void 0;
				const ctxPayload = core.channel.reply.finalizeInboundContext({
					Body: combinedBody,
					BodyForAgent: bodyText,
					InboundHistory: inboundHistory,
					RawBody: bodyText,
					CommandBody: commandBody,
					BodyForCommands: commandBody,
					From: kind === "direct" ? `mattermost:${senderId}` : kind === "group" ? `mattermost:group:${channelId}` : `mattermost:channel:${channelId}`,
					To: to,
					SessionKey: sessionKey,
					ParentSessionKey: parentSessionKey,
					AccountId: route.accountId,
					ChatType: chatType,
					ConversationLabel: fromLabel,
					GroupSubject: kind !== "direct" ? channelDisplay || roomLabel : void 0,
					GroupChannel: channelName ? `#${channelName}` : void 0,
					GroupSpace: teamId,
					SenderName: senderName,
					SenderId: senderId,
					Provider: "mattermost",
					Surface: "mattermost",
					MessageSid: post.id ?? void 0,
					MessageSids: allMessageIds.length > 1 ? allMessageIds : void 0,
					MessageSidFirst: allMessageIds.length > 1 ? allMessageIds[0] : void 0,
					MessageSidLast: allMessageIds.length > 1 ? allMessageIds[allMessageIds.length - 1] : void 0,
					ReplyToId: effectiveReplyToId,
					MessageThreadId: effectiveReplyToId,
					Timestamp: typeof post.create_at === "number" ? post.create_at : void 0,
					WasMentioned: kind !== "direct" ? mentionDecision.effectiveWasMentioned : void 0,
					CommandAuthorized: commandAuthorized,
					CommandSource: commandAuthorized && isControlCommand ? "text" : void 0,
					OriginatingChannel: "mattermost",
					OriginatingTo: to,
					...mediaPayload
				});
				const pinnedMainDmOwner = kind === "direct" ? resolvePinnedMainDmOwnerFromAllowlist({
					dmScope: cfg.session?.dmScope,
					allowFrom: account.config.allowFrom,
					normalizeEntry: normalizeMattermostAllowEntry
				}) : null;
				const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId });
				const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n");
				logVerboseMessage(`mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`);
				const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
				const tableMode = core.channel.text.resolveMarkdownTableMode({
					cfg,
					channel: "mattermost",
					accountId: account.accountId
				});
				const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelReplyPipeline({
					cfg,
					agentId: route.agentId,
					channel: "mattermost",
					accountId: account.accountId,
					typing: {
						start: () => sendTypingIndicator(channelId, effectiveReplyToId),
						onStartError: (err) => {
							logTypingFailure({
								log: (message) => logger.debug?.(message),
								channel: "mattermost",
								target: channelId,
								error: err
							});
						}
					}
				});
				const draftPreviewEnabled = account.streamingMode !== "off";
				const draftToolProgressEnabled = shouldUpdateMattermostDraftToolProgress(account);
				const suppressDefaultToolProgressMessages = shouldSuppressMattermostDefaultToolProgressMessages(account);
				const draftStream = draftPreviewEnabled ? createMattermostDraftStream({
					client,
					channelId,
					rootId: effectiveReplyToId,
					throttleMs: 1200,
					log: logVerboseMessage,
					warn: logVerboseMessage
				}) : createDisabledMattermostDraftStream();
				let lastPartialText = "";
				const previewState = { finalizedViaPreviewPost: false };
				const resolvePreviewFinalText = (text) => {
					if (typeof text !== "string") return;
					const formatted = core.channel.text.convertMarkdownTables(text, tableMode);
					const chunkMode = core.channel.text.resolveChunkMode(cfg, "mattermost", account.accountId);
					const chunks = core.channel.text.chunkMarkdownTextWithMode(formatted, textLimit, chunkMode);
					if (!chunks.length && formatted) chunks.push(formatted);
					if (chunks.length != 1) return;
					const trimmed = chunks[0]?.trim();
					if (!trimmed) return;
					if (lastPartialText && lastPartialText.startsWith(trimmed) && trimmed.length < lastPartialText.length) return;
					return trimmed;
				};
				const updateDraftFromPartial = (text) => {
					const cleaned = text?.trim();
					if (!cleaned) return;
					if (cleaned === lastPartialText) return;
					if (lastPartialText && lastPartialText.startsWith(cleaned) && cleaned.length < lastPartialText.length) return;
					lastPartialText = cleaned;
					draftStream.update(cleaned);
				};
				const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
					...replyPipeline,
					humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
					typingCallbacks,
					deliver: async (payloadEntry, info) => {
						await deliverMattermostReplyWithDraftPreview({
							payload: payloadEntry,
							info,
							kind,
							client,
							draftStream,
							effectiveReplyToId,
							resolvePreviewFinalText,
							previewState,
							logVerboseMessage,
							deliverPayload: async (payloadToDeliver) => {
								const deliveryLog = formatMattermostFinalDeliveryOutcomeLog({
									outcome: await deliverMattermostReplyPayload({
										core,
										cfg,
										payload: payloadToDeliver,
										to,
										accountId: account.accountId,
										agentId: route.agentId,
										replyToId: resolveMattermostReplyRootId({
											kind,
											threadRootId: effectiveReplyToId,
											replyToId: payloadToDeliver.replyToId
										}),
										textLimit,
										tableMode,
										sendMessage: sendMessageMattermost
									}),
									payload: payloadToDeliver,
									to,
									accountId: account.accountId,
									agentId: route.agentId
								});
								if (deliveryLog) runtime.log?.(deliveryLog);
							}
						});
					},
					onError: (err, info) => {
						runtime.error?.(`mattermost ${info.kind} reply failed: ${String(err)}`);
					}
				});
				const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
					route,
					sessionKey: route.sessionKey
				});
				let dispatchSettledBeforeStart = false;
				try {
					await core.channel.inbound.run({
						channel: "mattermost",
						accountId: route.accountId,
						raw: post,
						adapter: {
							ingest: () => ({
								id: post.id ?? `${to}:${Date.now()}`,
								timestamp: post.create_at ?? void 0,
								rawText,
								textForAgent: ctxPayload.BodyForAgent,
								textForCommands: ctxPayload.CommandBody,
								raw: post
							}),
							resolveTurn: () => ({
								channel: "mattermost",
								accountId: route.accountId,
								routeSessionKey: route.sessionKey,
								storePath,
								ctxPayload,
								recordInboundSession: core.channel.session.recordInboundSession,
								record: {
									updateLastRoute: kind === "direct" ? {
										sessionKey: inboundLastRouteSessionKey,
										channel: "mattermost",
										to,
										accountId: route.accountId,
										mainDmOwnerPin: inboundLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner ? {
											ownerRecipient: pinnedMainDmOwner,
											senderRecipient: normalizeMattermostAllowEntry(senderId),
											onSkip: ({ ownerRecipient, senderRecipient }) => {
												logVerboseMessage(`mattermost: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
											}
										} : void 0
									} : void 0,
									onRecordError: (err) => {
										logVerboseMessage(`mattermost: failed updating session meta id=${post.id ?? "unknown"}: ${String(err)}`);
									}
								},
								history: {
									isGroup: Boolean(historyKey),
									historyKey: historyKey ?? void 0,
									historyMap: channelHistories,
									limit: historyLimit
								},
								onPreDispatchFailure: async () => {
									dispatchSettledBeforeStart = true;
									await core.channel.reply.settleReplyDispatcher({
										dispatcher,
										onSettled: () => {
											markRunComplete();
											markDispatchIdle();
										}
									});
								},
								runDispatch: () => core.channel.reply.withReplyDispatcher({
									dispatcher,
									onSettled: () => {
										markDispatchIdle();
									},
									run: () => core.channel.reply.dispatchReplyFromConfig({
										ctx: ctxPayload,
										cfg,
										dispatcher,
										replyOptions: {
											...replyOptions,
											disableBlockStreaming: true,
											...suppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
											onModelSelected,
											onPartialReply: (payloadResult) => {
												if (account.streamingMode !== "progress") updateDraftFromPartial(payloadResult.text);
											},
											onAssistantMessageStart: () => {
												lastPartialText = "";
											},
											onReasoningEnd: () => {
												lastPartialText = "";
											},
											onReasoningStream: async () => {
												if (!lastPartialText) draftStream.update("Thinking…");
											},
											onToolStart: async (payloadValue) => {
												if (!draftToolProgressEnabled) return;
												draftStream.update(buildMattermostToolStatusText({
													...payloadValue,
													config: account.config
												}));
											},
											onItemEvent: async (payloadLocal) => {
												if (!draftToolProgressEnabled) return;
												const progressText = formatChannelProgressDraftLineForEntry(account.config, {
													event: "item",
													itemId: payloadLocal.itemId,
													itemKind: payloadLocal.kind,
													title: payloadLocal.title,
													name: payloadLocal.name,
													phase: payloadLocal.phase,
													status: payloadLocal.status,
													summary: payloadLocal.summary,
													progressText: payloadLocal.progressText,
													meta: payloadLocal.meta
												});
												if (progressText) draftStream.update(progressText);
											}
										}
									})
								})
							})
						}
					});
				} finally {
					try {
						await draftStream.stop();
					} catch (err) {
						logVerboseMessage(`mattermost draft preview cleanup failed: ${String(err)}`);
					}
					if (!dispatchSettledBeforeStart) markRunComplete();
				}
			}
		}) === "duplicate") logVerboseMessage(`mattermost: drop post (dedupe account=${account.accountId} ids=${allMessageIds.length})`);
	};
	const handleReactionEvent = async (payload) => {
		const reactionData = payload.data?.reaction;
		if (!reactionData) return;
		let reaction = null;
		if (typeof reactionData === "string") try {
			reaction = JSON.parse(reactionData);
		} catch {
			return;
		}
		else if (typeof reactionData === "object") reaction = reactionData;
		if (!reaction) return;
		const userId = reaction.user_id?.trim();
		const postId = reaction.post_id?.trim();
		const emojiName = reaction.emoji_name?.trim();
		if (!userId || !postId || !emojiName) return;
		if (userId === botUserId) return;
		const action = payload.event === "reaction_removed" ? "removed" : "added";
		const senderName = normalizeOptionalString((await resolveUserInfo(userId))?.username) ?? userId;
		const channelId = resolveMattermostReactionChannelId(payload);
		if (!channelId) {
			logVerboseMessage(`mattermost: drop reaction (no channel_id in broadcast, cannot enforce policy)`);
			return;
		}
		const channelInfo = await resolveChannelInfo(channelId);
		if (!channelInfo?.type) {
			logVerboseMessage(`mattermost: drop reaction (cannot resolve channel type for ${channelId})`);
			return;
		}
		const kind = mapMattermostChannelTypeToChatType(channelInfo.type);
		const reactionAccess = await resolveMattermostMonitorInboundAccess({
			account,
			cfg,
			senderId: userId,
			senderName,
			channelId,
			kind,
			groupPolicy,
			readStoreAllowFrom: pairing.readAllowFromStore,
			allowTextCommands: false,
			hasControlCommand: false,
			eventKind: "reaction",
			mayPair: false
		});
		if (reactionAccess.ingress.decision !== "allow") {
			if (kind === "direct") logVerboseMessage(`mattermost: drop reaction (dmPolicy=${dmPolicy} sender=${userId} reason=${reactionAccess.senderAccess.reasonCode})`);
			else logVerboseMessage(`mattermost: drop reaction (groupPolicy=${groupPolicy} sender=${userId} reason=${reactionAccess.senderAccess.reasonCode} channel=${channelId})`);
			return;
		}
		const teamId = channelInfo?.team_id ?? void 0;
		const sessionKey = core.channel.routing.resolveAgentRoute({
			cfg,
			channel: "mattermost",
			accountId: account.accountId,
			teamId,
			peer: {
				kind,
				id: kind === "direct" ? userId : channelId
			}
		}).sessionKey;
		const eventText = `Mattermost reaction ${action}: :${emojiName}: by @${senderName} on post ${postId} in channel ${channelId}`;
		core.system.enqueueSystemEvent(eventText, {
			sessionKey,
			contextKey: `mattermost:reaction:${postId}:${emojiName}:${userId}:${action}`
		});
		logVerboseMessage(`mattermost reaction: ${action} :${emojiName}: by ${senderName} on ${postId}`);
	};
	const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
		cfg,
		channel: "mattermost"
	});
	const debouncer = core.channel.debounce.createInboundDebouncer({
		debounceMs: inboundDebounceMs,
		buildKey: (entry) => {
			const channelId = entry.post.channel_id ?? entry.payload.data?.channel_id ?? entry.payload.broadcast?.channel_id;
			if (!channelId) return null;
			const threadId = normalizeOptionalString(entry.post.root_id);
			const threadKey = threadId ? `thread:${threadId}` : "channel";
			return `mattermost:${account.accountId}:${channelId}:${threadKey}`;
		},
		shouldDebounce: (entry) => {
			if (entry.post.file_ids && entry.post.file_ids.length > 0) return false;
			const text = normalizeOptionalString(entry.post.message) ?? "";
			if (!text) return false;
			return !core.channel.commands.isControlCommandMessage(text, cfg);
		},
		onFlush: async (entries) => {
			const last = entries.at(-1);
			if (!last) return;
			if (entries.length === 1) {
				await handlePost(last.post, last.payload);
				return;
			}
			const combinedText = entries.map((entry) => normalizeOptionalString(entry.post.message) ?? "").filter(Boolean).join("\n");
			const mergedPost = {
				...last.post,
				message: combinedText,
				file_ids: []
			};
			const ids = entries.map((entry) => entry.post.id).filter(Boolean);
			await handlePost(mergedPost, last.payload, ids.length > 0 ? ids : void 0);
		},
		onError: (err) => {
			runtime.error?.(`mattermost debounce flush failed: ${String(err)}`);
		}
	});
	const wsUrl = buildMattermostWsUrl(baseUrl);
	let seq = 1;
	const connectOnce = createMattermostConnectOnce({
		wsUrl,
		botToken,
		abortSignal: opts.abortSignal,
		statusSink: opts.statusSink,
		runtime,
		webSocketFactory: opts.webSocketFactory,
		nextSeq: () => seq++,
		getBotUpdateAt: async () => {
			return (await fetchMattermostMe(client)).update_at ?? 0;
		},
		onPosted: async (post, payload) => {
			await debouncer.enqueue({
				post,
				payload
			});
		},
		onReaction: async (payload) => {
			await handleReactionEvent(payload);
		}
	});
	let slashShutdownCleanup = null;
	if (slashEnabled) {
		const runAbortCleanup = () => {
			if (slashShutdownCleanup) return;
			const commands = getSlashCommandState(account.accountId)?.registeredCommands ?? [];
			deactivateSlashCommands(account.accountId);
			slashShutdownCleanup = cleanupSlashCommands({
				client,
				commands,
				log: (msg) => runtime.log?.(msg)
			}).catch((err) => {
				runtime.error?.(`mattermost: slash cleanup failed: ${String(err)}`);
			});
		};
		if (opts.abortSignal?.aborted) runAbortCleanup();
		else opts.abortSignal?.addEventListener("abort", runAbortCleanup, { once: true });
	}
	try {
		await runWithReconnect(connectOnce, {
			abortSignal: opts.abortSignal,
			jitterRatio: .2,
			onError: (err) => {
				runtime.error?.(`mattermost connection failed: ${String(err)}`);
				opts.statusSink?.({
					lastError: String(err),
					connected: false
				});
			},
			onReconnect: (delayMs) => {
				runtime.log?.(`mattermost reconnecting in ${Math.round(delayMs / 1e3)}s`);
			}
		});
	} finally {
		unregisterInteractions?.();
	}
	const slashShutdownCleanupPromise = slashShutdownCleanup;
	if (slashShutdownCleanupPromise) await Promise.resolve(slashShutdownCleanupPromise);
}
//#endregion
//#region extensions/mattermost/src/mattermost/probe.ts
async function probeMattermost(baseUrl, botToken, timeoutMs = 2500, allowPrivateNetwork = false) {
	const normalized = normalizeMattermostBaseUrl(baseUrl);
	if (!normalized) return {
		ok: false,
		error: "baseUrl missing"
	};
	const url = `${normalized}/api/v4/users/me`;
	const start = Date.now();
	const resolvedTimeoutMs = timeoutMs > 0 ? resolveTimerTimeoutMs(timeoutMs, 2500) : 0;
	const controller = resolvedTimeoutMs > 0 ? new AbortController() : void 0;
	let timer = null;
	if (controller) timer = setTimeout(() => controller.abort(), resolvedTimeoutMs);
	try {
		const { response: res, release } = await fetchWithSsrFGuard({
			url,
			init: {
				headers: { Authorization: `Bearer ${botToken}` },
				signal: controller?.signal
			},
			auditContext: "mattermost-probe",
			policy: ssrfPolicyFromPrivateNetworkOptIn(allowPrivateNetwork)
		});
		try {
			const elapsedMs = Date.now() - start;
			if (!res.ok) {
				const detail = await readMattermostError(res);
				return {
					ok: false,
					status: res.status,
					error: detail || res.statusText,
					elapsedMs
				};
			}
			const bot = await res.json();
			return {
				ok: true,
				status: res.status,
				elapsedMs,
				bot
			};
		} finally {
			await release();
		}
	} catch (err) {
		return {
			ok: false,
			status: null,
			error: formatErrorMessage(err),
			elapsedMs: Date.now() - start
		};
	} finally {
		if (timer) clearTimeout(timer);
	}
}
//#endregion
//#region extensions/mattermost/src/mattermost/reactions.ts
const BOT_USER_CACHE_TTL_MS = 10 * 6e4;
const botUserIdCache = /* @__PURE__ */ new Map();
async function resolveBotUserId(client, cacheKey) {
	const rawNow = Date.now();
	const now = asDateTimestampMs(rawNow);
	const cached = botUserIdCache.get(cacheKey);
	if (cached) {
		if (now !== void 0 && cached.expiresAt > now) return cached.userId;
		botUserIdCache.delete(cacheKey);
	}
	const userId = (await fetchMattermostMe(client))?.id?.trim();
	if (!userId) return null;
	const expiresAt = resolveExpiresAtMsFromDurationMs(BOT_USER_CACHE_TTL_MS, { nowMs: rawNow });
	if (expiresAt !== void 0) botUserIdCache.set(cacheKey, {
		userId,
		expiresAt
	});
	return userId;
}
async function addMattermostReaction(params) {
	return runMattermostReaction(params, {
		action: "add",
		mutation: createReaction
	});
}
async function removeMattermostReaction(params) {
	return runMattermostReaction(params, {
		action: "remove",
		mutation: deleteReaction
	});
}
async function runMattermostReaction(params, options) {
	const resolved = resolveMattermostAccount({
		cfg: params.cfg,
		accountId: params.accountId
	});
	const baseUrl = resolved.baseUrl?.trim();
	const botToken = resolved.botToken?.trim();
	if (!baseUrl || !botToken) return {
		ok: false,
		error: "Mattermost botToken/baseUrl missing."
	};
	const client = createMattermostClient({
		baseUrl,
		botToken,
		fetchImpl: params.fetchImpl,
		allowPrivateNetwork: isPrivateNetworkOptInEnabled(resolved.config)
	});
	const userId = await resolveBotUserId(client, `${baseUrl}:${botToken}`);
	if (!userId) return {
		ok: false,
		error: "Mattermost reactions failed: could not resolve bot user id."
	};
	try {
		await options.mutation(client, {
			userId,
			postId: params.postId,
			emojiName: params.emojiName
		});
	} catch (err) {
		return {
			ok: false,
			error: `Mattermost ${options.action} reaction failed: ${String(err)}`
		};
	}
	return { ok: true };
}
async function createReaction(client, params) {
	await client.request("/reactions", {
		method: "POST",
		body: JSON.stringify({
			user_id: params.userId,
			post_id: params.postId,
			emoji_name: params.emojiName
		})
	});
}
async function deleteReaction(client, params) {
	const emoji = encodeURIComponent(params.emojiName);
	await client.request(`/users/${params.userId}/posts/${params.postId}/reactions/${emoji}`, { method: "DELETE" });
}
//#endregion
export { addMattermostReaction, listMattermostDirectoryGroups, listMattermostDirectoryPeers, monitorMattermostProvider, probeMattermost, removeMattermostReaction, resolveMattermostOpaqueTarget, sendMessageMattermost };