UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

3,149 lines 129 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { C as resolveExpiresAtMsFromDurationMs, P as timestampMsToIsoString, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { _ as uniqueStrings, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { c as isRecord, y as truncateUtf16Safe } from "./utils-CCC-BEJH.js";
import { t as createNonExitingRuntime } from "./runtime-B4lgFmsS.js";
import { a as shouldLogVerbose, r as logVerbose, s as warn, t as danger } from "./globals-GTrXU4s9.js";
import { t as sanitizeTerminalText } from "./safe-text-BkQYdJi1.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { c as kindFromMime } from "./mime-C8mVE2Bw.js";
import { u as resolveStorePath } from "./paths-NEwU8m3X.js";
import { o as readSessionUpdatedAt, r as getSessionEntry } from "./store-Qsgtu-0y.js";
import { u as saveMediaBuffer } from "./store-C9M_0A1p.js";
import { t as hasControlCommand } from "./command-detection-iKcBJ8it.js";
import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js";
import { g as isInsideCode, h as findCodeRegions } from "./assistant-visible-text-QdHceFYu.js";
import { n as resolveSendPolicy } from "./send-policy-CfC5ATxp.js";
import { i as deliverTextOrMediaReply, m as resolveSendableOutboundReplyParts } from "./reply-payload-D-VMfpYI.js";
import { a as settleReplyDispatcher, t as dispatchInboundMessage } from "./dispatch-DO0Fpkbp.js";
import { n as resolveChannelGroupRequireMention, t as resolveChannelGroupPolicy } from "./group-policy-CShy7eWv.js";
import { a as resolveInboundLastRouteSessionKey, i as resolveAgentRoute } from "./resolve-route-xcIS8NFL.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, i as resolveOpenProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js";
import { l as resolvePinnedMainDmOwnerFromAllowlist } from "./dm-policy-shared-BjnswXJt.js";
import { t as isInboundPathAllowed } from "./inbound-path-policy-CYWsER5a.js";
import { n as loadWebMedia } from "./web-media-D8G2d_q7.js";
import { i as resolveHumanDelayConfig } from "./identity-Qcvvo6SY.js";
import { S as resolveChannelStreamingBlockEnabled } from "./streaming-_TVeMLGC.js";
import { c as resolveTextChunkLimit, o as chunkTextWithMode, s as resolveChunkMode } from "./chunk-Cpb8JO1x.js";
import "./media-runtime-fgpmX6eL.js";
import "./number-runtime-DBLVDypr.js";
import "./runtime-config-snapshot-CDoFLjqb.js";
import "./runtime-env-D_2q8-VK.js";
import "./security-runtime-CQm7DD1u.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as convertMarkdownTables } from "./tables-BgtXxld3.js";
import "./text-chunking-D45TeeEs.js";
import "./text-utility-runtime-BZ3jGr-e.js";
import { n as buildMentionRegexes, r as matchesMentionPatterns } from "./mentions-Be0DZkGs.js";
import { r as createReplyDispatcherWithTyping } from "./reply-dispatcher.types-CKL81tAi.js";
import "./routing-CQ63ICPX.js";
import { a as resolveEnvelopeFormatOptions, i as formatInboundFromLabel, r as formatInboundEnvelope } from "./envelope-IMcsY0tl.js";
import { E as toInboundMediaFacts, b as filterChannelInboundQuoteContext, y as buildChannelInboundEventContext } from "./kernel-BdraY_qZ.js";
import { n as resolveInboundMentionDecision } from "./mention-gating-3P8aSD7o.js";
import { t as recordInboundSession } from "./session-Do0yFgU8.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 resolveMarkdownTableMode } from "./markdown-tables-LfSv7QSx.js";
import { a as readChannelAllowFromStore, d as upsertChannelPairingRequest } from "./pairing-store-CxANz_ON.js";
import "./channel-policy-CIW58SA5.js";
import "./reply-history-Y9HXucO9.js";
import { t as CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "./approval-handler-adapter-runtime-DLWtIqHq.js";
import "./approval-handler-runtime-DF66dgbE.js";
import { r as registerChannelRuntimeContext } from "./channel-runtime-context-Bx08uB94.js";
import { t as requireRuntimeConfig } from "./plugin-config-runtime-CpE6DYgJ.js";
import { t as resolveChannelContextVisibilityMode } from "./context-visibility-D4NM8ac1.js";
import "./reply-runtime--Kbd63wS.js";
import { f as runChannelInboundEvent } from "./inbound-reply-dispatch-BpzI7GIp.js";
import { t as deliverInboundReplyWithMessageSendContext } from "./channel-outbound-B3_Zy-kG.js";
import { n as logInboundDrop, r as logTypingFailure } from "./logging-gUWPKC5g.js";
import { t as waitForTransportReady } from "./transport-ready-CPbvF2Ea.js";
import "./web-media-Nj3kZBh5.js";
import "./system-event-runtime-D80J5tzu.js";
import "./transport-ready-runtime-DNcaz8UY.js";
import { r as normalizeScpRemoteHost } from "./scp-host-D4uXCtRP.js";
import "./media-store-B6kmSr5u.js";
import { n as resolveConfiguredBindingRoute, r as resolveRuntimeConversationBindingRoute } from "./binding-routing-C-pxzG2P.js";
import "./conversation-runtime-VjM1-D7a.js";
import "./agent-runtime-CvpQRNVf.js";
import "./markdown-table-runtime-j_C4iTF_.js";
import "./command-auth-native-D4ML0UKp.js";
import { c as createChannelInboundDebouncer, l as shouldDebounceTextInbound } from "./channel-inbound-bl7VmTdr.js";
import "./channel-feedback-Bi1IYmE_.js";
import { n as createChannelIngressResolver, o as defineStableChannelIngressIdentity } from "./message-access-mwxdTUGC.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { t as createChannelPairingChallengeIssuer } from "./channel-pairing-Bk6_JhTm.js";
import "./session-store-runtime-rvUx2Fil.js";
import { t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js";
import { a as resolveIMessageAccount } from "./accounts-DzoymhDS.js";
import { t as createIMessageRpcClient } from "./client-RDkCLFWE.js";
import { n as imessageRpcSupportsMethod, t as getCachedIMessagePrivateApiStatus } from "./private-api-status-DBgq8x0h.js";
import { c as parseIMessageTarget, o as normalizeIMessageHandle, r as isAllowedIMessageReplyContextSender, s as parseIMessageAllowTarget, t as formatIMessageChatTarget } from "./targets-BhNdlUSi.js";
import { o as isKnownFromMeIMessageMessageId, s as rememberIMessageReplyCache, u as getIMessageRuntime } from "./monitor-reply-cache-DUH5oJzE.js";
import { n as resolveIMessageInboundConversationId, o as probeIMessage, t as sanitizeOutboundText } from "./sanitize-outbound-B_Wx0jBt.js";
import { a as extractIMessageApprovalPromptBinding, c as maybeResolveIMessageApprovalReaction, f as resolveIMessageReactionContext, l as registerIMessageApprovalReactionTarget, o as handleIMessageApprovalReaction, s as listPendingIMessageApprovalReactionPollTargets } from "./normalize-BfDcnrra.js";
import { n as resolveIMessageAttachmentRoots, r as resolveIMessageRemoteAttachmentRoots } from "./media-contract-BEafhpgp.js";
import { a as resolveCatchupConfig, i as performIMessageCatchup, n as advanceIMessageCatchupCursor } from "./catchup-BUorPTSs.js";
import { i as hasPersistedIMessageEcho } from "./persisted-echo-cache-C0OpKutp.js";
import { t as sendMessageIMessage } from "./send-CjeAdmmw.js";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import { createHash } from "node:crypto";
//#region extensions/imessage/src/approval-reaction-poller.ts
const RECENT_CHAT_LIMIT = 50;
const PER_CHAT_HISTORY_LIMIT = 30;
const OBSERVED_APPROVAL_PROMPT_TARGET_TTL_MS = 300 * 1e3;
const accountIdsWithCompletedNoTargetDiscovery = /* @__PURE__ */ new Set();
function normalizeChatId(value) {
	return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
}
function listTargetChatIds(targets) {
	const chatIds = /* @__PURE__ */ new Set();
	for (const target of targets) {
		const chatId = normalizeChatId(target.conversation.chatId);
		if (chatId !== null) chatIds.add(chatId);
	}
	return [...chatIds];
}
function hasUnscopedTarget(targets) {
	return targets.some((target) => normalizeChatId(target.conversation.chatId) === null);
}
function uniqueChatIds(chatIds) {
	return [...new Set(chatIds)];
}
function normalizeMessageGuid(value) {
	return value.trim().replace(/^p:\d+\//iu, "");
}
function enumerateMessageGuidCandidates(value) {
	const trimmed = value.trim();
	if (!trimmed) return [];
	return [trimmed, normalizeMessageGuid(trimmed)].filter((candidate, index, candidates) => candidate.length > 0 && candidates.indexOf(candidate) === index);
}
function buildPendingTargetsByMessageId(targets) {
	const pendingByMessageId = /* @__PURE__ */ new Map();
	for (const target of targets) for (const candidate of enumerateMessageGuidCandidates(target.messageId)) pendingByMessageId.set(candidate, target);
	return pendingByMessageId;
}
async function listRecentChatIds(client) {
	return ((await client.request("chats.list", { limit: RECENT_CHAT_LIMIT }, { timeoutMs: 1e4 })).chats ?? []).map((chat) => normalizeChatId(chat.id)).filter((chatId) => chatId !== null);
}
async function fetchRecentHistory(params) {
	return ((await params.client.request("messages.history", {
		chat_id: params.chatId,
		limit: PER_CHAT_HISTORY_LIMIT
	}, { timeoutMs: 1e4 })).messages ?? []).filter((message) => Boolean(message && typeof message === "object"));
}
function buildReactionPayload(params) {
	const emoji = params.reaction.emoji?.trim();
	const sender = params.reaction.sender?.trim();
	const targetGuid = params.targetMessage.guid?.trim();
	if (!emoji || !sender || !targetGuid) return null;
	const reactionId = normalizeChatId(params.reaction.id);
	return {
		...reactionId !== null ? { id: reactionId } : {},
		guid: `reaction:${targetGuid}:${sender}:${emoji}:${params.reaction.created_at ?? ""}`,
		chat_id: params.targetMessage.chat_id,
		chat_guid: params.targetMessage.chat_guid,
		chat_identifier: params.targetMessage.chat_identifier,
		chat_name: params.targetMessage.chat_name,
		participants: params.targetMessage.participants,
		is_group: params.targetMessage.is_group,
		sender,
		destination_caller_id: params.targetMessage.destination_caller_id,
		is_from_me: params.reaction.is_from_me,
		text: `${params.reaction.type ?? "reaction"} "${params.targetMessage.text ?? ""}"`,
		created_at: params.reaction.created_at,
		is_reaction: true,
		is_tapback: true,
		associated_message_guid: targetGuid,
		associated_message_type: 2e3,
		reaction_type: params.reaction.type ?? void 0,
		reaction_emoji: emoji,
		is_reaction_add: true,
		reacted_to_guid: targetGuid
	};
}
function buildConversationKeyFromMessage(message) {
	return {
		...message.chat_guid?.trim() ? { chatGuid: message.chat_guid.trim() } : {},
		...message.chat_identifier?.trim() ? { chatIdentifier: message.chat_identifier.trim() } : {},
		...normalizeChatId(message.chat_id) !== null ? { chatId: message.chat_id } : {}
	};
}
function bindObservedConversation(params) {
	const nowMs = asDateTimestampMs(Date.now());
	const expiresAtMs = asDateTimestampMs(params.target.expiresAtMs);
	if (nowMs === void 0 || expiresAtMs === void 0 || expiresAtMs <= nowMs) return;
	const ttlMs = expiresAtMs - nowMs;
	const conversation = buildConversationKeyFromMessage(params.message);
	const messageIds = new Set([...enumerateMessageGuidCandidates(params.target.messageId), ...enumerateMessageGuidCandidates(params.message.guid ?? "")]);
	for (const messageId of messageIds) registerIMessageApprovalReactionTarget({
		accountId: params.target.accountId,
		conversation,
		messageId,
		approvalId: params.target.approvalId,
		allowedDecisions: params.target.allowedDecisions,
		ttlMs
	});
}
function bindObservedApprovalPrompt(params) {
	if (params.message.is_from_me !== true) return null;
	const messageId = params.message.guid?.trim();
	if (!messageId) return null;
	const binding = extractIMessageApprovalPromptBinding(params.message.text ?? "");
	if (!binding) return null;
	const conversation = buildConversationKeyFromMessage(params.message);
	const expiresAtMs = resolveExpiresAtMsFromDurationMs(OBSERVED_APPROVAL_PROMPT_TARGET_TTL_MS);
	if (expiresAtMs === void 0) return null;
	const target = {
		accountId: params.accountId,
		conversation,
		messageId,
		approvalId: binding.approvalId,
		allowedDecisions: binding.allowedDecisions,
		expiresAtMs
	};
	bindObservedConversation({
		target,
		message: params.message
	});
	return target;
}
async function pollPendingIMessageApprovalReactions(params) {
	const targets = listPendingIMessageApprovalReactionPollTargets({ accountId: params.accountId });
	const shouldAttemptNoTargetDiscovery = targets.length === 0 && params.allowRecentChatDiscovery === true && !accountIdsWithCompletedNoTargetDiscovery.has(params.accountId);
	if (targets.length === 0 && !shouldAttemptNoTargetDiscovery) return;
	const pendingByMessageId = buildPendingTargetsByMessageId(targets);
	const explicitChatIds = listTargetChatIds(targets);
	const chatIds = params.allowRecentChatDiscovery === true && (targets.length === 0 || hasUnscopedTarget(targets)) ? uniqueChatIds([...explicitChatIds, ...await listRecentChatIds(params.client)]) : explicitChatIds;
	if (chatIds.length === 0) {
		if (shouldAttemptNoTargetDiscovery) accountIdsWithCompletedNoTargetDiscovery.add(params.accountId);
		return;
	}
	let hadHistoryFetchError = false;
	for (const chatId of chatIds) {
		let messages;
		try {
			messages = await fetchRecentHistory({
				client: params.client,
				chatId
			});
		} catch (err) {
			hadHistoryFetchError = true;
			params.logVerboseMessage?.(`imessage: approval reaction poll skipped chat_id=${chatId}: ${String(err)}`);
			continue;
		}
		for (const message of messages) {
			const targetGuid = message.guid?.trim();
			if (!targetGuid) continue;
			const target = pendingByMessageId.get(targetGuid) ?? pendingByMessageId.get(normalizeMessageGuid(targetGuid)) ?? bindObservedApprovalPrompt({
				accountId: params.accountId,
				message
			});
			if (!target) continue;
			bindObservedConversation({
				target,
				message
			});
			for (const reaction of message.reactions ?? []) {
				const reactionPayload = buildReactionPayload({
					targetMessage: message,
					reaction
				});
				if (!reactionPayload) continue;
				const handled = await handleIMessageApprovalReaction({
					cfg: params.cfg,
					accountId: params.accountId,
					message: reactionPayload,
					bodyText: reactionPayload.text ?? "",
					logVerboseMessage: params.logVerboseMessage
				});
				if (handled.stopPolling) {
					if (shouldAttemptNoTargetDiscovery && handled.stopPollingReason !== "resolver-error") break;
					return;
				}
			}
		}
	}
	if (shouldAttemptNoTargetDiscovery && !hadHistoryFetchError) accountIdsWithCompletedNoTargetDiscovery.add(params.accountId);
}
//#endregion
//#region extensions/imessage/src/chat.ts
function buildChatTargetParams(to, opts) {
	const cfg = requireRuntimeConfig(opts.cfg, "iMessage chat action");
	const account = opts.account ?? resolveIMessageAccount({
		cfg,
		accountId: opts.accountId
	});
	const target = parseIMessageTarget(opts.chatId ? formatIMessageChatTarget(opts.chatId) : to);
	const params = {};
	if (target.kind === "chat_id") params.chat_id = target.chatId;
	else if (target.kind === "chat_guid") params.chat_guid = target.chatGuid;
	else if (target.kind === "chat_identifier") params.chat_identifier = target.chatIdentifier;
	else params.to = target.to;
	return {
		params,
		service: opts.service ?? (target.kind === "handle" ? target.service : void 0) ?? account.config.service,
		region: opts.region?.trim() || account.config.region?.trim() || "US",
		account
	};
}
async function runChatAction(method, params, opts) {
	const cfg = requireRuntimeConfig(opts.cfg, "iMessage chat action");
	const account = opts.account ?? resolveIMessageAccount({
		cfg,
		accountId: opts.accountId
	});
	const cliPath = opts.cliPath?.trim() || account.config.cliPath?.trim() || "imsg";
	const dbPath = opts.dbPath?.trim() || account.config.dbPath?.trim();
	const client = opts.client ?? await createIMessageRpcClient({
		cliPath,
		dbPath
	});
	const shouldClose = !opts.client;
	try {
		return await client.request(method, params, { timeoutMs: opts.timeoutMs });
	} finally {
		if (shouldClose) await client.stop();
	}
}
async function sendIMessageTyping(to, isTyping, opts) {
	const { params, service } = buildChatTargetParams(to, opts);
	params.typing = isTyping;
	if (service) params.service = service;
	await runChatAction("typing", params, opts);
}
async function markIMessageChatRead(to, opts) {
	const { params } = buildChatTargetParams(to, opts);
	await runChatAction("read", params, opts);
}
//#endregion
//#region extensions/imessage/src/monitor/abort-handler.ts
function attachIMessageMonitorAbortHandler(params) {
	const abort = params.abortSignal;
	if (!abort) return () => {};
	const onAbort = () => {
		const subscriptionId = params.getSubscriptionId();
		if (subscriptionId) params.client.request("watch.unsubscribe", { subscription: subscriptionId }).catch(() => {});
		params.client.stop().catch(() => {});
	};
	abort.addEventListener("abort", onAbort, { once: true });
	return () => abort.removeEventListener("abort", onAbort);
}
//#endregion
//#region extensions/imessage/src/monitor/strip-imsg-length-prefixed-text.ts
const utf8Decoder = new TextDecoder();
function readVarint(buf, start) {
	let offset = start;
	let value = 0;
	let shift = 0;
	while (offset < buf.length && shift <= 28) {
		const byte = buf[offset];
		offset += 1;
		value |= (byte & 127) << shift;
		if ((byte & 128) === 0) return {
			nextOffset: offset,
			value
		};
		shift += 7;
	}
	return null;
}
function tryStripImessageLengthPrefixedUtf8Buffer(buf) {
	const key = readVarint(buf, 0);
	if (!key || key.nextOffset >= buf.length) return null;
	if (key.value !== 10) return null;
	const length = readVarint(buf, key.nextOffset);
	if (!length || length.value === 0) return null;
	if (length.nextOffset + length.value !== buf.length) return null;
	return buf.subarray(length.nextOffset, buf.length);
}
function stripImessageLengthPrefixedUtf8Text(text) {
	if (!text) return text;
	const stripped = tryStripImessageLengthPrefixedUtf8Buffer(Buffer.from(text, "utf8"));
	if (!stripped) return text;
	const inner = utf8Decoder.decode(stripped);
	return inner.length > 0 ? inner : text;
}
//#endregion
//#region extensions/imessage/src/monitor/parse-notification.ts
function isOptionalString(value) {
	return value === void 0 || value === null || typeof value === "string";
}
function isOptionalStringOrNumber(value) {
	return value === void 0 || value === null || typeof value === "string" || typeof value === "number";
}
function isOptionalNumber(value) {
	return value === void 0 || value === null || typeof value === "number";
}
function isOptionalBoolean(value) {
	return value === void 0 || value === null || typeof value === "boolean";
}
function isOptionalStringArray(value) {
	return value === void 0 || value === null || Array.isArray(value) && value.every((entry) => typeof entry === "string");
}
function isOptionalAttachments(value) {
	if (value === void 0 || value === null) return true;
	if (!Array.isArray(value)) return false;
	return value.every((attachment) => {
		if (!isRecord(attachment)) return false;
		return isOptionalString(attachment.original_path) && isOptionalString(attachment.mime_type) && isOptionalBoolean(attachment.missing) && isOptionalString(attachment.transfer_name) && isOptionalString(attachment.uti);
	});
}
function parseIMessageNotification(raw) {
	if (!isRecord(raw)) return null;
	const maybeMessage = raw.message;
	if (!isRecord(maybeMessage)) return null;
	const message = maybeMessage;
	if (!isOptionalNumber(message.id) || !isOptionalString(message.guid) || !isOptionalNumber(message.chat_id) || !isOptionalString(message.sender) || !isOptionalString(message.destination_caller_id) || !isOptionalString(message.balloon_bundle_id) || !isOptionalBoolean(message.is_from_me) || !isOptionalString(message.text) || !isOptionalStringOrNumber(message.reply_to_id) || !isOptionalString(message.reply_to_text) || !isOptionalString(message.reply_to_sender) || !isOptionalString(message.created_at) || !isOptionalBoolean(message.is_reaction) || !isOptionalBoolean(message.is_tapback) || !isOptionalString(message.associated_message_guid) || !isOptionalNumber(message.associated_message_type) || !isOptionalString(message.reaction_type) || !isOptionalString(message.reaction_emoji) || !isOptionalBoolean(message.is_reaction_add) || !isOptionalString(message.reacted_to_guid) || !isOptionalAttachments(message.attachments) || !isOptionalString(message.chat_identifier) || !isOptionalString(message.chat_guid) || !isOptionalString(message.chat_name) || !isOptionalStringArray(message.participants) || !isOptionalBoolean(message.is_group)) return null;
	return {
		...message,
		text: typeof message.text === "string" ? stripImessageLengthPrefixedUtf8Text(message.text) : message.text,
		reply_to_text: typeof message.reply_to_text === "string" ? stripImessageLengthPrefixedUtf8Text(message.reply_to_text) : message.reply_to_text
	};
}
//#endregion
//#region extensions/imessage/src/monitor/catchup-bridge.ts
const PER_CHAT_HISTORY_LIMIT_CAP = 500;
const CATCHUP_CHATS_LIST_LIMIT = 200;
const CATCHUP_RPC_TIMEOUT_MS = 3e4;
/**
* Wire `performIMessageCatchup` against the live `imsg` JSON-RPC client.
*
* Catchup recovers messages that landed in `chat.db` while the gateway was
* offline (crash, restart, mac sleep) by:
*   1. listing recently-active chats via `chats.list`,
*   2. fetching per-chat history since the cursor via `messages.history`,
*   3. sorting cross-chat by `rowid`, capping at `perRunLimit`,
*   4. replaying each row through the same `dispatchPayload` handler used
*      by the live notification loop, so existing dedupe / coalesce / echo
*      / read-receipt behavior covers replayed rows for free.
*
* Runs at most once per `monitorIMessageProvider` invocation, between
* `watch.subscribe` and the live dispatch loop. Anything that arrives during
* catchup itself flows through live dispatch; the existing inbound-dedupe
* cache absorbs any overlap.
*/
async function runIMessageCatchup(params) {
	const { client, accountId, config, includeAttachments, dispatchPayload, runtime } = params;
	const log = (msg) => runtime?.log?.(msg);
	const warnLog = (msg) => runtime?.log?.(warn(msg));
	const payloadByGuid = /* @__PURE__ */ new Map();
	const fetchFn = async ({ sinceMs, sinceRowid, limit }) => {
		const sinceISO = timestampMsToIsoString(sinceMs);
		if (!sinceISO) {
			warnLog(`imessage catchup: invalid since timestamp ${sinceMs}`);
			return {
				resolved: false,
				rows: []
			};
		}
		let chatsResult;
		try {
			chatsResult = await client.request("chats.list", { limit: CATCHUP_CHATS_LIST_LIMIT }, { timeoutMs: CATCHUP_RPC_TIMEOUT_MS });
		} catch (err) {
			warnLog(`imessage catchup: chats.list failed: ${String(err)}`);
			return {
				resolved: false,
				rows: []
			};
		}
		const chats = chatsResult?.chats ?? [];
		const collected = [];
		const perChatLimit = Math.min(limit, PER_CHAT_HISTORY_LIMIT_CAP);
		let historyFetchFailed = false;
		let rawWatermarkRowid = -Infinity;
		let rawWatermarkMs = -Infinity;
		for (const chat of chats) {
			const chatId = typeof chat.id === "number" && Number.isFinite(chat.id) ? chat.id : null;
			if (chatId === null) continue;
			const lastMs = typeof chat.last_message_at === "string" ? Date.parse(chat.last_message_at) : NaN;
			if (Number.isFinite(lastMs) && lastMs < sinceMs) continue;
			let historyResult;
			try {
				historyResult = await client.request("messages.history", {
					chat_id: chatId,
					limit: perChatLimit,
					start: sinceISO,
					attachments: includeAttachments
				}, { timeoutMs: CATCHUP_RPC_TIMEOUT_MS });
			} catch (err) {
				historyFetchFailed = true;
				warnLog(`imessage catchup: messages.history failed for chat_id=${chatId}: ${String(err)}`);
				continue;
			}
			const messages = Array.isArray(historyResult?.messages) ? historyResult.messages : [];
			for (const raw of messages) {
				const rawRecord = raw && typeof raw === "object" ? raw : null;
				const rawRowid = rawRecord && typeof rawRecord.id === "number" && Number.isFinite(rawRecord.id) ? rawRecord.id : null;
				const rawCreatedAt = rawRecord && typeof rawRecord.created_at === "string" ? rawRecord.created_at : null;
				const rawDateMs = rawCreatedAt ? Date.parse(rawCreatedAt) : NaN;
				if (rawRowid !== null) rawWatermarkRowid = Math.max(rawWatermarkRowid, rawRowid);
				if (Number.isFinite(rawDateMs)) rawWatermarkMs = Math.max(rawWatermarkMs, rawDateMs);
				const payload = parseIMessageNotification({ message: raw });
				if (!payload) continue;
				const guid = payload.guid?.trim();
				const rowid = typeof payload.id === "number" ? payload.id : null;
				const dateMs = typeof payload.created_at === "string" ? Date.parse(payload.created_at) : NaN;
				if (!guid || rowid === null || !Number.isFinite(rowid) || !Number.isFinite(dateMs)) continue;
				if (rowid <= sinceRowid) continue;
				collected.push({
					guid,
					rowid,
					date: dateMs,
					isFromMe: payload.is_from_me === true
				});
				payloadByGuid.set(guid, payload);
			}
		}
		const sorted = collected.toSorted((a, b) => a.rowid - b.rowid);
		const capped = sorted.slice(0, limit);
		const isCapTruncated = capped.length < sorted.length;
		if (isCapTruncated) {
			warnLog(`imessage catchup: fetched ${sorted.length} rows across chats, capped to perRunLimit=${limit} (oldest first); next startup picks up the rest`);
			const keep = new Set(capped.map((row) => row.guid));
			for (const guid of payloadByGuid.keys()) if (!keep.has(guid)) payloadByGuid.delete(guid);
		}
		let effectiveWatermarkRowid = rawWatermarkRowid;
		let effectiveWatermarkMs = rawWatermarkMs;
		if (isCapTruncated && capped.length > 0) {
			const last = capped.at(-1);
			if (last) {
				effectiveWatermarkRowid = Math.min(effectiveWatermarkRowid, last.rowid);
				effectiveWatermarkMs = Math.min(effectiveWatermarkMs, last.date);
			}
		} else if (isCapTruncated && capped.length === 0) {
			effectiveWatermarkRowid = NaN;
			effectiveWatermarkMs = NaN;
		}
		return {
			resolved: true,
			rows: capped,
			fullyCaughtUp: !historyFetchFailed && !isCapTruncated,
			...Number.isFinite(effectiveWatermarkRowid) ? { highWatermarkRowid: effectiveWatermarkRowid } : {},
			...Number.isFinite(effectiveWatermarkMs) ? { highWatermarkMs: effectiveWatermarkMs } : {}
		};
	};
	const dispatchFn = async (row) => {
		const payload = payloadByGuid.get(row.guid);
		if (!payload) {
			warnLog(`imessage catchup: missing payload for guid=${row.guid}, skipping`);
			return { ok: false };
		}
		try {
			await dispatchPayload(payload);
			return { ok: true };
		} catch (err) {
			warnLog(`imessage catchup: dispatch threw for guid=${row.guid}: ${String(err)}`);
			return { ok: false };
		}
	};
	return await performIMessageCatchup({
		accountId,
		config,
		fetch: fetchFn,
		dispatch: dispatchFn,
		observeSkippedFromMe: async (row) => {
			const payload = payloadByGuid.get(row.guid);
			if (!payload) {
				warnLog(`imessage catchup: missing skipped from-me payload for guid=${row.guid}`);
				return;
			}
			await params.observeSkippedFromMePayload?.(payload);
		},
		log,
		warn: warnLog,
		...params.now ? { now: params.now() } : {}
	});
}
//#endregion
//#region extensions/imessage/src/monitor/coalesce.ts
/**
* Bounds on the merged output when multiple inbound iMessage payloads are
* folded into one agent turn. Caps each merge so a sender who
* rapid-fires DMs inside the debounce window cannot amplify the downstream
* prompt past a safe ceiling. Every source GUID still surfaces via
* `coalescedMessageGuids` so a future replay path can recognize duplicates.
*/
const MAX_COALESCED_TEXT_CHARS = 4e3;
const IMESSAGE_URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
function hasIMessageUrlBalloonBundleID(payload) {
	return payload.balloon_bundle_id === IMESSAGE_URL_BALLOON_BUNDLE_ID;
}
function hasIMessageBalloonMetadata(payload) {
	return typeof payload.balloon_bundle_id === "string" && payload.balloon_bundle_id.length > 0;
}
/**
* Decide whether a debounced same-sender bucket should merge into one turn.
*
* `buildEmitsBalloonMetadata` is a session-level capability latch: once any
* inbound row from this imsg build has carried balloon metadata, absence of a
* URL marker is meaningful (the row genuinely is not a URL split-send), so we
* can keep ordinary buffered DMs separate. It must be session-scoped, not
* per-bucket: imsg omits `balloon_bundle_id` on the wire for non-balloon rows,
* so a bucket of plain text rows looks identical on old and new builds.
*/
function shouldCombineIMessagePayloadBucket(payloads, buildEmitsBalloonMetadata) {
	if (payloads.some(hasIMessageUrlBalloonBundleID)) return true;
	if (buildEmitsBalloonMetadata || payloads.some(hasIMessageBalloonMetadata)) return false;
	return true;
}
/**
* Combine consecutive same-sender iMessage payloads into a single payload for
* downstream dispatch. Used when the debouncer flushes a bucket containing
* more than one event — e.g. Apple's split-send for `Dump https://example.com`
* arriving as two separate `chat.db` rows ~0.8-2.0 s apart.
*
* The first payload anchors the merged shape (preserving its GUID for reply
* threading). Text is concatenated with deduplication, attachments are merged
* (capped), and the latest `created_at` wins so downstream sees the most
* recent activity timestamp.
*/
function combineIMessagePayloads(payloads) {
	if (payloads.length === 0) throw new Error("combineIMessagePayloads: cannot combine empty payloads");
	if (payloads.length === 1) return payloads[0];
	const first = payloads[0];
	const last = payloads[payloads.length - 1];
	const boundedPayloads = payloads.length > 10 ? [...payloads.slice(0, 9), last] : payloads;
	const seenTexts = /* @__PURE__ */ new Set();
	const textParts = [];
	for (const payload of boundedPayloads) {
		const text = (payload.text ?? "").trim();
		if (!text) continue;
		const normalized = text.toLowerCase();
		if (seenTexts.has(normalized)) continue;
		seenTexts.add(normalized);
		textParts.push(text);
	}
	let combinedText = textParts.join(" ");
	if (combinedText.length > 4e3) combinedText = `${combinedText.slice(0, MAX_COALESCED_TEXT_CHARS)}…[truncated]`;
	const allAttachments = boundedPayloads.flatMap((p) => p.attachments ?? []).slice(0, 20);
	const createdAts = payloads.map((p) => p.created_at).filter((c) => typeof c === "string" && c.length > 0);
	const latestCreatedAt = createdAts.length > 0 ? createdAts.reduce((a, b) => a > b ? a : b) : first.created_at;
	let maxRowid = -Infinity;
	let maxDateMs = -Infinity;
	for (const payload of payloads) {
		if (typeof payload.id === "number" && Number.isFinite(payload.id)) maxRowid = Math.max(maxRowid, payload.id);
		const dateMs = typeof payload.created_at === "string" ? Date.parse(payload.created_at) : NaN;
		if (Number.isFinite(dateMs)) maxDateMs = Math.max(maxDateMs, dateMs);
	}
	const seenGuids = /* @__PURE__ */ new Set();
	const coalescedMessageGuids = [];
	for (const payload of payloads) {
		const guid = payload.guid?.trim();
		if (!guid || seenGuids.has(guid)) continue;
		seenGuids.add(guid);
		coalescedMessageGuids.push(guid);
	}
	const entryWithReply = payloads.find((p) => p.reply_to_id != null);
	return {
		...first,
		text: combinedText,
		attachments: allAttachments.length > 0 ? allAttachments : null,
		created_at: latestCreatedAt,
		reply_to_id: entryWithReply?.reply_to_id ?? first.reply_to_id ?? null,
		reply_to_text: entryWithReply?.reply_to_text ?? first.reply_to_text ?? null,
		reply_to_sender: entryWithReply?.reply_to_sender ?? first.reply_to_sender ?? null,
		coalescedMessageGuids: coalescedMessageGuids.length > 0 ? coalescedMessageGuids : void 0,
		coalescedCatchupCursor: Number.isFinite(maxRowid) && Number.isFinite(maxDateMs) ? {
			lastSeenMs: maxDateMs,
			lastSeenRowid: maxRowid
		} : void 0
	};
}
//#endregion
//#region extensions/imessage/src/monitor/conversation-repair.ts
const DEFAULT_CHATS_LIMIT = 20;
const DEFAULT_PER_CHAT_HISTORY_LIMIT = 50;
const DEFAULT_RPC_TIMEOUT_MS = 5e3;
function isNonEmptyString(value) {
	return typeof value === "string" && value.trim() !== "";
}
function hasPositiveChatId(value) {
	return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function isExplicitEmptyString(value) {
	return typeof value === "string" && value.trim() === "";
}
function isIMessageAnchorless(message) {
	if (hasPositiveChatId(message.chat_id) || isNonEmptyString(message.chat_guid) || isNonEmptyString(message.chat_identifier)) return false;
	return message.chat_id === null || typeof message.chat_id === "number" && (!Number.isFinite(message.chat_id) || message.chat_id <= 0) || isExplicitEmptyString(message.chat_guid) || isExplicitEmptyString(message.chat_identifier);
}
function overlayRecoveredConversation(message, entry) {
	const repaired = { ...message };
	if (hasPositiveChatId(entry.chat_id)) repaired.chat_id = entry.chat_id;
	if (isNonEmptyString(entry.chat_guid)) repaired.chat_guid = entry.chat_guid;
	if (isNonEmptyString(entry.chat_identifier)) repaired.chat_identifier = entry.chat_identifier;
	if (typeof entry.is_group === "boolean") repaired.is_group = entry.is_group;
	if (typeof entry.chat_name === "string") repaired.chat_name = entry.chat_name;
	if (Array.isArray(entry.participants) && entry.participants.every((participant) => typeof participant === "string")) repaired.participants = entry.participants;
	return repaired;
}
async function repairIMessageConversationAnchor(params) {
	const { client, message, runtime } = params;
	if (!isIMessageAnchorless(message)) return message;
	const guid = message.guid?.trim();
	if (!guid) {
		runtime?.error?.("imessage: dropping anchorless message without GUID");
		return null;
	}
	let chatsResult;
	try {
		chatsResult = await client.request("chats.list", { limit: params.chatsLimit ?? DEFAULT_CHATS_LIMIT }, { timeoutMs: params.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS });
	} catch (err) {
		runtime?.error?.(`imessage: anchorless message recovery failed listing chats: ${String(err)}`);
		return null;
	}
	const chats = chatsResult?.chats ?? [];
	for (const chat of chats) {
		const chatId = hasPositiveChatId(chat.id) ? chat.id : null;
		if (chatId === null) continue;
		let historyResult;
		try {
			historyResult = await client.request("messages.history", {
				attachments: false,
				chat_id: chatId,
				limit: params.perChatHistoryLimit ?? DEFAULT_PER_CHAT_HISTORY_LIMIT
			}, { timeoutMs: params.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS });
		} catch {
			continue;
		}
		const messages = Array.isArray(historyResult?.messages) ? historyResult.messages : [];
		for (const raw of messages) {
			if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
			const entry = raw;
			if (entry.guid !== guid) continue;
			const repaired = overlayRecoveredConversation(message, entry);
			if (isIMessageAnchorless(repaired)) {
				runtime?.error?.(`imessage: dropping anchorless message GUID=${guid} after recovery found no usable conversation anchor`);
				return null;
			}
			runtime?.log?.(`imessage: recovered anchorless message GUID=${guid} chat_id=${repaired.chat_id ?? "unknown"} is_group=${repaired.is_group === true}`);
			return repaired;
		}
	}
	runtime?.error?.(`imessage: dropping anchorless message GUID=${guid}; no recent chat matched`);
	return null;
}
//#endregion
//#region extensions/imessage/src/monitor/deliver.ts
async function deliverReplies(params) {
	const { replies, target, runtime, maxBytes, textLimit, accountId, sentMessageCache } = params;
	const scope = `${accountId ?? ""}:${target}`;
	const { cfg } = params;
	const tableMode = resolveMarkdownTableMode({
		cfg,
		channel: "imessage",
		accountId
	});
	const chunkMode = resolveChunkMode(cfg, "imessage", accountId);
	for (const payload of replies) if (await deliverTextOrMediaReply({
		payload,
		text: resolveSendableOutboundReplyParts(payload, { text: convertMarkdownTables(sanitizeOutboundText(payload.text ?? ""), tableMode) }).text,
		chunkText: (value) => chunkTextWithMode(value, textLimit, chunkMode),
		sendText: async (chunk) => {
			const sent = await sendMessageIMessage(target, chunk, {
				config: params.cfg,
				maxBytes,
				accountId,
				replyToId: payload.replyToId
			});
			sentMessageCache?.remember(scope, {
				text: sent.echoText ?? sent.sentText,
				messageId: sent.messageId
			});
		},
		sendMedia: async ({ mediaUrl, caption }) => {
			const sent = await sendMessageIMessage(target, caption ?? "", {
				config: params.cfg,
				mediaUrl,
				maxBytes,
				accountId,
				replyToId: payload.replyToId
			});
			sentMessageCache?.remember(scope, {
				text: sent.echoText ?? (sent.sentText || void 0),
				messageId: sent.messageId
			});
		}
	}) !== "empty") runtime.log?.(`imessage: delivered reply to ${target}`);
}
function createIMessageEchoCachingSend(params) {
	return async (target, text, opts) => {
		const sent = await sendMessageIMessage(target, sanitizeOutboundText(text), opts);
		const scope = `${params.accountId ?? opts.accountId ?? ""}:${target}`;
		params.sentMessageCache?.remember(scope, {
			text: sent.echoText ?? (sent.sentText || void 0),
			messageId: sent.messageId
		});
		return sent;
	};
}
//#endregion
//#region extensions/imessage/src/monitor/dm-history.ts
const DM_HISTORY_RPC_TIMEOUT_MS = 1e4;
function resolveIMessageDmHistoryLimit(params) {
	const senderCandidates = [
		normalizeOptionalString(params.senderNormalized),
		normalizeOptionalString(params.sender),
		params.sender ? normalizeIMessageHandle(params.sender) : void 0
	].filter((candidate) => Boolean(candidate));
	for (const candidate of senderCandidates) {
		const override = params.config.dms?.[candidate]?.historyLimit;
		if (override !== void 0) return Math.max(0, override);
	}
	return Math.max(0, params.config.dmHistoryLimit ?? 0);
}
function historyRowSortValue(message) {
	if (typeof message.id === "number" && Number.isFinite(message.id)) return message.id;
	const createdAtMs = typeof message.created_at === "string" ? Date.parse(message.created_at) : NaN;
	return Number.isFinite(createdAtMs) ? createdAtMs : 0;
}
function isBeforeCurrentMessage(params) {
	const { message, currentMessage } = params;
	if (typeof message.id === "number" && typeof currentMessage.id === "number" && Number.isFinite(message.id) && Number.isFinite(currentMessage.id)) return message.id < currentMessage.id;
	const guid = normalizeOptionalString(message.guid);
	const currentGuid = normalizeOptionalString(currentMessage.guid);
	if (guid && currentGuid) return guid !== currentGuid;
	return true;
}
function historyEntryFromMessage(message, fallbackSender) {
	const body = normalizeOptionalString(message.text);
	if (!body) return null;
	const timestamp = typeof message.created_at === "string" ? Date.parse(message.created_at) : NaN;
	return {
		sender: message.is_from_me === true ? "Me" : normalizeIMessageHandle(normalizeOptionalString(message.sender) ?? fallbackSender) || fallbackSender,
		body,
		...Number.isFinite(timestamp) ? { timestamp } : {}
	};
}
async function resolveIMessageDmHistoryContext(params) {
	const maxMessages = Math.max(0, Math.floor(params.limit));
	const chatId = typeof params.message.chat_id === "number" && Number.isFinite(params.message.chat_id) ? params.message.chat_id : void 0;
	if (maxMessages <= 0 || chatId === void 0) return {};
	let result;
	try {
		result = await params.client.request("messages.history", {
			chat_id: chatId,
			limit: maxMessages + 1,
			attachments: false
		}, { timeoutMs: DM_HISTORY_RPC_TIMEOUT_MS });
	} catch (err) {
		params.logVerbose?.(`imessage: DM history fetch failed for chat_id=${chatId}: ${String(err)}`);
		return {};
	}
	const history = (Array.isArray(result?.messages) ? result.messages : []).map((row) => parseIMessageNotification({ message: row })).filter((message) => Boolean(message)).filter((message) => message.is_group !== true).filter((message) => isBeforeCurrentMessage({
		message,
		currentMessage: params.message
	})).toSorted((a, b) => historyRowSortValue(a) - historyRowSortValue(b)).map((message) => historyEntryFromMessage(message, params.senderNormalized)).filter((entry) => Boolean(entry)).slice(-maxMessages);
	if (history.length === 0) return {};
	return {
		inboundHistory: history,
		body: history.map((entry) => formatInboundEnvelope({
			channel: "iMessage",
			from: entry.sender,
			timestamp: entry.timestamp,
			body: entry.body,
			chatType: "direct",
			senderLabel: entry.sender,
			envelope: params.envelopeOptions
		})).join("\n\n")
	};
}
//#endregion
//#region extensions/imessage/src/monitor/echo-cache.ts
const SENT_MESSAGE_TEXT_TTL_MS = 4e3;
const SENT_MESSAGE_ID_TTL_MS = 6e4;
const LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS = /^[\uFEFF\uFFFD\uFFFE\uFFFF]+/u;
function normalizeEchoTextKey(text) {
	if (!text) return null;
	const normalized = text.replace(/\r\n?/g, "\n").trim().replace(LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS, "").trim();
	return normalized ? normalized : null;
}
function normalizeEchoMessageIdKey(messageId) {
	if (!messageId) return null;
	const normalized = messageId.trim();
	if (!normalized || normalized === "ok" || normalized === "unknown") return null;
	return normalized;
}
var DefaultSentMessageCache = class {
	constructor() {
		this.textCache = /* @__PURE__ */ new Map();
		this.textBackedByIdCache = /* @__PURE__ */ new Map();
		this.messageIdCache = /* @__PURE__ */ new Map();
	}
	remember(scope, lookup) {
		const textKey = normalizeEchoTextKey(lookup.text);
		if (textKey) this.textCache.set(`${scope}:${textKey}`, Date.now());
		const messageIdKey = normalizeEchoMessageIdKey(lookup.messageId);
		if (messageIdKey) {
			this.messageIdCache.set(`${scope}:${messageIdKey}`, Date.now());
			if (textKey) this.textBackedByIdCache.set(`${scope}:${textKey}`, Date.now());
		}
		this.cleanup();
	}
	has(scope, lookup, options = false) {
		this.cleanup();
		const resolvedOptions = typeof options === "boolean" ? { skipIdShortCircuit: options } : options;
		if (hasPersistedIMessageEcho({
			scope,
			...lookup,
			includePendingText: resolvedOptions.includePendingText
		})) return true;
		const textKey = normalizeEchoTextKey(lookup.text);
		const messageIdKey = normalizeEchoMessageIdKey(lookup.messageId);
		if (messageIdKey) {
			const idTimestamp = this.messageIdCache.get(`${scope}:${messageIdKey}`);
			if (idTimestamp && Date.now() - idTimestamp <= SENT_MESSAGE_ID_TTL_MS) return true;
			const textTimestamp = textKey ? this.textCache.get(`${scope}:${textKey}`) : void 0;
			const textBackedByIdTimestamp = textKey ? this.textBackedByIdCache.get(`${scope}:${textKey}`) : void 0;
			const hasTextOnlyMatch = typeof textTimestamp === "number" && (!textBackedByIdTimestamp || textTimestamp > textBackedByIdTimestamp);
			if (!resolvedOptions.skipIdShortCircuit && !hasTextOnlyMatch) return false;
		}
		if (textKey) {
			const textTimestamp = this.textCache.get(`${scope}:${textKey}`);
			if (textTimestamp && Date.now() - textTimestamp <= SENT_MESSAGE_TEXT_TTL_MS) return true;
		}
		return false;
	}
	cleanup() {
		const now = Date.now();
		for (const [key, timestamp] of this.textCache.entries()) if (now - timestamp > SENT_MESSAGE_TEXT_TTL_MS) this.textCache.delete(key);
		for (const [key, timestamp] of this.textBackedByIdCache.entries()) if (now - timestamp > SENT_MESSAGE_TEXT_TTL_MS) this.textBackedByIdCache.delete(key);
		for (const [key, timestamp] of this.messageIdCache.entries()) if (now - timestamp > SENT_MESSAGE_ID_TTL_MS) this.messageIdCache.delete(key);
	}
};
function createSentMessageCache() {
	return new DefaultSentMessageCache();
}
//#endregion
//#region extensions/imessage/src/monitor/group-allowlist-warnings.ts
const startupWarned = /* @__PURE__ */ new Set();
const perChatWarned = /* @__PURE__ */ new Set();
/**
* Fires once per `accountId` at monitor startup when `groupPolicy === "allowlist"`
* but `channels.imessage.groups` is empty (no `"*"` wildcard, no explicit
* `chat_id` entries). Without one of those, every group message is dropped at
* the second gate even when the sender passes `groupAllowFrom`.
*/
function warnGroupAllowlistMisconfigOnce(params) {
	if (params.groupPolicy !== "allowlist") return false;
	if ((params.groups ? Object.keys(params.groups) : []).length > 0) return false;
	const key = `imessage:${params.accountId}`;
	if (startupWarned.has(key)) return false;
	startupWarned.add(key);
	params.log(`imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "${params.accountId}". Every inbound group message will be dropped. Add channels.imessage.groups["*"] = { requireMention: true } to allow all groups, or explicit per-chat_id entries to allow specific groups.`);
	return true;
}
/**
* Fires once per `accountId:chat_id` when the runtime allowlist gate drops a
* group message because that chat_id is not in `channels.imessage.groups`.
* Bounded by the number of distinct group chats the gateway sees.
*/
function warnGroupAllowlistDropPerChatOnce(params) {
	const chat = params.chatId == null ? "" : String(params.chatId).trim();
	if (!chat) return false;
	const key = `imessage:${params.accountId}:${chat}`;
	if (perChatWarned.has(key)) return false;
	perChatWarned.add(key);
	params.log(`imessage: dropping group message from chat_id=${chat} (account "${params.accountId}") — not in channels.imessage.groups allowlist. Add channels.imessage.groups["${chat}"] or channels.imessage.groups["*"] to allow it.`);
	return true;
}
//#endregion
//#region extensions/imessage/src/monitor/inbound-dedupe.ts
const IMESSAGE_INBOUND_DEDUPE_PLUGIN_ID = "imessage";
const IMESSAGE_INBOUND_DEDUPE_NAMESPACE_PREFIX = "imessage.inbound-dedupe";
const IMESSAGE_INBOUND_DEDUPE_TTL_MS = 14400 * 1e3;
const IMESSAGE_INBOUND_DEDUPE_MEMORY_MAX = 5e3;
const IMESSAGE_INBOUND_DEDUPE_STATE_MAX_ENTRIES = 1e4;
const IMESSAGE_STALE_INBOUND_THRESHOLD_MS = 900 * 1e3;
const IMESSAGE_RECOVERY_MAX_AGE_MS = 7200 * 1e3;
/**
* Persistent inbound replay guard. Claimable (not a bare check/record) so the
* claim is atomic: a duplicate emitted twice in a reconnect burst while the
* first copy is still in flight is reported as a duplicate/inflight instead of
* racing through. Persistent so a claim committed before a crash still blocks a
* post-restart re-emit; release on dispatch failure lets a transient failure
* retry instead of being permanently suppressed.
*/
function createIMessageInboundReplayGuard() {
	return createClaimableDedupe({
		pluginId: IMESSAGE_INBOUND_DEDUPE_PLUGIN_ID,
		namespacePrefix: IMESSAGE_INBOUND_DEDUPE_NAMESPACE_PREFIX,
		ttlMs: IMESSAGE_INBOUND_DEDUPE_TTL_MS,
		memoryMaxSize: IMESSAGE_INBOUND_DEDUPE_MEMORY_MAX,
		stateMaxEntries: IMESSAGE_INBOUND_DEDUPE_STATE_MAX_ENTRIES
	});
}
/**
* Claim a message before handling. Returns the key to commit/release later, and
* `claimed=false` when a recent copy already owns the key (duplicate/inflight)
* so the caller drops it. A message with no derivable key fails open (claimed,
* key=null) so it is always handled and nothing to commit.
*/
async function claimIMessageInboundReplay(params) {
	const key = buildIMessageInboundReplayKey({
		accountId: params.accountId,
		message: params.message
	});
	if (!key) return {
		claimed: true,
		key: null
	};
	return {
		claimed: (await params.guard.claim(key, { namespace: params.accountId })).kind === "claimed",
		key
	};
}
async function commitIMessageInboundReplay(params) {
	for (const key of new Set(params.keys)) await params.guard.commit(key, { namespace: params.accountId });
}
function releaseIMessageInboundReplay(params) {
	for (const key of new Set(params.keys)) params.guard.release(key, {
		namespace: params.accountId,
		error: params.error
	});
}
/**
* Stable replay key for an inbound message. Prefers the Apple GUID (globally
* unique, survives chat.db rowid churn). Falls back to a composite of the
* fields that identify a distinct send when no GUID is present, and returns
* null when the message cannot be identified at all (fail open: never suppress
* an unidentifiable message).
*/
function buildIMessageInboundReplayKey(params) {
	const { accountId, message } = params;
	const guid = message.guid?.trim();
	if (guid) return `${accountId}:guid:${guid}`;
	const sender = message.sender?.trim();
	const conversation = message.chat_id != null ? `chat:${message.chat_id}` : message.chat_guid?.trim() ?? message.chat_identifier?.trim();
	const createdAt = message.created_at?.trim();
	if (!sender || !conversation || !createdAt) return null;
	const text = (message.text ?? "").trim();
	return `${accountId}:c:${createHash("sha256").update(`${conversation}\0${sender}\0${createdAt}\0${text}`).digest("hex").slice(0, 32)}`;
}
/**
* Age fence: true when the message's own send date is materially older than
* now, i.e. stale backlog rather than a live message. Fails open (returns
* false) when the send date is missing or unparseable so an undateable message
* is never suppressed on a timestamp we cannot read.
*/
function isStaleIMessageBacklog(message, nowMs, thresholdMs = IMESSAGE_STALE_INBOUND_THRESHOLD_MS) {
	const createdAt = message.created_at?.trim();
	if (!createdAt) return false;
	const sentMs = Date.parse(createdAt);
	if (!Number.isFinite(sentMs)) return false;
	return nowMs - sentMs > thresholdMs;
}
//#endregion
//#region extensions/imessage/src/conversation-route.ts
function resolveIMessageConversationRoute(params) {
	let route = resolveAgentRoute({
		cfg: params.cfg,
		channel: "imessage",
		accountId: params.accountId,
		peer: {
			kind: params.isGroup ? "group" : "direct",
			id: params.peerId
		}
	});
	const conversationId = resolveIMessageInboundConversationId({
		isGroup: params.isGroup,
		sender: params.sender,
		chatId: params.chatId
	});
	if (!conversationId) return route;
	route = resolveConfiguredBindingRoute({
		cfg: params.cfg,
		route,
		conversation: {
			channel: "imessage",
			accountId: params.accountId,
			conversationId
		}
	}).route;
	const runtimeRoute = resolveRuntimeConversationBindingRoute({
		route,
		conversation: {
			channel: "imessage",
			accountId: params.accountId,
			conversationId
		}
	});
	route = runtimeRoute.route;
	if (runtimeRoute.bindingRecord && !runtimeRoute.boundSessionKey) logVerbose(`imessage: plugin-bound conversation ${conversationId}`);
	else if (runtimeRoute.boundSessionKey) logVerbose(`imessage: routed via bound conversation ${conversationId} -> ${runtimeRoute.boundSessionKey}`);
	return route;
}
//#endregion
//#region extensions/imessage/src/monitor/reflection-guard.ts
const REFLECTION_PATTERNS = [
	{
		re: /(?:#\+){2,}#?/,
		label: "internal-separator"
	},
	{
		re: /\bassistant\s+to\s*=\s*\w+/i,
		label: "assistant-role-marker"
	},
	{
		re: /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/i,
		label: "thinking-tag"
	},
	{
		re: /<\s*\/?\s*relevant[-_]memories\b[^<>]*>/i,
		label: "relevant-memories-tag"
	},
	{
		re: /<\s*\/?\s*final\b[^<>]*>/i,
		label: "final-tag"
	},
	{
		re: /\bACP error\s*\(\s*ACP_[A-Z0-9_]+\s*\):/i,
		label: "acp-error"
	},
	{
		re: /\bMissing API key for\b.+\bon the gateway\b/i,
		label: "gateway-missing-api-key"
	}
];
function hasMatchOutsideCode(text, re) {
	const codeRegions = findCodeRegions(text);
	const globalRe = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
	for (const match of text.matchAll(globalRe)) {
		const start = match.index ?? -1;
		if (start >= 0 && !isInsideCode(start, codeRegions)) return true;
	}
	return false;
}
/**
* Check whether an inbound message appears to be a reflection of
* assistant-originated content. Returns matched pattern labels for telemetry.
*/
function detectReflectedContent(text) {
	if (!text) return {
		isReflection: false,
		matchedLabels: []
	};
	const matchedLabels = [];
	for (const { re, label } of REFLECTION_PATTERNS) if (hasMatchOutsideCode(text, re)) matchedLabels.push(label);
	return {
		isReflection: matchedLabels.length > 0,
		matchedLabels
	};
}
//#endregion
//#region extensions/imessage/src/monitor/inbound-processing.ts
const normalizeNonEmpty = (value) => value.trim() || null;
const imessageConversationIdentityKinds = new Set([
	"plugin:imessage-chat-id",
	"plugin:imessage-chat-guid",
	"plugin:imessage-chat-identifier"
]);
const matchIMessageIngressEntry = ({ entry, context }) => {
	if (imessageConversationIdentityKinds.has(entry.kind) && context !== "group") return false;
};
function isIMessageConversationAllowTarget(entry) {
	const parsed = parseIMessageAllowTarget(entry);
	return parsed.kind === "chat_id" || parsed.kind === "chat_guid" || parsed.kind === "chat_identifier";
}
function mergeIMessageGroupAllowFromWithLegacyChatTargets(params) {
	if (params.groupAllowFrom.length > 0 || !params.allowLegacyConversationTargets) return params.groupAllowFrom;
	const legacyChatTargets = params.allowFrom.filter((entry) => isIMessageConversationAllowTarget(entry));
	if (legacyChatTargets.length === 0) return params.groupAllowFrom;
	return uniqueStrings([...params.groupAllowFrom, ...legacyChatTargets]);
}
const imessageIngressIdentity = defineStableChannelIngressIdentity({
	key: "imessage-sender",
	normalizeEntry: normalizeIMessageHandleEntry,
	normalizeSubject: normalizeIMessageHandle,
	sensitivity: "pii",
	matchEntry: matchIMessageIngressEntry,
	aliases: [
		[
			"imessage-chat-id",
			"plugin:imessage-chat-id",
			normalizeIMessageChatIdEntry
		],
		[
			"imessage-chat-guid",
			"plugin:imessage-chat-guid",
			normalizeIMessageChatGuidEntry
		],
		[
			"imessage-chat-identifier",
			"plugin:imessage-chat-identifier",
			normalizeIMessageChatIdentifierEntry
		]
	].map(([key, kind, normalizeEntry]) => ({
		key,
		kind,
		normalizeEntry,
		normalizeSubject: normalizeNonEmpty,
		sensitivity: "pii"
	})),
	resolveEntryId: ({ entryIndex }) => `imessage-entry-${entryIndex + 1}`
});
function normalizeIMessageHandleEntry(entry) {
	const parsed = parseIMessageAllowTarget(entry.trim());
	return parsed.kind === "handle" ? normalizeIMessageHandle(parsed.handle) : null;
}
function normalizeIMessageChatIdEntry(entry) {
	const parsed = parseIMessageAllowTarget(entry.trim());
	return parsed.kind === "chat_id" ? String(parsed.chatId) : null;
}
function normalizeIMessageChatGuidEntry(entry) {
	const parsed = parseIMessageAllowTarget(entry.trim());
	return parsed.kind === "chat_guid" ? parsed.chatGuid.trim() || null : null;
}
function normalizeIMessageChatIdentifierEntry(entry) {
	const parsed = parseIMessageAllowTarget(entry.trim());
	return parsed.kind === "chat_identifier" ? parsed.chatIdentifier.trim() || null : null;
}
function normalizeDmPolicy(policy) {
	return policy === "open" || policy === "allowlist" || policy === "disabled" ? policy : "pairing";
}
function normalizeGroupPolicy(policy) {
	return policy === "open" || policy === "disabled" ? policy : "allowlist";
}
function normalizeReplyField(value) {
	if (typeof value === "string") {
		const trimmed = value.trim();
		return trimmed ? trimmed : void 0;
	}
	if (typeof value === "number") return String(value);
}
function describeReplyContext(message) {
	const body = normalizeReplyField(message.reply_to_text);
	if (!body) return null;
	return {
		body,
		id: normalizeReplyField(message.reply_to_id),
		sender: normalizeReplyField(message.reply_to_sender)
	};
}
function resolveInboundEchoMessageIds(message) {
	const values = [message.id != null ? String(message.id) : void 0, normalizeReplyField(message.guid)];
	const ids = [];
	for (const value of values) {
		if (!value || ids.includes(value)) continue;
		ids.push(value);
	}
	return ids;
}
function rememberIMessageSkippedFromMeForSelfChatDedupe(params) {
	if (params.message.is_from_me !== true) return;
	const sender = params.message.sender?.trim();
	if (!sender) return;
	const chatId = params.message.chat_id ?? void 0;
	const isGroup = Boolean(params.message.is_group);
	const chatIdentifierNormalized = normalizeIMessageHandle(params.message.chat_identifier ?? "") || void 0;
	const destinationCallerIdNormalized = normalizeIMessageHandle(params.message.destination_caller_id ?? "") || void 0;
	const senderNormalized = normalizeIMessageHandle(sender);
	const createdAt = params.message.created_at ? Date.parse(params.message.created_at) : void 0;
	const lookup = {
		accountId: params.accountId,
		isGroup,
		chatId,
		sender,
		text: params.bodyText.trim(),
		createdAt
	};
	const isSelfChat = !isGroup && chatIdentifierNormalized != null && senderNormalized === chatIdentifierNormalized && destinationCallerIdNormalized != null && destinationCallerIdNormalized === senderNormalized;
	const isAmbiguousSelfThread = !isGroup && chatIdentifierNormalized != null && senderNormalized === chatIdentifierNormalized && destinationCallerIdNormalized == null;
	if (isSelfChat) params.selfChatCache?.remember({
		...lookup,
		allowCreatedAtSkew: true
	});
	else if (isAmbiguousSelfThread) params.selfChatCache?.remember(lookup);
}
function hasIMessageEchoMatch(params) {
	const scopes = typeof params.scope === "string" ? [params.scope] : params.scope;
	for (const scope of scopes) {
		if (!scope) continue;
		for (const messageId of params.messageIds) if (params.echoCache.has(scope, { messageId })) return true;
		const fallbackMessageId = params.messageIds[0];
		if (!params.text && !fallbackMessageId) continue;
		if (params.echoCache.has(scope, {
			text: params.text,
			messageId: fallbackMessageId
		}, {
			skipIdShortCircuit: params.skipIdShortCircuit,
			includePendingText: params.includePendingText
		})) return true;
	}
	return false;
}
function isKnownFromMeIMessageReactionTarget(params) {
	const { messageId, accountId, chatId, chatGuid, chatIdentifier } = params;
	const ctx = {
		accountId,
		chatId,
		chatGuid,
		chatIdentifier
	};
	if (params.isKnownFromMeMessageId) return params.isKnownFromMeMessageId(messageId, ctx);
	return isKnownFromMeIMessageMessageId(messageId, ctx);
}
/**
* Per-group `systemPrompt` resolution. Mirrors `resolveWhatsAppGroupSystemPrompt`
* in `extensions/whatsapp/src/system-prompt.ts`:
*
* 1. If the matched per-`chat_id` entry exists AND defines `systemPrompt` (key
*    is present, value is non-null), use it. Trim whitespace; if the trim
*    leaves an empty string, return `undefined` and DO NOT fall through to the
*    wildcard. This is how operators say "this specific group has no prompt"
*    without inheriting from `groups["*"]`.
* 2. Otherwise, return the wildcard `groups["*"].systemPrompt` (trimmed; empty
*    after trim → `undefined`).
*/
function resolveIMessageGroupSystemPrompt(params) {
	const specific = params.groupConfig;
	if (specific != null && specific.systemPrompt != null) return specific.systemPrompt.trim() || void 0;
	const wildcard = params.defaultConfig?.systemPrompt;
	return wildcard != null ? wildcard.trim() || void 0 : void 0;
}
async function resolveIMessageInboundDecision(params) {
	const sender = (params.message.sender ?? "").trim();
	if (!sender) return {
		kind: "drop",
		reason: "missing sender"
	};
	const senderNormalized = normalizeIMessageHandle(sender);
	const chatId = params.message.chat_id ?? void 0;
	const chatGuid = params.message.chat_guid ?? void 0;
	const chatIdentifier = params.message.chat_identifier ?? void 0;
	const destinationCallerId = params.message.destination_caller_id ?? void 0;
	const createdAt = params.message.created_at ? Date.parse(params.message.created_at) : void 0;
	const messageText = params.messageText.trim();
	const bodyText = params.bodyText.trim();
	const reactionContext = resolveIMessageReactionContext(params.message, bodyText || messageText);
	const groupIdCandidate = chatId !== void 0 ? String(chatId) : void 0;
	const groupAllowFromWithLegacyChatTargets = mergeIMessageGroupAllowFromWithLegacyChatTargets({
		groupAllowFrom: params.groupAllowFrom,
		allowFrom: params.allowFrom,
		allowLegacyConversationTargets: params.allowLegacyConversationAllowFromForGroup
	});
	const groupListPolicy = groupIdCandidate ? resolveChannelGroupPolicy({
		cfg: params.cfg,
		channel: "imessage",
		accountId: params.accountId,
		groupId: groupIdCandidate,
		hasGroupAllowFrom: groupAllowFromWithLegacyChatTargets.length > 0
	}) : {
		allowlistEnabled: false,
		allowed: true,
		groupConfig: void 0,
		defaultConfig: void 0
	};
	const treatAsGroupByConfig = Boolean(groupIdCandidate && groupListPolicy.allowlistEnabled && groupListPolicy.groupConfig);
	const isGroup = Boolean(params.message.is_group) || treatAsGroupByConfig;
	const selfChatLookup = {
		accountId: params.accountId,
		isGroup,
		chatId,
		sender,
		text: bodyText,
		createdAt
	};
	const chatIdentifierNormalized = normalizeIMessageHandle(chatIdentifier ?? "") || void 0;
	const destinationCallerIdNormalized = normalizeIMessageHandle(destinationCallerId ?? "") || void 0;
	const isSelfChat = !isGroup && chatIdentifierNormalized != null && senderNormalized === chatIdentifierNormalized && destinationCallerIdNormalized != null && destinationCallerIdNormalized === senderNormalized;
	const isAmbiguousSelfThread = !isGroup && chatIdentifierNormalized != null && senderNormalized === chatIdentifierNormalized && destinationCallerIdNormalized == null;
	let skipSelfChatHasCheck = false;
	const inboundMessageIds = resolveInboundEchoMessageIds(params.message);
	const inboundMessageId = inboundMessageIds[0];
	const hasInboundGuid = Boolean(normalizeReplyField(params.message.guid));
	if (params.message.is_from_me) {
		if (isAmbiguousSelfThread) params.selfChatCache?.remember(selfChatLookup);
		if (isSelfChat) {
			params.selfChatCache?.remember({
				...selfChatLookup,
				allowCreatedAtSkew: true
			});
			const echoScope = buildIMessageEchoScope({
				accountId: params.accountId,
				isGroup,
				chatId,
				chatGuid,
				chatIdentifier,
				sender
			});
			if (params.echoCache && (bodyText || inboundMessageId) && hasIMessageEchoMatch({
				echoCache: params.echoCache,
				scope: echoScope,
				text: bodyText || void 0,
				messageIds: inboundMessageIds,
				skipIdShortCircuit: !hasInboundGuid,
				includePendingText: true
			})) return {
				kind: "drop",
				reason: "agent echo in self-chat"
			};
			skipSelfChatHasCheck = true;
		} else return {
			kind: "drop",
			reason: "from me"
		};
	}
	if (isGroup && !chatId) return {
		kind: "drop",
		reason: "group without chat_id"
	};
	const groupId = isGroup ? groupIdCandidate : void 0;
	const hasControlCommandInMessage = hasControlCommand(messageText, params.cfg);
	const groupAllowFromForAccess = isGroup ? groupAllowFromWithLegacyChatTargets : params.groupAllowFrom;
	const { commandAccess, senderAccess } = await createChannelIngressResolver({
		channelId: "imessage",
		accountId: params.accountId,
		identity: imessageIngressIdentity,
		cfg: params.cfg,
		readStoreAllowFrom: async () => params.storeAllowFrom
	}).message({
		subject: {
			stableId: sender,
			aliases: {
				...chatId != null ? { "imessage-chat-id": String(chatId) } : {},
				...chatGuid ? { "imessage-chat-guid": chatGuid } : {},
				...chatIdentifier ? { "imessage-chat-identifier": chatIdentifier } : {}
			}
		},
		conversation: {
			kind: isGroup ? "group" : "direct",
			id: isGroup ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") : normalizeIMessageHandle(sender)
		},
		dmPolicy: normalizeDmPolicy(params.dmPolicy),
		groupPolicy: normalizeGroupPolicy(params.groupPolicy),
		policy: { groupAllowFromFallbackToAllowFrom: false },
		allowFrom: params.allowFrom,
		groupAllowFrom: groupAllowFromForAccess,
		command: {
			allowTextCommands: isGroup,
			hasControlCommand: hasControlCommandInMessage,
			directGroupAllowFrom: "effective"
		}
	});
	const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
	if (senderAccess.decision !== "allow") {
		if (isGroup) {
			if (senderAccess.reasonCode === "group_policy_disabled") {
				params.logVerbose?.("Blocked iMessage group message (groupPolicy: disabled)");
				return {
					kind: "drop",
					reason: "groupPolicy disabled"
				};
			}
			if (senderAccess.reasonCode === "group_policy_empty_allowlist") {
				params.logVerbose?.("Blocked iMessage group message (groupPolicy: allowlist, no groupAllowFrom)");
				return {
					kind: "drop",
					reason: "groupPolicy allowlist (empty groupAllowFrom)"
				};
			}
			if (senderAccess.reasonCode === "group_policy_not_allowlisted") {
				params.logVerbose?.(`Blocked iMessage sender ${sender} (not in groupAllowFrom)`);
				return {
					kind: "drop",
					reason: "not in groupAllowFrom"
				};
			}
			params.logVerbose?.(`Blocked iMessage group message (${senderAccess.reasonCode})`);
			return {
				kind: "drop",
				reason: senderAccess.reasonCode
			};
		}
		if (senderAccess.reasonCode === "dm_policy_disabled") return {
			kind: "drop",
			reason: "dmPolicy disabled"
		};
		if (senderAccess.decision === "pairing") return {
			kind: "pairing",
			senderId: senderNormalized
		};
		params.logVerbose?.(`Blocked iMessage sender ${sender} (dmPolicy=${params.dmPolicy})`);
		return {
			kind: "drop",
			reason: "dmPolicy blocked"
		};
	}
	if (isGroup && groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) {
		params.logVerbose?.(`imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`);
		return {
			kind: "drop",
			reason: "group id not in allowlist"
		};
	}
	const route = resolveIMessageConversationRoute({
		cfg: params.cfg,
		accountId: params.accountId,
		isGroup,
		peerId: isGroup ? String(chatId ?? "unknown") : senderNormalized,
		sender,
		chatId
	});
	if (reactionContext) {
		const notificationMode = params.reactionNotifications ?? "own";
		if (notificationMode === "off") return {
			kind: "drop",
			reason: "reaction notifications disabled"
		};
		const targetGuid = reactionContext.targetGuid;
		const targetGuids = reactionContext.targetGuids ?? (targetGuid ? [targetGuid] : []);
		const targetIsOwn = Boolean(targetGuid && (params.echoCache && hasIMessageEchoMatch({
			echoCache: params.echoCache,
			scope: buildIMessageEchoScope({
				accountId: params.accountId,
				isGroup,
				chatId,
				chatGuid,
				chatIdentifier,
				sender
			}),
			messageIds: targetGuids
		}) || targetGuids.some((messageId) => isKnownFromMeIMessageReactionTarget({
			messageId,
			accountId: params.accountId,
			chatId,
			chatGuid,
			chatIdentifier,
			isKnownFromMeMessageId: params.isKnownFromMeMessageId
		}))));
		if (notificationMode === "own" && !targetIsOwn) return {
			kind: "drop",
			reason: "reaction target not sent by agent"
		};
		const target = targetGuid ? `msg ${targetGuid}` : reactionContext.targetText ? `message "${truncateUtf16Safe(reactionContext.targetText, 80)}"` : "a message";
		return {
			kind: "reaction",
			isGroup,
			chatId,
			chatGuid,
			chatIdentifier,
			sender,
			senderNormalized,
			route,
			reaction: reactionContext,
			text: `iMessage reaction ${reactionContext.action}: ${reactionContext.emoji} by ${senderNormalized} on ${target}`,
			contextKey: [
				"imessage",
				"reaction",
				reactionContext.action,
				chatId ?? chatGuid ?? chatIdentifier ?? senderNormalized,
				targetGuid ?? reactionContext.targetText ?? "unknown",
				senderNormalized,
				reactionContext.emoji
			].join(":")
		};
	}
	const mentionRegexes = buildMentionRegexes(params.cfg, route.agentId);
	if (!bodyText) return {
		kind: "drop",
		reason: "empty body"
	};
	if (skipSelfChatHasCheck ? false : params.selfChatCache?.has({
		...selfChatLookup,
		text: bodyText
	})) {
		const preview = sanitizeTerminalText(truncateUtf16Safe(bodyText, 50));
		params.logVerbose?.(`imessage: dropping self-chat reflected duplicate: "${preview}"`);
		return {
			kind: "drop",
			reason: "self-chat echo"
		};
	}
	if (params.echoCache && (messageText || inboundMessageId)) {
		const echoScope = buildIMessageEchoScope({
			accountId: params.accountId,
			isGroup,
			chatId,
			chatGuid,
			chatIdentifier,
			sender
		});
		if (hasIMessageEchoMatch({
			echoCache: params.echoCache,
			scope: echoScope,
			text: bodyText || void 0,
			messageIds: inboundMessageIds,
			includePendingText: isSelfChat
		})) {
			params.logVerbose?.(describeIMessageEchoDropLog({
				messageText: bodyText,
				messageId: inboundMessageId
			}));
			return {
				kind: "drop",
				reason: "echo"
			};
		}
	}
	const reflection = detectReflectedContent(messageText);
	if (reflection.isReflection) {
		params.logVerbose?.(`imessage: dropping reflected assistant content (markers: ${reflection.matchedLabels.join(", ")})`);
		return {
			kind: "drop",
			reason: "reflected assistant content"
		};
	}
	const replyContext = describeReplyContext(params.message);
	const contextVisibilityMode = resolveChannelContextVisibilityMode({
		cfg: params.cfg,
		channel: "imessage",
		accountId: params.accountId
	});
	const replyContextAllowFrom = Array.from(new Set([...groupAllowFromForAccess, ...effectiveGroupAllowFrom]));
	const replySenderAllowed = !isGroup || replyContextAllowFrom.length === 0 ? true : replyContext?.sender ? isAllowedIMessageReplyContextSender({
		allowFrom: replyContextAllowFrom,
		sender: replyContext.sender,
		chatId,
		chatGuid,
		chatIdentifier
	}) : false;
	const visibleReply = filterChannelInboundQuoteContext(contextVisibilityMode, replyContext ? {
		id: replyContext.id,
		body: replyContext.body,
		sender: replyContext.sender,
		senderAllowed: replySenderAllowed
	} : void 0);
	const filteredReplyContext = visibleReply ? {
		id: visibleReply.id,
		body: visibleReply.body ?? "",
		sender: visibleReply.sender
	} : null;
	if (replyContext && !filteredReplyContext && isGroup) params.logVerbose?.(`imessage: drop reply context (mode=${contextVisibilityMode}, sender_allowed=${replySenderAllowed ? "yes" : "no"})`);
	const historyKey = isGroup ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") : void 0;
	const mentioned = isGroup ? matchesMentionPatterns(messageText, mentionRegexes) : true;
	const requireMention = resolveChannelGroupRequireMention({
		cfg: params.cfg,
		channel: "imessage",
		accountId: params.accountId,
		groupId,
		requireMentionOverride: params.opts?.requireMention,
		overrideOrder: "before-config"
	});
	const canDetectMention = mentionRegexes.length > 0;
	const commandAuthorized = commandAccess.authorized;
	if (commandAccess.shouldBlockControlCommand) {
		if (params.logVerbose) logInboundDrop({
			log: params.logVerbose,
			channel: "imessage",
			reason: "control command (unauthorized)",
			target: sender
		});
		return {
			kind: "drop",
			reason: "control command (unauthorized)"
		};
	}
	const mentionDecision = resolveInboundMentionDecision({
		facts: {
			canDetectMention,
			wasMentioned: mentioned,
			hasAnyMention: false,
			implicitMentionKinds: []
		},
		policy: {
			isGroup,
			requireMention,
			allowTextCommands: true,
			hasControlCommand: hasControlCommandInMessage,
			commandAuthorized
		}
	});
	const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
	if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
		params.logVerbose?.(`imessage: skipping group message (no mention)`);
		createChannelHistoryWindow({ historyMap: params.groupHistories }).record({
			historyKey: historyKey ?? "",
			limit: params.historyLimit,
			entry: historyKey ? {
				sender: senderNormalized,
				body: bodyText,
				timestamp: createdAt,
				messageId: params.message.id ? String(params.message.id) : void 0
			} : null
		});
		return {
			kind: "drop",
			reason: "no mention"
		};
	}
	return {
		kind: "dispatch",
		isGroup,
		chatId,
		chatGuid,
		chatIdentifier,
		groupId,
		historyKey,
		sender,
		senderNormalized,
		route,
		bodyText,
		createdAt,
		replyContext: filteredReplyContext,
		effectiveWasMentioned,
		commandAuthorized,
		hasControlCommand: hasControlCommandInMessage,
		groupSystemPrompt: isGroup ? resolveIMessageGroupSystemPrompt({
			groupConfig: groupListPolicy.groupConfig,
			defaultConfig: groupListPolicy.defaultConfig
		}) : void 0
	};
}
async function buildIMessageInboundContext(params) {
	const envelopeOptions = params.envelopeOptions ?? resolveEnvelopeFormatOptions(params.cfg);
	const { decision } = params;
	const chatId = decision.chatId;
	const chatTarget = decision.isGroup && chatId != null ? formatIMessageChatTarget(chatId) : void 0;
	const messageGuid = normalizeReplyField(params.message.guid);
	const messageSid = (messageGuid ? rememberIMessageReplyCache({
		accountId: decision.route.accountId,
		messageId: messageGuid,
		chatGuid: decision.chatGuid,
		chatIdentifier: decision.chatIdentifier,
		chatId: decision.chatId,
		timestamp: Date.now(),
		isFromMe: false
	}) : null)?.shortId || void 0;
	const replySuffix = decision.replyContext ? `\n\n[Replying to ${decision.replyContext.sender ?? "unknown sender"}${decision.replyContext.id ? ` id:${decision.replyContext.id}` : ""}]\n${decision.replyContext.body}\n[/Replying]` : "";
	const fromLabel = formatInboundFromLabel({
		isGroup: decision.isGroup,
		groupLabel: params.message.chat_name ?? void 0,
		groupId: chatId !== void 0 ? String(chatId) : "unknown",
		groupFallback: "Group",
		directLabel: decision.senderNormalized,
		directId: decision.sender
	});
	let combinedBody = formatInboundEnvelope({
		channel: "iMessage",
		from: fromLabel,
		timestamp: decision.createdAt,
		body: `${decision.bodyText}${replySuffix}`,
		chatType: decision.isGroup ? "group" : "direct",
		sender: {
			name: decision.senderNormalized,
			id: decision.sender
		},
		previousTimestamp: params.previousTimestamp,
		envelope: envelopeOptions
	});
	if (!decision.isGroup && params.dmHistory?.body) combinedBody = `${params.dmHistory.body}\n\n${combinedBody}`;
	if (decision.isGroup && decision.historyKey) combinedBody = createChannelHistoryWindow({ historyMap: params.groupHistories }).buildPendingContext({
		historyKey: decision.historyKey,
		limit: params.historyLimit,
		currentMessage: combinedBody,
		formatEntry: (entry) => formatInboundEnvelope({
			channel: "iMessage",
			from: fromLabel,
			timestamp: entry.timestamp,
			body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`,
			chatType: "group",
			senderLabel: entry.sender,
			envelope: envelopeOptions
		})
	});
	const imessageTo = decision.isGroup ? chatTarget || `imessage:${decision.sender}` : buildDirectIMessageReplyTarget({
		cfg: params.cfg,
		accountId: decision.route.accountId,
		sender: decision.sender
	});
	const imessageFrom = decision.isGroup ? `imessage:group:${chatId ?? "unknown"}` : imessageTo;
	const inboundHistory = !decision.isGroup && params.dmHistory?.inboundHistory ? params.dmHistory.inboundHistory : decision.isGroup && decision.historyKey && params.historyLimit > 0 ? createChannelHistoryWindow({ historyMap: params.groupHistories }).buildInboundHistory({
		historyKey: decision.historyKey,
		limit: params.historyLimit
	}) : void 0;
	const media = toInboundMediaFacts(params.media?.paths && params.media.paths.length > 0 ? params.media.paths.map((path, index) => ({
		path,
		url: path,
		contentType: params.media?.types?.[index]
	})) : params.media?.path ? [{
		path: params.media.path,
		url: params.media.path,
		contentType: params.media.type
	}] : void 0);
	return {
		ctxPayload: buildChannelInboundEventContext({
			channel: "imessage",
			supplemental: {
				quote: decision.replyContext ? {
					id: decision.replyContext.id,
					body: decision.replyContext.body,
					sender: decision.replyContext.sender
				} : void 0,
				groupSystemPrompt: decision.isGroup ? decision.groupSystemPrompt : void 0
			},
			media,
			messageId: messageSid,
			messageIdFull: messageGuid,
			timestamp: decision.createdAt,
			from: imessageFrom,
			sender: {
				id: decision.sender,
				name: decision.senderNormalized
			},
			conversation: {
				kind: decision.isGroup ? "group" : "direct",
				id: chatId != null ? String(chatId) : decision.sender,
				label: fromLabel
			},
			route: {
				agentId: decision.route.agentId,
				accountId: decision.route.accountId,
				routeSessionKey: decision.route.sessionKey
			},
			reply: { to: imessageTo },
			message: {
				body: combinedBody,
				bodyForAgent: decision.bodyText,
				inboundHistory,
				rawBody: decision.bodyText,
				commandBody: decision.bodyText
			},
			access: {
				mentions: {
					canDetectMention: decision.isGroup,
					wasMentioned: decision.effectiveWasMentioned
				},
				commands: { authorized: decision.commandAuthorized }
			},
			extra: {
				GroupSubject: decision.isGroup ? params.message.chat_name ?? void 0 : void 0,
				GroupMembers: decision.isGroup ? (params.message.participants ?? []).filter(Boolean).join(", ") : void 0,
				MediaRemoteHost: params.remoteHost,
				CommandSource: decision.commandAuthorized && decision.hasControlCommand ? "text" : void 0
			}
		}),
		fromLabel,
		chatTarget,
		imessageTo,
		inboundHistory
	};
}
function buildIMessageEchoScope(params) {
	const scopes = [];
	if (params.isGroup) {
		const chatIdScope = formatIMessageChatTarget(params.chatId);
		if (chatIdScope) scopes.push(`${params.accountId}:${chatIdScope}`);
	} else scopes.push(`${params.accountId}:imessage:${params.sender}`);
	if (params.chatGuid) scopes.push(`${params.accountId}:chat_guid:${params.chatGuid}`);
	if (params.chatIdentifier) scopes.push(`${params.accountId}:chat_identifier:${params.chatIdentifier}`);
	return scopes;
}
function buildDirectIMessageReplyTarget(params) {
	if (resolveIMessageAccount({
		cfg: params.cfg,
		accountId: params.accountId
	}).config.service === "sms") return `sms:${params.sender}`;
	return `imessage:${params.sender}`;
}
function describeIMessageEchoDropLog(params) {
	const preview = truncateUtf16Safe(params.messageText, 50);
	return `imessage: skipping echo message${params.messageId ? ` id=${params.messageId}` : ""}: "${preview}"`;
}
//#endregion
//#region extensions/imessage/src/monitor/loop-rate-limiter.ts
/**
* Per-conversation rate limiter that detects rapid-fire identical echo
* patterns and suppresses them before they amplify into queue overflow.
*/
const DEFAULT_WINDOW_MS = 6e4;
const DEFAULT_MAX_HITS = 5;
const CLEANUP_INTERVAL_MS = 12e4;
function createLoopRateLimiter(opts) {
	const windowMs = opts?.windowMs ?? DEFAULT_WINDOW_MS;
	const maxHits = opts?.maxHits ?? DEFAULT_MAX_HITS;
	const conversations = /* @__PURE__ */ new Map();
	let lastCleanup = Date.now();
	function cleanup() {
		const now = Date.now();
		if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
		lastCleanup = now;
		for (const [key, win] of conversations.entries()) {
			const recent = win.timestamps.filter((ts) => now - ts <= windowMs);
			if (recent.length === 0) conversations.delete(key);
			else win.timestamps = recent;
		}
	}
	return {
		record(conversationKey) {
			cleanup();
			let win = conversations.get(conversationKey);
			if (!win) {
				win = { timestamps: [] };
				conversations.set(conversationKey, win);
			}
			win.timestamps.push(Date.now());
		},
		isRateLimited(conversationKey) {
			cleanup();
			const win = conversations.get(conversationKey);
			if (!win) return false;
			const now = Date.now();
			const recent = win.timestamps.filter((ts) => now - ts <= windowMs);
			win.timestamps = recent;
			return recent.length >= maxHits;
		}
	};
}
//#endregion
//#region extensions/imessage/src/monitor/media-staging.ts
function isHeicAttachment(attachmentPath, mimeType) {
	const normalizedMime = mimeType?.toLowerCase();
	if (normalizedMime === "image/heic" || normalizedMime === "image/heif") return true;
	const ext = path.extname(attachmentPath).toLowerCase();
	return ext === ".heic" || ext === ".heif";
}
function jpegFilenameForAttachment(attachmentPath) {
	return `${path.parse(attachmentPath).name || "imessage-attachment"}.jpg`;
}
function hasWildcardSegment(root) {
	return root.replaceAll("\\", "/").split("/").includes("*");
}
async function canonicalizeAllowedRoots(roots) {
	const canonicalRoots = [];
	for (const root of roots) {
		canonicalRoots.push(root);
		if (hasWildcardSegment(root)) continue;
		const canonicalRoot = await fs.realpath(root).catch(() => void 0);
		if (canonicalRoot && canonicalRoot !== root) canonicalRoots.push(canonicalRoot);
	}
	return canonicalRoots;
}
async function resolveAllowedCanonicalAttachmentPath(params) {
	if (!params.allowedRoots) return params.attachmentPath;
	const canonicalPath = await fs.realpath(params.attachmentPath);
	if (!isInboundPathAllowed({
		filePath: canonicalPath,
		roots: await canonicalizeAllowedRoots(params.allowedRoots)
	})) throw new Error("attachment path resolves outside allowed roots");
	return canonicalPath;
}
async function readAttachmentBuffer(params) {
	const stat = await fs.lstat(params.attachmentPath);
	if (stat.isSymbolicLink()) throw new Error("attachment path is a symlink");
	if (!stat.isFile()) throw new Error("attachment path is not a file");
	if (stat.size > params.maxBytes) throw new Error(`attachment exceeds ${Math.round(params.maxBytes / (1024 * 1024))}MB limit`);
	const canonicalPath = await resolveAllowedCanonicalAttachmentPath({
		attachmentPath: params.attachmentPath,
		allowedRoots: params.allowedRoots
	});
	const canonicalStat = await fs.stat(canonicalPath);
	if (!canonicalStat.isFile()) throw new Error("attachment path is not a file");
	if (canonicalStat.size > params.maxBytes) throw new Error(`attachment exceeds ${Math.round(params.maxBytes / (1024 * 1024))}MB limit`);
	if (isHeicAttachment(params.attachmentPath, params.mimeType)) try {
		const convert = params.deps.convertHeicToJpeg;
		const converted = convert ? {
			buffer: await convert(canonicalPath, params.maxBytes),
			fileName: jpegFilenameForAttachment(params.attachmentPath)
		} : await loadWebMedia(canonicalPath, {
			maxBytes: params.maxBytes,
			localRoots: [path.dirname(canonicalPath)]
		});
		return {
			buffer: converted.buffer,
			contentType: "image/jpeg",
			originalFilename: converted.fileName ?? jpegFilenameForAttachment(params.attachmentPath)
		};
	} catch (err) {
		params.deps.logVerbose?.(`imessage: HEIC attachment conversion failed; staging original instead: ${String(err)}`);
	}
	return {
		buffer: await fs.readFile(canonicalPath),
		contentType: params.mimeType ?? void 0,
		originalFilename: path.basename(params.attachmentPath)
	};
}
async function stageIMessageAttachments(attachments, params) {
	const deps = params.deps ?? {};
	const save = deps.saveMediaBuffer ?? saveMediaBuffer;
	const staged = [];
	for (const attachment of attachments) {
		const attachmentPath = attachment.original_path?.trim();
		if (!attachmentPath || attachment.missing) continue;
		try {
			const media = await readAttachmentBuffer({
				attachmentPath,
				mimeType: attachment.mime_type,
				maxBytes: params.maxBytes,
				allowedRoots: params.allowedRoots,
				deps
			});
			const saved = await save(media.buffer, media.contentType, "inbound", params.maxBytes, media.originalFilename);
			staged.push({
				path: saved.path,
				contentType: saved.contentType
			});
		} catch (err) {
			deps.logVerbose?.(`imessage: failed to stage inbound attachment: ${String(err)}`);
		}
	}
	return staged;
}
//#endregion
//#region extensions/imessage/src/monitor/reaction-system-event.ts
function enqueueIMessageReactionSystemEvent(params) {
	const { decision, runtime } = params;
	const queued = enqueueSystemEvent(decision.text, {
		sessionKey: decision.route.sessionKey,
		contextKey: decision.contextKey
	});
	runtime.log?.(`imessage: reaction system event ${queued ? "queued" : "deduped"} session=${decision.route.sessionKey} target=${decision.reaction.targetGuid ?? "unknown"} action=${decision.reaction.action} emoji=${decision.reaction.emoji}`);
	params.logVerbose?.(`imessage: reaction event enqueued: ${decision.text}`);
	return queued;
}
//#endregion
//#region extensions/imessage/src/monitor/recovery-cursor.ts
const IMESSAGE_RECOVERY_CURSOR_NAMESPACE = "imessage.recovery-cursor";
const LEGACY_CATCHUP_CURSOR_NAMESPACE = "imessage.catchup-cursors";
const LEGACY_CATCHUP_CURSOR_MAX_ENTRIES = 256;
function openRecoveryCursorStore() {
	return getIMessageRuntime().state.openSyncKeyedStore({
		namespace: IMESSAGE_RECOVERY_CURSOR_NAMESPACE,
		maxEntries: 64
	});
}
function readRecoveryCursor(accountId) {
	try {
		const value = openRecoveryCursorStore().lookup(accountId);
		return typeof value?.lastRowid === "number" && Number.isFinite(value.lastRowid) ? value.lastRowid : null;
	} catch {
		return null;
	}
}
function migrateLegacyCatchupCursor(accountId) {
	try {
		const legacy = getIMessageRuntime().state.openSyncKeyedStore({
			namespace: LEGACY_CATCHUP_CURSOR_NAMESPACE,
			maxEntries: LEGACY_CATCHUP_CURSOR_MAX_ENTRIES
		});
		const key = createHash("sha256").update(accountId, "utf8").digest("hex").slice(0, 32);
		const value = legacy.consume(key);
		const rowid = typeof value?.lastSeenRowid === "number" && Number.isFinite(value.lastSeenRowid) ? value.lastSeenRowid : null;
		if (rowid !== null) advanceIMessageRecoveryCursor(accountId, rowid);
		return rowid;
	} catch {
		return null;
	}
}
/** Last dispatched rowid for this account, or null when none is recorded yet. */
function loadIMessageRecoveryCursor(accountId, options = {}) {
	const current = readRecoveryCursor(accountId);
	if (current !== null) return current;
	if (options.migrateLegacyCatchup === false) return null;
	return migrateLegacyCatchupCursor(accountId);
}
/** Advance the cursor forward to `rowid` (monotonic; never rewinds). */
function advanceIMessageRecoveryCursor(accountId, rowid) {
	if (!Number.isFinite(rowid)) return;
	try {
		const store = openRecoveryCursorStore();
		const current = store.lookup(accountId);
		if (current && current.lastRowid >= rowid) return;
		store.register(accountId, { lastRowid: rowid });
	} catch {}
}
//#endregion
//#region extensions/imessage/src/monitor/runtime.ts
function resolveRuntime(opts) {
	return opts.runtime ?? createNonExitingRuntime();
}
function normalizeAllowList(list) {
	return normalizeStringEntries(list);
}
//#endregion
//#region extensions/imessage/src/monitor/self-chat-cache.ts
const SELF_CHAT_TTL_MS = 1e4;
const SELF_CHAT_CREATED_AT_TOLERANCE_MS = 1e3;
const MAX_SELF_CHAT_CACHE_ENTRIES = 512;
const CLEANUP_MIN_INTERVAL_MS = 1e3;
function normalizeText(text) {
	if (!text) return null;
	const normalized = text.replace(/\r\n?/g, "\n").trim();
	return normalized ? normalized : null;
}
function isUsableTimestamp(createdAt) {
	return typeof createdAt === "number" && Number.isFinite(createdAt);
}
function digestText(text) {
	return createHash("sha256").update(text).digest("hex");
}
function buildScope(parts) {
	if (!parts.isGroup) return `${parts.accountId}:imessage:${parts.sender}`;
	const chatTarget = formatIMessageChatTarget(parts.chatId) || "chat_id:unknown";
	return `${parts.accountId}:${chatTarget}:imessage:${parts.sender}`;
}
var DefaultSelfChatCache = class {
	constructor() {
		this.cache = /* @__PURE__ */ new Map();
		this.insertionOrder = [];
		this.insertionOrderOffset = 0;
		this.entryCount = 0;
		this.lastCleanupAt = 0;
		this.nextEntryId = 1;
	}
	buildBucketKey(lookup) {
		const text = normalizeText(lookup.text);
		if (!text) return null;
		return `${buildScope(lookup)}:${digestText(text)}`;
	}
	remember(lookup) {
		const key = this.buildBucketKey(lookup);
		if (!key || !isUsableTimestamp(lookup.createdAt)) return;
		const entries = this.cache.get(key) ?? /* @__PURE__ */ new Map();
		const entry = {
			id: this.nextEntryId,
			createdAt: lookup.createdAt,
			createdAtSkewToleranceMs: lookup.allowCreatedAtSkew ? SELF_CHAT_CREATED_AT_TOLERANCE_MS : 0,
			rememberedAt: Date.now()
		};
		this.nextEntryId += 1;
		entries.set(entry.id, entry);
		this.cache.set(key, entries);
		this.insertionOrder.push({
			key,
			id: entry.id
		});
		this.entryCount += 1;
		this.maybeCleanup();
	}
	has(lookup) {
		this.maybeCleanup();
		const key = this.buildBucketKey(lookup);
		if (!key || !isUsableTimestamp(lookup.createdAt)) return false;
		const entries = this.cache.get(key);
		if (!entries) return false;
		const now = Date.now();
		const createdAt = lookup.createdAt;
		return [...entries.values()].some((entry) => {
			const createdAtDelta = Math.abs(entry.createdAt - createdAt);
			return now - entry.rememberedAt <= SELF_CHAT_TTL_MS && (createdAtDelta === 0 || createdAtDelta < entry.createdAtSkewToleranceMs);
		});
	}
	maybeCleanup() {
		const now = Date.now();
		if (now - this.lastCleanupAt < CLEANUP_MIN_INTERVAL_MS) return;
		this.lastCleanupAt = now;
		for (const [key, entries] of this.cache.entries()) {
			for (const [id, entry] of entries.entries()) if (now - entry.rememberedAt > SELF_CHAT_TTL_MS) {
				entries.delete(id);
				this.entryCount -= 1;
			}
			if (entries.size === 0) this.cache.delete(key);
		}
		while (this.entryCount > MAX_SELF_CHAT_CACHE_ENTRIES && this.insertionOrderOffset < this.insertionOrder.length) {
			const oldest = this.insertionOrder[this.insertionOrderOffset];
			this.insertionOrderOffset += 1;
			const entries = this.cache.get(oldest.key);
			if (!entries) continue;
			if (!entries.delete(oldest.id)) continue;
			this.entryCount -= 1;
			if (entries.size === 0) this.cache.delete(oldest.key);
		}
		this.compactInsertionOrder();
	}
	compactInsertionOrder() {
		if (this.insertionOrderOffset <= 1024 && this.insertionOrder.length <= this.entryCount + 1024) return;
		this.insertionOrder = this.insertionOrder.slice(this.insertionOrderOffset).filter((entry) => this.cache.get(entry.key)?.has(entry.id));
		this.insertionOrderOffset = 0;
	}
};
function createSelfChatCache() {
	return new DefaultSelfChatCache();
}
//#endregion
//#region extensions/imessage/src/monitor/watch-error-log.ts
const MAX_WATCH_ERROR_MESSAGE_CHARS = 200;
function sanitizeIMessageWatchErrorPayload(payload) {
	if (!isRecord(payload)) return {};
	const safe = {};
	if (typeof payload.code === "number" && Number.isFinite(payload.code)) safe.code = payload.code;
	if (typeof payload.message === "string") {
		const sanitizedMessage = sanitizeTerminalText(payload.message);
		if (sanitizedMessage) safe.message = sanitizedMessage.length > MAX_WATCH_ERROR_MESSAGE_CHARS ? `${truncateUtf16Safe(sanitizedMessage, MAX_WATCH_ERROR_MESSAGE_CHARS - 1)}…` : sanitizedMessage;
	}
	return safe;
}
//#endregion
//#region extensions/imessage/src/monitor/monitor-provider.ts
const WATCH_SUBSCRIBE_MAX_ATTEMPTS = 3;
const WATCH_SUBSCRIBE_RETRY_DELAY_MS = 1e3;
const APPROVAL_REACTION_POLL_INTERVAL_MS = 2e3;
const APPROVAL_REACTION_DISCOVERY_INTERVAL_MS = 6e4;
const IMESSAGE_TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
const IMESSAGE_TYPING_KEEPALIVE_MAX_DURATION_MS = 10 * 6e4;
function resolveConfiguredIMessageTypingMode(cfg) {
	return cfg.session?.typingMode ?? cfg.agents?.defaults?.typingMode;
}
function isIMessagePluginPayloadAttachment(attachment) {
	const attachmentPath = attachment.original_path?.trim().toLowerCase() ?? "";
	const transferName = attachment.transfer_name?.trim().toLowerCase() ?? "";
	const uti = attachment.uti?.trim().toLowerCase() ?? "";
	return attachmentPath.endsWith(".pluginpayloadattachment") || transferName.endsWith(".pluginpayloadattachment") || uti === "com.apple.messages.pluginpayloadattachment";
}
async function detectRemoteHostFromCliPath(cliPath) {
	try {
		const expanded = cliPath.startsWith("~") ? cliPath.replace(/^~/, process.env.HOME ?? "") : cliPath;
		const content = await fs.readFile(expanded, "utf8");
		const userHostMatch = content.match(/\bssh\b[^\n]*?\s+([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+)/);
		if (userHostMatch) return userHostMatch[1];
		return content.match(/\bssh\b[^\n]*?\s+([a-zA-Z][a-zA-Z0-9._-]*)\s+\S*\bimsg\b/)?.[1];
	} catch (err) {
		const code = err?.code;
		if (code !== "ENOENT" && code !== "ENOTDIR") logVerbose(`imessage: failed to inspect cliPath ${cliPath} for remoteHost detection: ${String(err)}`);
		return;
	}
}
function resolveLocalMessagesHomeDir() {
	const home = process.env.HOME?.trim();
	if (home) return home;
	try {
		return os.homedir().trim() || void 0;
	} catch {
		return;
	}
}
function resolveLocalMessagesDbPath(dbPath) {
	if (!dbPath.startsWith("~")) return dbPath;
	const home = resolveLocalMessagesHomeDir();
	return home ? path.join(home, dbPath.slice(1).replace(/^\/+/, "")) : dbPath;
}
function resolveIMessageWatchSourceDbPath(params) {
	if (params.remoteHost) return;
	const configured = params.dbPath?.trim();
	if (configured) return configured;
	const cliPath = params.cliPath.trim();
	if (cliPath !== "imsg" && path.basename(cliPath) !== "imsg") return;
	const home = resolveLocalMessagesHomeDir();
	return home ? path.join(home, "Library", "Messages", "chat.db") : void 0;
}
async function resolveIMessageStartupRowidWatermark(dbPath) {
	const resolvedDbPath = resolveLocalMessagesDbPath(dbPath);
	let database;
	try {
		const { DatabaseSync } = await import("node:sqlite");
		database = new DatabaseSync(resolvedDbPath, { readOnly: true });
		const row = database.prepare("SELECT MAX(ROWID) AS maxRowid FROM message").get();
		return typeof row?.maxRowid === "number" && Number.isFinite(row.maxRowid) ? row.maxRowid : null;
	} catch (err) {
		logVerbose(`imessage: startup rowid watermark unavailable for db=${dbPath}: ${String(err)}`);
		return null;
	} finally {
		database?.close();
	}
}
const warnIfImsgUpgradeNeeded = (() => {
	let fired = false;
	return { fireOnce: (rpcMethods, runtime) => {
		if (fired) return;
		fired = true;
		const detail = rpcMethods.length === 0 ? "imsg build pre-dates the rpc_methods capability list" : `imsg rpc_methods=[${rpcMethods.join(", ")}] does not include typing/read`;
		runtime.log?.(warn(`imessage: typing indicators / read receipts gated off (${detail}). Upgrade imsg (current bridge needs typing+read in rpc_methods).`));
	} };
})();
function isRetriableWatchSubscribeStartupError(error) {
	return /imsg rpc timeout \(watch\.subscribe\)|imsg rpc (closed|exited|not running)/i.test(String(error));
}
const IMESSAGE_DIAGNOSTIC_DROP_REASONS = new Set([
	"agent echo in self-chat",
	"echo",
	"from me",
	"reflected assistant content",
	"self-chat echo"
]);
const IMESSAGE_THROTTLED_DIAGNOSTIC_DROP_REASONS = new Set(["from me"]);
function shouldThrottleIMessageInboundDropDiagnostic(reason) {
	return IMESSAGE_THROTTLED_DIAGNOSTIC_DROP_REASONS.has(reason);
}
function describeIMessageInboundDropDiagnostic(params) {
	if (!IMESSAGE_DIAGNOSTIC_DROP_REASONS.has(params.reason)) return null;
	const messageId = typeof params.message.id === "number" || typeof params.message.id === "string" ? String(params.message.id) : "unknown";
	return `imessage: dropped inbound message account=${params.accountId} reason=${JSON.stringify(params.reason)} chat_id=${params.message.chat_id ?? "unknown"} group=${params.message.is_group === true} message_id=${messageId} guid=${params.message.guid ? "present" : "missing"} created_at=${params.message.created_at ?? "unknown"}`;
}
function describeIMessageWatchSubscribeStartupFailure(params) {
	const retry = params.retryDelayMs !== void 0 ? ` retry_in_ms=${params.retryDelayMs}` : "";
	return `imessage: watch.subscribe startup failed attempt=${params.attempt}/${params.maxAttempts} account=${params.accountId} cliPath=${params.cliPath} dbPath=${params.dbPath ? "configured" : "default"} remoteHost=${params.remoteHost ? "configured" : "none"} timeoutMs=${params.probeTimeoutMs} since_rowid=${params.watchSinceRowid ?? "none"} attachments=${params.includeAttachments} include_reactions=true${retry}: ${String(params.error)}`;
}
async function waitForWatchSubscribeRetryDelay(params) {
	if (params.ms <= 0) return;
	await new Promise((resolve) => {
		const timer = setTimeout(() => {
			params.abortSignal?.removeEventListener("abort", onAbort);
			resolve();
		}, params.ms);
		const onAbort = () => {
			clearTimeout(timer);
			params.abortSignal?.removeEventListener("abort", onAbort);
			resolve();
		};
		params.abortSignal?.addEventListener("abort", onAbort, { once: true });
	});
}
async function monitorIMessageProvider(opts = {}) {
	const runtime = resolveRuntime(opts);
	const cfg = opts.config ?? getRuntimeConfig();
	const accountInfo = resolveIMessageAccount({
		cfg,
		accountId: opts.accountId
	});
	const imessageCfg = accountInfo.config;
	const historyLimit = Math.max(0, imessageCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 50);
	const groupHistories = /* @__PURE__ */ new Map();
	const sentMessageCache = createSentMessageCache();
	const selfChatCache = createSelfChatCache();
	const loopRateLimiter = createLoopRateLimiter();
	const textLimit = resolveTextChunkLimit(cfg, "imessage", accountInfo.accountId);
	const allowFrom = normalizeAllowList(opts.allowFrom ?? imessageCfg.allowFrom);
	const configuredGroupAllowFrom = opts.groupAllowFrom ?? imessageCfg.groupAllowFrom;
	const groupAllowFrom = normalizeAllowList(configuredGroupAllowFrom ?? (imessageCfg.allowFrom && imessageCfg.allowFrom.length > 0 ? imessageCfg.allowFrom : []));
	const allowLegacyConversationAllowFromForGroup = configuredGroupAllowFrom == null;
	const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
	const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
		providerConfigPresent: cfg.channels?.imessage !== void 0,
		groupPolicy: imessageCfg.groupPolicy,
		defaultGroupPolicy
	});
	warnMissingProviderGroupPolicyFallbackOnce({
		providerMissingFallbackApplied,
		providerKey: "imessage",
		accountId: accountInfo.accountId,
		log: (message) => runtime.log?.(warn(message))
	});
	warnGroupAllowlistMisconfigOnce({
		groupPolicy,
		groups: imessageCfg.groups,
		accountId: accountInfo.accountId,
		log: (message) => runtime.log?.(warn(message))
	});
	const dmPolicy = imessageCfg.dmPolicy ?? "pairing";
	const catchupCfg = resolveCatchupConfig(imessageCfg.catchup);
	const includeAttachments = opts.includeAttachments ?? imessageCfg.includeAttachments ?? false;
	const mediaMaxBytes = (opts.mediaMaxMb ?? imessageCfg.mediaMaxMb ?? 16) * 1024 * 1024;
	const cliPath = opts.cliPath ?? imessageCfg.cliPath ?? "imsg";
	const dbPath = opts.dbPath ?? imessageCfg.dbPath;
	const probeTimeoutMs = imessageCfg.probeTimeoutMs ?? 1e4;
	const attachmentRoots = resolveIMessageAttachmentRoots({
		cfg,
		accountId: accountInfo.accountId
	});
	const remoteAttachmentRoots = resolveIMessageRemoteAttachmentRoots({
		cfg,
		accountId: accountInfo.accountId
	});
	const configuredRemoteHost = normalizeScpRemoteHost(imessageCfg.remoteHost);
	if (imessageCfg.remoteHost && !configuredRemoteHost) logVerbose("imessage: ignoring unsafe channels.imessage.remoteHost value");
	let remoteHost = configuredRemoteHost;
	if (!remoteHost && cliPath && cliPath !== "imsg") {
		const detected = await detectRemoteHostFromCliPath(cliPath);
		const normalizedDetected = normalizeScpRemoteHost(detected);
		if (detected && !normalizedDetected) logVerbose("imessage: ignoring unsafe auto-detected remoteHost from cliPath");
		remoteHost = normalizedDetected;
		if (remoteHost) logVerbose(`imessage: detected remoteHost=${remoteHost} from cliPath`);
	}
	const inboundReplayGuard = createIMessageInboundReplayGuard();
	let staleBacklogSuppressed = 0;
	const loggedThrottledDropDiagnostics = /* @__PURE__ */ new Set();
	const watchSourceDbPath = resolveIMessageWatchSourceDbPath({
		cliPath,
		dbPath,
		remoteHost
	});
	const recoveryBoundaryRowid = watchSourceDbPath ? await resolveIMessageStartupRowidWatermark(watchSourceDbPath) : null;
	const recoveryCursorRowid = loadIMessageRecoveryCursor(accountInfo.accountId, { migrateLegacyCatchup: !catchupCfg.enabled });
	const watchSinceRowid = catchupCfg.enabled ? null : recoveryCursorRowid !== null ? recoveryBoundaryRowid !== null ? Math.max(recoveryCursorRowid, recoveryBoundaryRowid - 500) : recoveryCursorRowid : recoveryBoundaryRowid;
	const coalesceSameSenderDms = imessageCfg.coalesceSameSenderDms === true;
	const inboundCfg = cfg.messages?.inbound;
	const hasExplicitInboundDebounce = typeof inboundCfg?.debounceMs === "number" || typeof inboundCfg?.byChannel?.imessage === "number";
	const debounceMsOverride = coalesceSameSenderDms && !hasExplicitInboundDebounce ? 2500 : void 0;
	let imsgEmitsBalloonMetadata = false;
	let recoveryCursorHoldBeforeRowid = null;
	let latestAdvancedRecoveryCursorRowid = recoveryCursorRowid ?? -1;
	const pendingRecoveryReplayRowids = /* @__PURE__ */ new Set();
	const handledRecoveryCursorRowids = /* @__PURE__ */ new Set();
	function collectFiniteRowids(entries) {
		const rowids = [];
		for (const entry of entries) if (typeof entry.message.id === "number" && Number.isFinite(entry.message.id)) rowids.push(entry.message.id);
		return rowids;
	}
	function holdRecoveryCursorBeforeFailedRows(entries) {
		if (catchupCfg.enabled || recoveryCursorRowid === null) return;
		if (recoveryBoundaryRowid === null) return;
		const failedReplayRowids = collectFiniteRowids(entries).filter((rowid) => rowid <= recoveryBoundaryRowid);
		if (failedReplayRowids.length === 0) return;
		const firstFailedRowid = Math.min(...failedReplayRowids);
		for (const rowid of failedReplayRowids) pendingRecoveryReplayRowids.delete(rowid);
		recoveryCursorHoldBeforeRowid = recoveryCursorHoldBeforeRowid === null ? firstFailedRowid : Math.min(recoveryCursorHoldBeforeRowid, firstFailedRowid);
	}
	function trackPendingRecoveryReplayRow(message) {
		if (catchupCfg.enabled || recoveryCursorRowid === null || recoveryBoundaryRowid === null) return;
		if (typeof message.id === "number" && Number.isFinite(message.id) && message.id <= recoveryBoundaryRowid) pendingRecoveryReplayRowids.add(message.id);
	}
	function minSetValue(values) {
		let min = null;
		for (const value of values) min = min === null ? value : Math.min(min, value);
		return min;
	}
	function resolveRecoveryCursorHoldFloor() {
		const pendingFloor = minSetValue(pendingRecoveryReplayRowids);
		if (pendingFloor === null) return recoveryCursorHoldBeforeRowid;
		if (recoveryCursorHoldBeforeRowid === null) return pendingFloor;
		return Math.min(pendingFloor, recoveryCursorHoldBeforeRowid);
	}
	function advanceRecoveryCursorAfterHandled(entries) {
		if (catchupCfg.enabled) return;
		const rowids = collectFiniteRowids(entries);
		if (rowids.length === 0) return;
		for (const rowid of rowids) {
			pendingRecoveryReplayRowids.delete(rowid);
			handledRecoveryCursorRowids.add(rowid);
		}
		const maxHandledRowid = Math.max(...handledRecoveryCursorRowids);
		const holdFloor = resolveRecoveryCursorHoldFloor();
		const nextCursorRowid = holdFloor !== null && maxHandledRowid >= holdFloor ? holdFloor - 1 : maxHandledRowid;
		if (nextCursorRowid >= 0 && nextCursorRowid > latestAdvancedRecoveryCursorRowid) {
			advanceIMessageRecoveryCursor(accountInfo.accountId, nextCursorRowid);
			latestAdvancedRecoveryCursorRowid = nextCursorRowid;
			for (const rowid of handledRecoveryCursorRowids) if (rowid <= nextCursorRowid) handledRecoveryCursorRowids.delete(rowid);
		}
	}
	const { debouncer: inboundDebouncer } = createChannelInboundDebouncer({
		cfg,
		channel: "imessage",
		debounceMsOverride,
		buildKey: (entry) => {
			const msg = entry.message;
			const sender = msg.sender?.trim();
			if (!sender) return null;
			const conversationId = msg.chat_id != null ? `chat:${msg.chat_id}` : msg.chat_guid ?? msg.chat_identifier ?? "unknown";
			if (coalesceSameSenderDms && msg.is_group !== true) return `imessage:${accountInfo.accountId}:dm:${conversationId}:${sender}`;
			return `imessage:${accountInfo.accountId}:${conversationId}:${sender}`;
		},
		shouldDebounce: (entry) => {
			const msg = entry.message;
			if (resolveIMessageReactionContext(msg, (msg.text ?? "").trim())) return false;
			if (msg.is_from_me === true) return false;
			if (coalesceSameSenderDms) return msg.is_group !== true;
			return shouldDebounceTextInbound({
				text: msg.text,
				cfg,
				hasMedia: Boolean(msg.attachments?.some((attachment) => !isIMessagePluginPayloadAttachment(attachment)))
			});
		},
		onFlush: async (entries) => {
			if (entries.length === 0) return;
			const dispatchUnit = async (unitEntries, message) => {
				const keys = unitEntries.map((entry) => entry.replayKey).filter((key) => key !== null);
				try {
					await handleMessageNow(message);
					await commitIMessageInboundReplay({
						guard: inboundReplayGuard,
						accountId: accountInfo.accountId,
						keys
					});
					advanceRecoveryCursorAfterHandled(unitEntries);
				} catch (err) {
					holdRecoveryCursorBeforeFailedRows(unitEntries);
					releaseIMessageInboundReplay({
						guard: inboundReplayGuard,
						accountId: accountInfo.accountId,
						keys,
						error: err
					});
					runtime.error?.(`imessage: inbound dispatch failed: ${String(err)}`);
				}
			};
			if (entries.length === 1) {
				await dispatchUnit(entries, entries[0].message);
				return;
			}
			const messages = entries.map((e) => e.message);
			if (!shouldCombineIMessagePayloadBucket(messages, imsgEmitsBalloonMetadata)) {
				for (const entry of entries) await dispatchUnit([entry], entry.message);
				return;
			}
			const combined = combineIMessagePayloads(messages);
			if (shouldLogVerbose()) {
				const text = combined.text ?? "";
				const preview = text.slice(0, 50);
				const ellipsis = text.length > 50 ? "..." : "";
				logVerbose(`[imessage] coalesced ${entries.length} messages: "${preview}${ellipsis}"`);
			}
			await dispatchUnit(entries, combined);
		},
		onError: (err) => {
			runtime.error?.(`imessage debounce flush failed: ${String(err)}`);
		}
	});
	let client;
	let detachAbortHandler = () => {};
	let liveCatchupCursorAdvanceEnabled = false;
	let startupCatchupInProgress = false;
	const pendingLiveCatchupCursorAdvances = [];
	const getActiveClient = () => {
		if (!client) throw new Error("imessage monitor client not initialized");
		return client;
	};
	async function repairMessageConversationAnchor(message) {
		return await repairIMessageConversationAnchor({
			client: getActiveClient(),
			message,
			runtime
		});
	}
	function resolveLiveCatchupCursor(message) {
		const coalescedCursor = message.coalescedCatchupCursor;
		const rowid = typeof coalescedCursor?.lastSeenRowid === "number" && Number.isFinite(coalescedCursor.lastSeenRowid) ? coalescedCursor.lastSeenRowid : typeof message.id === "number" && Number.isFinite(message.id) ? message.id : null;
		const dateMs = typeof coalescedCursor?.lastSeenMs === "number" && Number.isFinite(coalescedCursor.lastSeenMs) ? coalescedCursor.lastSeenMs : typeof message.created_at === "string" ? Date.parse(message.created_at) : NaN;
		if (rowid === null || !Number.isFinite(dateMs)) return null;
		return {
			lastSeenMs: dateMs,
			lastSeenRowid: rowid
		};
	}
	async function maybeAdvanceLiveCatchupCursor(message) {
		if (!catchupCfg.enabled) return;
		const cursor = resolveLiveCatchupCursor(message);
		if (!cursor) return;
		if (!liveCatchupCursorAdvanceEnabled) {
			if (startupCatchupInProgress) pendingLiveCatchupCursorAdvances.push(cursor);
			return;
		}
		try {
			await advanceIMessageCatchupCursor(accountInfo.accountId, cursor, catchupCfg);
		} catch (err) {
			runtime.error?.(`imessage catchup: failed to advance live cursor: ${String(err)}`);
		}
	}
	async function flushPendingLiveCatchupCursorAdvances() {
		for (const cursor of pendingLiveCatchupCursorAdvances.splice(0)) try {
			await advanceIMessageCatchupCursor(accountInfo.accountId, cursor, catchupCfg);
		} catch (err) {
			runtime.error?.(`imessage catchup: failed to advance pending live cursor: ${String(err)}`);
		}
	}
	async function handleMessageNow(message, options = {}) {
		await handleMessageNowInner(message);
		if (options.advanceCatchupCursor !== false) await maybeAdvanceLiveCatchupCursor(message);
	}
	function resolveIMessageInboundBodyText(message) {
		const messageText = (message.text ?? "").trim();
		const attachments = includeAttachments ? message.attachments ?? [] : [];
		const effectiveAttachmentRoots = remoteHost ? remoteAttachmentRoots : attachmentRoots;
		const validAttachments = attachments.filter((entry) => {
			if (isIMessagePluginPayloadAttachment(entry)) return false;
			const attachmentPath = entry?.original_path?.trim();
			if (!attachmentPath || entry?.missing) return false;
			if (isInboundPathAllowed({
				filePath: attachmentPath,
				roots: effectiveAttachmentRoots
			})) return true;
			logVerbose(`imessage: dropping inbound attachment outside allowed roots: ${attachmentPath}`);
			return false;
		});
		const rawMediaAttachments = validAttachments.flatMap((a) => {
			const attachmentPath = a.original_path?.trim();
			return attachmentPath ? [{
				path: attachmentPath,
				contentType: a.mime_type ?? void 0
			}] : [];
		});
		const placeholderMediaType = rawMediaAttachments[0]?.contentType;
		const kind = kindFromMime(placeholderMediaType ?? void 0);
		const placeholder = kind ? `<media:${kind}>` : validAttachments.length ? "<media:attachment>" : "";
		return {
			messageText,
			bodyText: messageText || placeholder,
			validAttachments,
			rawMediaAttachments,
			effectiveAttachmentRoots
		};
	}
	async function handleMessageNowInner(rawMessage) {
		const message = await repairMessageConversationAnchor(rawMessage);
		if (!message) return;
		const { messageText, bodyText, validAttachments, rawMediaAttachments, effectiveAttachmentRoots } = resolveIMessageInboundBodyText(message);
		if (await maybeResolveIMessageApprovalReaction({
			cfg,
			accountId: accountInfo.accountId,
			message,
			bodyText,
			logVerboseMessage: logVerbose
		})) return;
		const storeAllowFrom = await readChannelAllowFromStore("imessage", process.env, accountInfo.accountId).catch(() => []);
		const decision = await resolveIMessageInboundDecision({
			cfg,
			accountId: accountInfo.accountId,
			message,
			opts,
			messageText,
			bodyText,
			allowFrom,
			groupAllowFrom,
			allowLegacyConversationAllowFromForGroup,
			groupPolicy,
			dmPolicy,
			storeAllowFrom,
			historyLimit,
			groupHistories,
			echoCache: sentMessageCache,
			selfChatCache,
			reactionNotifications: imessageCfg.reactionNotifications,
			logVerbose
		});
		const chatId = message.chat_id ?? void 0;
		const senderForKey = (message.sender ?? "").trim();
		const conversationKey = chatId != null ? `group:${chatId}` : `dm:${senderForKey}`;
		const rateLimitKey = `${accountInfo.accountId}:${conversationKey}`;
		if (decision.kind === "drop") {
			if (decision.reason === "echo" || decision.reason === "self-chat echo" || decision.reason === "reflected assistant content" || decision.reason === "from me") loopRateLimiter.record(rateLimitKey);
			const diagnostic = describeIMessageInboundDropDiagnostic({
				accountId: accountInfo.accountId,
				reason: decision.reason,
				message
			});
			if (diagnostic) {
				const throttleKey = `${rateLimitKey}:${decision.reason}`;
				const shouldThrottleDiagnostic = shouldThrottleIMessageInboundDropDiagnostic(decision.reason);
				if (!shouldThrottleDiagnostic || !loggedThrottledDropDiagnostics.has(throttleKey)) {
					if (shouldThrottleDiagnostic) loggedThrottledDropDiagnostics.add(throttleKey);
					runtime.log?.(warn(diagnostic));
				}
			}
			if (decision.reason === "group id not in allowlist") warnGroupAllowlistDropPerChatOnce({
				accountId: accountInfo.accountId,
				chatId: message.chat_id ?? void 0,
				log: (msg) => runtime.log?.(warn(msg))
			});
			return;
		}
		if (decision.kind === "dispatch" && loopRateLimiter.isRateLimited(rateLimitKey)) {
			logVerbose(`imessage: rate-limited conversation ${conversationKey} (echo loop detected)`);
			return;
		}
		if (decision.kind === "pairing") {
			const sender = (message.sender ?? "").trim();
			if (!sender) return;
			await createChannelPairingChallengeIssuer({
				channel: "imessage",
				upsertPairingRequest: async ({ id, meta }) => await upsertChannelPairingRequest({
					channel: "imessage",
					id,
					accountId: accountInfo.accountId,
					meta
				})
			})({
				senderId: decision.senderId,
				senderIdLine: `Your iMessage sender id: ${decision.senderId}`,
				meta: {
					sender: decision.senderId,
					chatId: chatId ? String(chatId) : void 0
				},
				onCreated: () => {
					logVerbose(`imessage pairing request sender=${decision.senderId}`);
				},
				sendPairingReply: async (text) => {
					await sendMessageIMessage(sender, text, {
						config: cfg,
						client: getActiveClient(),
						maxBytes: mediaMaxBytes,
						accountId: accountInfo.accountId,
						...chatId ? { chatId } : {}
					});
				},
				onReplyError: (err) => {
					runtime.error?.(`imessage pairing reply failed for ${decision.senderId}: ${String(err)}`);
				}
			});
			return;
		}
		if (decision.kind === "reaction") {
			enqueueIMessageReactionSystemEvent({
				decision,
				runtime,
				logVerbose
			});
			return;
		}
		const storePath = resolveStorePath(cfg.session?.store, { agentId: decision.route.agentId });
		const stagedAttachments = remoteHost ? [] : await stageIMessageAttachments(validAttachments, {
			maxBytes: mediaMaxBytes,
			allowedRoots: effectiveAttachmentRoots,
			deps: { logVerbose }
		});
		const mediaAttachments = remoteHost ? rawMediaAttachments : stagedAttachments;
		const firstAttachment = mediaAttachments[0];
		const mediaPath = firstAttachment?.path ?? void 0;
		const mediaType = firstAttachment?.contentType ?? void 0;
		const mediaPaths = mediaAttachments.map((a) => a.path).filter(Boolean);
		const mediaTypes = mediaAttachments.map((a) => a.contentType ?? void 0);
		const previousTimestamp = readSessionUpdatedAt({
			storePath,
			sessionKey: decision.route.sessionKey
		});
		const dmHistoryLimit = !decision.isGroup ? resolveIMessageDmHistoryLimit({
			config: imessageCfg,
			sender: decision.sender,
			senderNormalized: decision.senderNormalized
		}) : 0;
		const dmHistory = !decision.isGroup && dmHistoryLimit > 0 && !previousTimestamp ? await resolveIMessageDmHistoryContext({
			client: getActiveClient(),
			message,
			senderNormalized: decision.senderNormalized,
			limit: dmHistoryLimit,
			envelopeOptions: resolveEnvelopeFormatOptions(cfg),
			logVerbose
		}) : void 0;
		const { ctxPayload, chatTarget, imessageTo } = await buildIMessageInboundContext({
			cfg,
			decision,
			message,
			previousTimestamp,
			remoteHost,
			historyLimit,
			groupHistories,
			dmHistory,
			media: {
				path: mediaPath,
				type: mediaType,
				paths: mediaPaths,
				types: mediaTypes
			}
		});
		const updateTarget = chatTarget || imessageTo;
		const pinnedMainDmOwner = resolvePinnedMainDmOwnerFromAllowlist({
			dmScope: cfg.session?.dmScope,
			allowFrom,
			normalizeEntry: normalizeIMessageHandle
		});
		if (shouldLogVerbose()) {
			const preview = truncateUtf16Safe(ctxPayload.Body ?? "", 200).replace(/\n/g, "\\n");
			logVerbose(`imessage inbound: chatId=${chatId ?? "unknown"} from=${ctxPayload.From} len=${(ctxPayload.Body ?? "").length} preview="${preview}"`);
		}
		const privateApiStatus = getCachedIMessagePrivateApiStatus(cliPath);
		const supportsTyping = imessageRpcSupportsMethod(privateApiStatus, "typing");
		const supportsRead = imessageRpcSupportsMethod(privateApiStatus, "read");
		if (privateApiStatus?.available === true) {
			if (!supportsTyping || !supportsRead) warnIfImsgUpgradeNeeded.fireOnce(privateApiStatus.rpcMethods, runtime);
		}
		const sendReadReceipts = imessageCfg.sendReadReceipts !== false;
		const typingTarget = ctxPayload.To;
		if (supportsRead && sendReadReceipts && typingTarget) try {
			await markIMessageChatRead(typingTarget, {
				cfg,
				accountId: accountInfo.accountId,
				client: getActiveClient()
			});
		} catch (err) {
			runtime.error?.(`imessage: mark read failed: ${String(err)}`);
		}
		const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
			cfg,
			agentId: decision.route.agentId,
			channel: "imessage",
			accountId: decision.route.accountId,
			typing: supportsTyping && typingTarget ? {
				start: async () => {
					await sendIMessageTyping(typingTarget, true, {
						cfg,
						accountId: accountInfo.accountId,
						client: getActiveClient()
					});
				},
				stop: async () => {
					await sendIMessageTyping(typingTarget, false, {
						cfg,
						accountId: accountInfo.accountId,
						client: getActiveClient()
					});
				},
				keepaliveIntervalMs: IMESSAGE_TYPING_KEEPALIVE_INTERVAL_MS,
				maxDurationMs: IMESSAGE_TYPING_KEEPALIVE_MAX_DURATION_MS,
				onStartError: (err) => {
					logTypingFailure({
						log: (msg) => logVerbose(msg),
						channel: "imessage",
						action: "start",
						target: typingTarget,
						error: err
					});
				},
				onStopError: (err) => {
					logTypingFailure({
						log: (msg) => logVerbose(msg),
						channel: "imessage",
						action: "stop",
						target: typingTarget,
						error: err
					});
				}
			} : void 0
		});
		const { dispatcher, replyOptions: typingReplyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
			...replyPipeline,
			humanDelay: resolveHumanDelayConfig(cfg, decision.route.agentId),
			deliver: async (payload, info) => {
				const target = ctxPayload.To;
				if (!target) {
					runtime.error?.(danger("imessage: missing delivery target"));
					return;
				}
				const durable = await deliverInboundReplyWithMessageSendContext({
					cfg,
					channel: "imessage",
					accountId: accountInfo.accountId,
					agentId: decision.route.agentId,
					ctxPayload,
					payload,
					info,
					to: target,
					deps: { imessage: createIMessageEchoCachingSend({
						accountId: accountInfo.accountId,
						sentMessageCache
					}) }
				});
				if (durable.status === "failed") throw durable.error;
				if (durable.status === "handled_visible" || durable.status === "handled_no_send") return;
				await deliverReplies({
					cfg,
					replies: [payload],
					target,
					accountId: accountInfo.accountId,
					runtime,
					maxBytes: mediaMaxBytes,
					textLimit,
					sentMessageCache
				});
			},
			onError: (err, info) => {
				runtime.error?.(danger(`imessage ${info.kind} reply failed: ${String(err)}`));
			}
		});
		let directTypingController;
		const configuredTypingMode = resolveConfiguredIMessageTypingMode(cfg);
		const sendPolicy = resolveSendPolicy({
			cfg,
			entry: getSessionEntry({
				storePath,
				sessionKey: decision.route.sessionKey
			}),
			sessionKey: decision.route.sessionKey,
			channel: "imessage",
			chatType: decision.isGroup ? "group" : "direct"
		});
		const directToolTypingOptions = !decision.isGroup && sendPolicy !== "deny" && (configuredTypingMode === void 0 || configuredTypingMode === "instant") ? {
			suppressDefaultToolProgressMessages: true,
			allowProgressCallbacksWhenSourceDeliverySuppressed: true,
			onTypingController: (typing) => {
				directTypingController = typing;
				typingReplyOptions.onTypingController?.(typing);
			},
			onToolStart: async () => {
				await directTypingController?.startTypingLoop();
			}
		} : {};
		const configuredBlockStreaming = resolveChannelStreamingBlockEnabled(accountInfo.config);
		const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
			route: decision.route,
			sessionKey: decision.route.sessionKey
		});
		await runChannelInboundEvent({
			channel: "imessage",
			accountId: decision.route.accountId,
			raw: decision,
			adapter: {
				ingest: () => ({
					id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
					timestamp: typeof ctxPayload.Timestamp === "number" ? ctxPayload.Timestamp : void 0,
					rawText: ctxPayload.RawBody ?? "",
					textForAgent: ctxPayload.BodyForAgent,
					textForCommands: ctxPayload.CommandBody,
					raw: decision
				}),
				resolveTurn: () => ({
					channel: "imessage",
					accountId: decision.route.accountId,
					routeSessionKey: decision.route.sessionKey,
					storePath,
					ctxPayload,
					recordInboundSession,
					record: {
						updateLastRoute: !decision.isGroup && updateTarget ? {
							sessionKey: inboundLastRouteSessionKey,
							channel: "imessage",
							to: updateTarget,
							accountId: decision.route.accountId,
							mainDmOwnerPin: inboundLastRouteSessionKey === decision.route.mainSessionKey && pinnedMainDmOwner && decision.senderNormalized ? {
								ownerRecipient: pinnedMainDmOwner,
								senderRecipient: decision.senderNormalized,
								onSkip: ({ ownerRecipient, senderRecipient }) => {
									logVerbose(`imessage: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
								}
							} : void 0
						} : void 0,
						onRecordError: (err) => {
							logVerbose(`imessage: failed updating session meta: ${String(err)}`);
						}
					},
					history: {
						isGroup: decision.isGroup,
						historyKey: decision.historyKey,
						historyMap: groupHistories,
						limit: historyLimit
					},
					onPreDispatchFailure: () => settleReplyDispatcher({
						dispatcher,
						onSettled: () => markDispatchIdle()
					}),
					runDispatch: async () => {
						try {
							return await dispatchInboundMessage({
								ctx: ctxPayload,
								cfg,
								dispatcher,
								replyOptions: {
									...typingReplyOptions,
									disableBlockStreaming: typeof configuredBlockStreaming === "boolean" ? !configuredBlockStreaming : void 0,
									onModelSelected,
									...directToolTypingOptions
								}
							});
						} finally {
							markDispatchIdle();
						}
					}
				})
			}
		});
	}
	const handleMessage = async (raw) => {
		const message = parseIMessageNotification(raw);
		if (!message) {
			const shape = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.keys(raw).toSorted().join(",") : typeof raw;
			runtime.error?.(`imessage: dropping malformed RPC message payload (keys=${shape})`);
			return;
		}
		if (!imsgEmitsBalloonMetadata && hasIMessageBalloonMetadata(message)) imsgEmitsBalloonMetadata = true;
		const isRecoveryReplay = recoveryCursorRowid !== null && recoveryBoundaryRowid !== null && typeof message.id === "number" && message.id <= recoveryBoundaryRowid;
		const staleThresholdMs = isRecoveryReplay ? IMESSAGE_RECOVERY_MAX_AGE_MS : IMESSAGE_STALE_INBOUND_THRESHOLD_MS;
		if (isStaleIMessageBacklog(message, Date.now(), staleThresholdMs)) {
			staleBacklogSuppressed += 1;
			runtime.log?.(warn(`imessage: suppressed stale inbound backlog account=${accountInfo.accountId} sent=${message.created_at ?? "unknown"} recovery=${isRecoveryReplay} (${staleBacklogSuppressed} suppressed since start)`));
			const suppressedKey = buildIMessageInboundReplayKey({
				accountId: accountInfo.accountId,
				message
			});
			if (suppressedKey) await commitIMessageInboundReplay({
				guard: inboundReplayGuard,
				accountId: accountInfo.accountId,
				keys: [suppressedKey]
			});
			return;
		}
		const repairedMessage = await repairMessageConversationAnchor(message);
		if (!repairedMessage) return;
		const replay = await claimIMessageInboundReplay({
			guard: inboundReplayGuard,
			accountId: accountInfo.accountId,
			message: repairedMessage
		});
		if (!replay.claimed) {
			logVerbose(`imessage: dropping duplicate inbound notification account=${accountInfo.accountId}`);
			return;
		}
		trackPendingRecoveryReplayRow(repairedMessage);
		await inboundDebouncer.enqueue({
			message: repairedMessage,
			replayKey: replay.key
		});
	};
	await waitForTransportReady({
		label: "imsg rpc",
		timeoutMs: 3e4,
		logAfterMs: 1e4,
		logIntervalMs: 1e4,
		pollIntervalMs: 500,
		abortSignal: opts.abortSignal,
		runtime,
		check: async () => {
			const probe = await probeIMessage(probeTimeoutMs, {
				cliPath,
				dbPath,
				runtime
			});
			if (probe.ok) return { ok: true };
			if (probe.fatal) throw new Error(probe.error ?? "imsg rpc unavailable");
			return {
				ok: false,
				error: probe.error ?? "unreachable"
			};
		}
	});
	if (opts.abortSignal?.aborted) return;
	const abort = opts.abortSignal;
	const createWatchClient = async () => await createIMessageRpcClient({
		cliPath,
		dbPath,
		runtime,
		onNotification: (msg) => {
			if (msg.method === "message") handleMessage(msg.params).catch((err) => {
				runtime.error?.(`imessage: handler failed: ${String(err)}`);
			});
			else if (msg.method === "error") runtime.error?.(`imessage: watch error ${JSON.stringify(sanitizeIMessageWatchErrorPayload(msg.params))}`);
		}
	});
	const requireWatchClient = (watchClient) => {
		if (!watchClient) throw new Error("imessage monitor client not initialized");
		return watchClient;
	};
	for (let attempt = 1; attempt <= WATCH_SUBSCRIBE_MAX_ATTEMPTS; attempt++) {
		if (abort?.aborted) return;
		let attemptClient;
		let attemptDetachAbortHandler = () => {};
		let keepAttemptClient = false;
		try {
			attemptClient = requireWatchClient(await createWatchClient());
			let attemptSubscriptionId = null;
			attemptDetachAbortHandler = attachIMessageMonitorAbortHandler({
				abortSignal: abort,
				client: attemptClient,
				getSubscriptionId: () => attemptSubscriptionId
			});
			attemptSubscriptionId = (await attemptClient.request("watch.subscribe", {
				attachments: includeAttachments,
				include_reactions: true,
				...watchSinceRowid !== null ? { since_rowid: watchSinceRowid } : {}
			}, { timeoutMs: probeTimeoutMs }))?.subscription ?? null;
			client = attemptClient;
			detachAbortHandler = attemptDetachAbortHandler;
			keepAttemptClient = true;
			break;
		} catch (err) {
			if (abort?.aborted) return;
			if (!(attempt < WATCH_SUBSCRIBE_MAX_ATTEMPTS && isRetriableWatchSubscribeStartupError(err))) {
				runtime.error?.(danger(`imessage: monitor failed: ${describeIMessageWatchSubscribeStartupFailure({
					accountId: accountInfo.accountId,
					attempt,
					maxAttempts: WATCH_SUBSCRIBE_MAX_ATTEMPTS,
					cliPath,
					dbPath,
					remoteHost,
					includeAttachments,
					probeTimeoutMs,
					watchSinceRowid,
					error: err
				})}`));
				throw err;
			}
			runtime.log?.(warn(describeIMessageWatchSubscribeStartupFailure({
				accountId: accountInfo.accountId,
				attempt,
				maxAttempts: WATCH_SUBSCRIBE_MAX_ATTEMPTS,
				cliPath,
				dbPath,
				remoteHost,
				includeAttachments,
				probeTimeoutMs,
				watchSinceRowid,
				error: err,
				retryDelayMs: WATCH_SUBSCRIBE_RETRY_DELAY_MS
			})));
			attemptDetachAbortHandler();
			attemptDetachAbortHandler = () => {};
			await attemptClient?.stop();
			attemptClient = void 0;
			await waitForWatchSubscribeRetryDelay({
				ms: WATCH_SUBSCRIBE_RETRY_DELAY_MS,
				abortSignal: abort
			});
			if (abort?.aborted) return;
		} finally {
			if (!keepAttemptClient) {
				attemptDetachAbortHandler();
				await attemptClient?.stop();
			}
		}
	}
	const activeClient = client;
	if (!activeClient) return;
	const approvalContextLease = opts.channelRuntime ? registerChannelRuntimeContext({
		channelRuntime: opts.channelRuntime,
		channelId: "imessage",
		accountId: accountInfo.accountId,
		capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
		context: { accountId: accountInfo.accountId },
		abortSignal: abort
	}) : void 0;
	let approvalReactionPollInFlight = false;
	const pollApprovalReactions = async (allowRecentChatDiscovery = false) => {
		if (approvalReactionPollInFlight) return;
		approvalReactionPollInFlight = true;
		try {
			await pollPendingIMessageApprovalReactions({
				client: activeClient,
				cfg,
				accountId: accountInfo.accountId,
				allowRecentChatDiscovery,
				logVerboseMessage: logVerbose
			});
		} catch (err) {
			logVerbose(`imessage: approval reaction poll failed: ${String(err)}`);
		} finally {
			approvalReactionPollInFlight = false;
		}
	};
	const approvalReactionPollTimer = setInterval(() => {
		pollApprovalReactions();
	}, APPROVAL_REACTION_POLL_INTERVAL_MS);
	const approvalReactionDiscoveryTimer = setInterval(() => {
		pollApprovalReactions(true);
	}, APPROVAL_REACTION_DISCOVERY_INTERVAL_MS);
	pollApprovalReactions(true);
	if (catchupCfg.enabled && !abort?.aborted) {
		startupCatchupInProgress = true;
		try {
			const catchupSummary = await runIMessageCatchup({
				client: activeClient,
				accountId: accountInfo.accountId,
				config: catchupCfg,
				includeAttachments,
				dispatchPayload: (message) => handleMessageNow(message, { advanceCatchupCursor: false }),
				observeSkippedFromMePayload: (message) => {
					const { bodyText } = resolveIMessageInboundBodyText(message);
					rememberIMessageSkippedFromMeForSelfChatDedupe({
						accountId: accountInfo.accountId,
						message,
						bodyText,
						selfChatCache
					});
				},
				runtime
			});
			liveCatchupCursorAdvanceEnabled = catchupSummary.querySucceeded && catchupSummary.fullyCaughtUp;
			if (liveCatchupCursorAdvanceEnabled) await flushPendingLiveCatchupCursorAdvances();
			else pendingLiveCatchupCursorAdvances.length = 0;
		} catch (err) {
			pendingLiveCatchupCursorAdvances.length = 0;
			runtime.error?.(`imessage catchup: pass failed: ${String(err)}`);
		} finally {
			startupCatchupInProgress = false;
		}
	}
	try {
		await activeClient.waitForClose();
	} catch (err) {
		if (abort?.aborted) return;
		runtime.error?.(danger(`imessage: monitor failed: ${String(err)}`));
		throw err;
	} finally {
		clearInterval(approvalReactionPollTimer);
		clearInterval(approvalReactionDiscoveryTimer);
		approvalContextLease?.dispose();
		detachAbortHandler();
		await activeClient.stop();
	}
}
//#endregion
export { monitorIMessageProvider as t };