openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
9,254 lines • 335 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { M as resolveTimestampMsToIsoString, S as resolveDateTimestampMs, _ as parseStrictFiniteNumber, b as parseStrictPositiveInteger, g as parseFiniteNumber, o as asDateTimestampMs, t as MAX_DATE_TIMESTAMP_MS, v as parseStrictInteger, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { l as normalizeStringEntries, u as normalizeStringEntriesLower } from "./string-normalization-WNUDCpXX.js";
import { c as withTimeout } from "./fs-safe-aqmM_n6V.js";
import { t as createNonExitingRuntime } from "./runtime-B4lgFmsS.js";
import { t as isVerbose } from "./global-state-BAD7XgmL.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { n as normalizeAccountId } from "./account-id-Df9e41E6.js";
import { a as shouldLogVerbose, r as logVerbose, s as warn, t as danger } from "./globals-GTrXU4s9.js";
import { n as logError, t as logDebug } from "./logger-lqqYRtFw.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { o as resolveEnvHttpProxyAgentOptions } from "./proxy-env-B9aW4MXJ.js";
import { n as createHttp1EnvHttpProxyAgent, r as createHttp1ProxyAgent } from "./undici-runtime-CdQVyjAR.js";
import { n as ensureAuthProfileStore } from "./store-C8spD0DG.js";
import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js";
import { u as resolveStorePath } from "./paths-NEwU8m3X.js";
import { d as updateSessionStore, o as readSessionUpdatedAt, r as getSessionEntry } from "./store-Qsgtu-0y.js";
import { o as isProfileInCooldown, t as clearExpiredCooldowns } from "./usage-state-glPP7WfI.js";
import { s as resolveProfilesUnavailableReason } from "./usage-K7-k2Oys.js";
import { c as resolveDefaultModelForAgent } from "./model-selection-CA_65iXi.js";
import { n as createNodeProxyAgent } from "./node-proxy-agent-CWnkEd0Y.js";
import { n as listChatCommands } from "./commands-registry-list-MVYs4B-p.js";
import { a as enqueueSystemEvent } from "./system-events-C5WI3S5a.js";
import { h as resolveTextChunksWithFallback, m as resolveSendableOutboundReplyParts } from "./reply-payload-D-VMfpYI.js";
import { i as resolveEffectiveDebugProxyUrl, r as resolveDebugProxySettings } from "./env-ckWue_wY.js";
import { i as resolveAgentRoute } from "./resolve-route-xcIS8NFL.js";
import { t as resolveCommandAuthorizedFromAuthorizers } from "./command-gating-65fgTdwb.js";
import { t as readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from-CwbNJoLu.js";
import { a as warnMissingProviderGroupPolicyFallbackOnce, i as resolveOpenProviderRuntimeGroupPolicy, r as resolveDefaultGroupPolicy, t as GROUP_POLICY_BLOCKED_LABEL } from "./runtime-group-policy-CSD1Is8G.js";
import { l as resolvePinnedMainDmOwnerFromAllowlist } from "./dm-policy-shared-BjnswXJt.js";
import { r as getAgentScopedMediaLocalRoots } from "./local-roots-BqdtViGS.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, s as resolveChunkMode } from "./chunk-Cpb8JO1x.js";
import { t as applyModelOverrideToSessionEntry } from "./model-overrides-D5WaLvcp.js";
import { t as formatThreadBindingDurationLabel } from "./thread-bindings-messages-Dew3j_KP.js";
import { i as formatDurationSeconds } from "./format-duration-BrZ-AaEJ.js";
import "./error-runtime-C8vbtAJt.js";
import "./logging-core-BH_8JYZP.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 { t as summarizeStringEntries } from "./string-sample-BYGbtG9S.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { t as chunkItems } from "./chunk-items-HPRB2OIa.js";
import "./text-chunking-D45TeeEs.js";
import "./text-utility-runtime-BZ3jGr-e.js";
import { r as matchPluginCommand, t as executePluginCommand } from "./commands-D0WhGlh-.js";
import { c as parseCommandArgs, d as serializeCommandArgs, i as formatCommandArgMenuTitle, l as resolveCommandArgChoices, n as buildCommandTextFromArgs, r as findCommandByNativeName, s as listNativeCommandSpecsForConfig, u as resolveCommandArgMenu } from "./commands-registry-wvHlpYBK.js";
import { n as resolveStoredModelOverride } from "./stored-model-override-B3aWM_od.js";
import { t as finalizeInboundContext } from "./inbound-context-CZx-NgvC.js";
import { n as wrapFetchWithAbortSignal } from "./fetch-C0IyrVwh.js";
import "./fetch-runtime-hJGvq3C8.js";
import { n as captureWsEvent, t as captureHttpExchange } from "./runtime-DrwkIUiy.js";
import "./proxy-capture-x_2F2qw1.js";
import { t as resolveRequestUrl } from "./request-url-vv3NYC-S.js";
import { l as normalizeProviderId } from "./provider-model-shared-D-lyTLvb.js";
import "./routing-CQ63ICPX.js";
import { a as resolveEnvelopeFormatOptions, r as formatInboundEnvelope } from "./envelope-IMcsY0tl.js";
import { t as createChannelReplyPipeline } from "./reply-pipeline-eJPstCAz.js";
import { t as resolveMarkdownTableMode } from "./markdown-tables-LfSv7QSx.js";
import { t as buildPairingReply } from "./pairing-messages-BJSYThef.js";
import { d as upsertChannelPairingRequest } from "./pairing-store-CxANz_ON.js";
import { c as buildAllowlistResolutionSummary, d as patchAllowlistUsersInConfigEntries, f as summarizeMapping, l as canonicalizeAllowlistWithResolvedIds, s as addAllowlistUserEntriesFromConfigEntry } from "./allow-from-DCaV3hzI.js";
import { n as isDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js";
import { t as resolveApprovalOverGateway } from "./approval-gateway-resolver-kRkWoLay.js";
import "./approval-gateway-runtime-D7fO1U7E.js";
import { t as CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "./approval-handler-adapter-runtime-DLWtIqHq.js";
import { r as registerChannelRuntimeContext } from "./channel-runtime-context-Bx08uB94.js";
import { n as resolveNativeCommandsEnabled, r as resolveNativeSkillsEnabled } from "./commands-0zpR1Wsm.js";
import { t as listSkillCommandsForAgents } from "./chat-commands-DKDF6ilZ.js";
import { n as dispatchReplyWithDispatcher } from "./reply-dispatch-runtime-B3glKPcA.js";
import "./reply-chunking-C9pgDFVG.js";
import { f as runChannelInboundEvent } from "./inbound-reply-dispatch-BpzI7GIp.js";
import "./channel-outbound-B3_Zy-kG.js";
import "./web-media-Nj3kZBh5.js";
import "./system-event-runtime-D80J5tzu.js";
import "./model-session-runtime-XUhYGuhe.js";
import "./ssrf-runtime-BOGN5pUi.js";
import { n as resolveConfiguredBindingRoute, t as ensureConfiguredBindingRouteReady } from "./binding-routing-C-pxzG2P.js";
import "./conversation-binding-runtime-DRPBolNq.js";
import "./conversation-runtime-VjM1-D7a.js";
import "./agent-runtime-CvpQRNVf.js";
import { i as createInteractiveConversationBindingHelpers, r as dispatchPluginInteractiveHandler } from "./plugin-runtime-Dw_Zc32A.js";
import { n as createTransportActivityStatusPatch, t as createConnectedChannelStatusPatch } from "./gateway-runtime-BMyBmMxe.js";
import "./markdown-table-runtime-j_C4iTF_.js";
import "./dangerous-name-runtime-zP2lSdMQ.js";
import { t as resolveNativeCommandSessionTargets } from "./native-command-session-targets-C-zj9Kp-.js";
import "./command-auth-native-D4ML0UKp.js";
import { t as resolveDirectStatusReplyForSession } from "./command-status-runtime-0toPIA3I.js";
import "./channel-inbound-bl7VmTdr.js";
import "./channel-ingress-runtime-rGltVkKC.js";
import { t as createChannelPairingChallengeIssuer } from "./channel-pairing-Bk6_JhTm.js";
import { t as loadSessionStore } from "./session-store-runtime-rvUx2Fil.js";
import "./native-command-config-runtime-Dla2pZe-.js";
import "./native-command-registry-BTybrgYE.js";
import "./state-paths-DzVdErem.js";
import { t as normalizeDiscordToken } from "./token-ByxvVRHv.js";
import { c as resolveDiscordAccountAllowFrom, d as resolveDiscordAccountDmPolicy, f as resolveDiscordMaxLinesPerMessage, s as resolveDiscordAccount } from "./accounts-BI_6uJlY.js";
import { a as resolveDiscordModalEntryWithPersistence, i as resolveDiscordComponentEntryWithPersistence, t as editDiscordComponentMessage } from "./send.components-BkX2GzM6.js";
import { A as Button, B as Separator, Bt as GatewayIntentBits, D as Modal, Ft as ButtonStyle, G as BaseMessageInteractiveComponent, H as TextDisplay, It as ChannelType, Lt as ComponentType, M as Container, Pt as ApplicationCommandOptionType, R as Row, Rt as GatewayCloseCodes, S as User, V as StringSelectMenu, Vt as GatewayOpcodes, a as PresenceUpdateListener, b as Guild, c as ThreadUpdateListener, d as Plugin, i as MessageReactionRemoveListener, m as RateLimitError, n as MessageCreateListener, o as ReadyListener, r as MessageReactionAddListener, t as InteractionCreateListener, u as Client, v as Command, x as Message, y as CommandWithSubcommands, zt as GatewayDispatchEvents } from "./discord-DdqXEGEm.js";
import { c as setPresence, i as unregisterGateway, r as registerGateway } from "./gateway-registry-DPxmW0Db.js";
import { B as DISCORD_REST_TIMEOUT_MS, H as withValidatedDiscordProxy, I as createDiscordRestClient, V as validateDiscordProxyUrl } from "./send.shared-SdWkx4sF.js";
import { t as chunkDiscordTextWithMode } from "./chunk-D_QII2-W.js";
import { a as summarizeDiscordResponseBody, i as isDiscordRateLimitResponseBody } from "./api-IrbNzRWx.js";
import { n as formatDiscordUserTag, t as formatDiscordReactionEmoji } from "./format-DZW075F7.js";
import { _ as resolveGroupDmAllow, a as normalizeDiscordSlug, c as resolveDiscordChannelConfigWithFallback, d as resolveDiscordGuildEntry, f as resolveDiscordMemberAccessState, i as normalizeDiscordDisplaySlug, l as resolveDiscordChannelPolicyCommandAuthorizer, m as resolveDiscordOwnerAccess, n as isDiscordGroupAllowedByPolicy, o as resolveDiscordAllowListMatch, r as normalizeDiscordAllowList, v as shouldEmitDiscordReactionNotification } from "./allow-list-CBhA2l4a.js";
import { r as formatMention } from "./mentions-Da7srbtk.js";
import { g as parseDiscordModalCustomIdForInteraction, h as parseDiscordModalCustomId, m as parseDiscordComponentCustomIdForInteraction, p as parseDiscordComponentCustomId } from "./components-CDHzCfc9.js";
import { t as getDiscordRuntime } from "./runtime-n11XewvP.js";
import { i as isDiscordExecApprovalClientEnabled, n as getDiscordExecApprovalApprovers } from "./approval-shared-CvDEtoxb.js";
import "./approval-native-OgnqttC5.js";
import { t as resolveDiscordConversationIdentity } from "./conversation-identity-LwFFXgXY.js";
import { t as resolveDiscordChannelAllowlist } from "./resolve-channels-BG-mtE2T.js";
import { t as resolveDiscordUserAllowlist } from "./resolve-users-CaPMu3_q.js";
import { a as isThreadArchived } from "./thread-bindings.discord-api-CMaNjIJM.js";
import { i as resolveDiscordChannelParentIdSafe, n as resolveDiscordChannelInfoSafe, o as resolveDiscordChannelTopicSafe, r as resolveDiscordChannelNameSafe, t as resolveDiscordChannelIdSafe } from "./channel-access-C12aDZ0p.js";
import { r as parseApplicationIdFromToken, t as fetchDiscordApplicationId } from "./probe-B7Yl68Zy.js";
import { d as withAbortTimeout, l as raceWithTimeout } from "./timeouts-BG9tJyKJ.js";
import { i as resolveDiscordEffectiveRoute, n as resolveDiscordBoundConversationRoute, o as handleDiscordDmCommandDecision, s as resolveDiscordDmCommandAccess } from "./route-resolution-CHy64Xc7.js";
import { u as resolveDiscordChannelInfo } from "./message-utils-BjIDHgFC.js";
import { s as resolveDiscordThreadParentInfo } from "./threading-D6pXQbX7.js";
import { t as resolveDiscordSenderIdentity } from "./sender-identity-BrSQQ4mW.js";
import { n as buildDiscordInboundAccessContext, t as buildDiscordGroupSystemPrompt } from "./inbound-context-DaAiPgjT.js";
import { n as resolveDiscordVoiceEnabled, t as authorizeDiscordVoiceIngress } from "./access-BkJOKuSE.js";
import { i as buildGuildLabel, r as buildDirectLabel, t as deliverDiscordReply } from "./reply-delivery-CRZ075t2.js";
import "./approval-handler.runtime-D6KU_obA.js";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { inspect } from "node:util";
import { Check, Errors } from "typebox/value";
import * as ws from "ws";
import * as dns from "node:dns";
import { Type } from "typebox";
import { EventEmitter } from "node:events";
import { Agent } from "node:https";
import { Agent as Agent$1, fetch as fetch$1 } from "undici";
//#region extensions/discord/src/monitor/listeners.queue.ts
const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 3e4;
const discordEventQueueLog = createSubsystemLogger("discord/event-queue");
function formatListenerContextValue(value) {
if (value === void 0 || value === null) return null;
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
return null;
}
function formatListenerContextSuffix(context) {
if (!context) return "";
const entries = Object.entries(context).flatMap(([key, value]) => {
const formatted = formatListenerContextValue(value);
return formatted ? [`${key}=${formatted}`] : [];
});
if (entries.length === 0) return "";
return ` (${entries.join(" ")})`;
}
function logSlowDiscordListener(params) {
if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS) return;
const duration = formatDurationSeconds(params.durationMs, {
decimals: 1,
unit: "seconds"
});
const message = `Slow listener detected: ${params.listener} took ${duration} for event ${params.event}`;
(params.logger ?? discordEventQueueLog).warn("Slow listener detected", {
listener: params.listener,
event: params.event,
durationMs: params.durationMs,
duration,
...params.context,
consoleMessage: `${message}${formatListenerContextSuffix(params.context)}`
});
}
async function runDiscordListenerWithSlowLog(params) {
const startedAt = Date.now();
try {
await params.run();
} catch (err) {
if (params.onError) {
params.onError(err);
return;
}
throw err;
} finally {
logSlowDiscordListener({
logger: params.logger,
listener: params.listener,
event: params.event,
durationMs: Date.now() - startedAt,
context: params.context
});
}
}
//#endregion
//#region extensions/discord/src/monitor/thread-channel-context.ts
function isDiscordThreadChannelType(type) {
return type === ChannelType.PublicThread || type === ChannelType.PrivateThread || type === ChannelType.AnnouncementThread;
}
function buildFetchedChannelInfo(channel) {
const channelInfo = resolveDiscordChannelInfoSafe(channel);
if (channelInfo.type === void 0) return null;
return {
type: channelInfo.type,
name: channelInfo.name,
topic: channelInfo.topic,
parentId: channelInfo.parentId,
ownerId: channelInfo.ownerId
};
}
async function resolveDiscordThreadLikeChannelContext(params) {
const safeChannelInfo = resolveDiscordChannelInfoSafe(params.channel);
const channelId = resolveDiscordChannelIdSafe(params.channel) ?? params.channelIdFallback ?? "";
const channelInfo = params.channelInfo !== void 0 ? params.channelInfo : channelId ? await resolveDiscordChannelInfo(params.client, channelId) : null;
const channelType = safeChannelInfo.type ?? channelInfo?.type;
const channelName = safeChannelInfo.name ?? channelInfo?.name;
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const parentId = resolveDiscordChannelParentIdSafe(params.channel) ?? channelInfo?.parentId;
const isThreadChannel = isDiscordThreadChannelType(channelType);
let threadParentId;
let threadParentName;
let threadParentSlug = "";
if (channelId && isThreadChannel) {
const parentInfo = await resolveDiscordThreadParentInfo({
client: params.client,
threadChannel: {
id: channelId,
name: channelName,
parentId,
parent: void 0
},
channelInfo
});
threadParentId = parentInfo.id;
threadParentName = parentInfo.name;
threadParentSlug = threadParentName ? normalizeDiscordSlug(threadParentName) : "";
}
return {
channelType,
isThreadChannel,
channelId,
channelName,
channelSlug,
parentId,
threadParentId,
threadParentName,
threadParentSlug,
channelInfo
};
}
async function resolveFetchedDiscordThreadLikeChannelContext(params) {
return await resolveDiscordThreadLikeChannelContext({
...params,
channelInfo: buildFetchedChannelInfo(params.channel)
});
}
//#endregion
//#region extensions/discord/src/monitor/listeners.reactions.ts
var DiscordReactionListener = class extends MessageReactionAddListener {
constructor(params) {
super();
this.params = params;
}
async handle(data, client) {
this.params.onEvent?.();
await runDiscordReactionHandler({
data,
client,
action: "added",
handlerParams: this.params,
listener: this.constructor.name,
event: this.type
});
}
};
var DiscordReactionRemoveListener = class extends MessageReactionRemoveListener {
constructor(params) {
super();
this.params = params;
}
async handle(data, client) {
this.params.onEvent?.();
await runDiscordReactionHandler({
data,
client,
action: "removed",
handlerParams: this.params,
listener: this.constructor.name,
event: this.type
});
}
};
async function runDiscordReactionHandler(params) {
await runDiscordListenerWithSlowLog({
logger: params.handlerParams.logger,
listener: params.listener,
event: params.event,
run: async () => handleDiscordReactionEvent({
data: params.data,
client: params.client,
action: params.action,
cfg: params.handlerParams.cfg,
accountId: params.handlerParams.accountId,
botUserId: params.handlerParams.botUserId,
dmEnabled: params.handlerParams.dmEnabled,
groupDmEnabled: params.handlerParams.groupDmEnabled,
groupDmChannels: params.handlerParams.groupDmChannels,
dmPolicy: params.handlerParams.dmPolicy,
allowFrom: params.handlerParams.allowFrom,
groupPolicy: params.handlerParams.groupPolicy,
allowNameMatching: params.handlerParams.allowNameMatching,
guildEntries: params.handlerParams.guildEntries,
logger: params.handlerParams.logger
})
});
}
async function authorizeDiscordReactionIngress(params) {
if (params.isDirectMessage && !params.dmEnabled) return {
allowed: false,
reason: "dm-disabled"
};
if (params.isGroupDm && !params.groupDmEnabled) return {
allowed: false,
reason: "group-dm-disabled"
};
if (params.isDirectMessage) {
const access = await resolveDiscordDmCommandAccess({
cfg: params.cfg,
accountId: params.accountId,
dmPolicy: params.dmPolicy,
configuredAllowFrom: params.allowFrom,
sender: {
id: params.user.id,
name: params.user.username,
tag: formatDiscordUserTag(params.user)
},
allowNameMatching: params.allowNameMatching,
eventKind: "reaction"
});
if (access.senderAccess.decision !== "allow") return {
allowed: false,
reason: access.senderAccess.reasonCode
};
}
if (params.isGroupDm && !resolveGroupDmAllow({
channels: params.groupDmChannels,
channelId: params.channelId,
channelName: params.channelName,
channelSlug: params.channelSlug
})) return {
allowed: false,
reason: "group-dm-not-allowlisted"
};
if (!params.isGuildMessage) return { allowed: true };
const channelAllowlistConfigured = Boolean(params.guildInfo?.channels) && Object.keys(params.guildInfo?.channels ?? {}).length > 0;
const channelAllowed = params.channelConfig?.allowed !== false;
if (!isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(params.guildInfo),
channelAllowlistConfigured,
channelAllowed
})) return {
allowed: false,
reason: "guild-policy"
};
if (params.channelConfig?.allowed === false) return {
allowed: false,
reason: "guild-channel-denied"
};
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig: params.channelConfig,
guildInfo: params.guildInfo,
memberRoleIds: params.memberRoleIds,
sender: {
id: params.user.id,
name: params.user.username,
tag: formatDiscordUserTag(params.user)
},
allowNameMatching: params.allowNameMatching
});
if (hasAccessRestrictions && !memberAllowed) return {
allowed: false,
reason: "guild-member-denied"
};
return { allowed: true };
}
async function handleDiscordThreadReactionNotification(params) {
if (params.reactionMode === "off") return;
if (params.reactionMode === "all" || params.reactionMode === "allowlist") {
const { access, channelConfig } = await params.resolveThreadChannelAccess();
if (!access.allowed || !params.shouldNotifyReaction({
mode: params.reactionMode,
channelConfig
})) return;
const { baseText } = params.resolveReactionBase();
params.emitReaction(baseText, params.parentId);
return;
}
const message = await params.message.fetch().catch(() => null);
const { access, channelConfig } = await params.resolveThreadChannelAccess();
const messageAuthorId = message?.author?.id ?? void 0;
if (!access.allowed || !params.shouldNotifyReaction({
mode: params.reactionMode,
messageAuthorId,
channelConfig
})) return;
params.emitReactionWithAuthor(message);
}
async function handleDiscordChannelReactionNotification(params) {
if (params.isGuildMessage) {
if (!(await params.authorizeReactionIngressForChannel(params.channelConfig)).allowed) return;
}
if (params.reactionMode === "off") return;
if (params.reactionMode === "all" || params.reactionMode === "allowlist") {
if (!params.shouldNotifyReaction({
mode: params.reactionMode,
channelConfig: params.channelConfig
})) return;
const { baseText } = params.resolveReactionBase();
params.emitReaction(baseText, params.parentId);
return;
}
const message = await params.message.fetch().catch(() => null);
const messageAuthorId = message?.author?.id ?? void 0;
if (!params.shouldNotifyReaction({
mode: params.reactionMode,
messageAuthorId,
channelConfig: params.channelConfig
})) return;
params.emitReactionWithAuthor(message);
}
function hasDiscordGuildChannelOverrides(guildInfo) {
return Boolean(guildInfo?.channels && Object.keys(guildInfo.channels).length > 0);
}
function shouldSkipGuildReactionBeforeChannelFetch(params) {
if (params.reactionMode === "off" || params.groupPolicy === "disabled") return true;
if (params.reactionMode !== "allowlist") return false;
if (hasDiscordGuildChannelOverrides(params.guildInfo)) return false;
return !shouldEmitDiscordReactionNotification({
mode: params.reactionMode,
botId: params.botUserId,
userId: params.user.id,
userName: params.user.username,
userTag: formatDiscordUserTag(params.user),
guildInfo: params.guildInfo,
memberRoleIds: params.memberRoleIds,
allowNameMatching: params.allowNameMatching
});
}
async function handleDiscordReactionEvent(params) {
try {
const { data, client, action, botUserId, guildEntries } = params;
if (!("user" in data)) return;
const user = data.user;
if (!user || user.bot) return;
if (botUserId && user.id === botUserId) return;
const isGuildMessage = Boolean(data.guild_id);
const guildInfo = isGuildMessage ? resolveDiscordGuildEntry({
guild: data.guild ?? void 0,
guildId: data.guild_id ?? void 0,
guildEntries
}) : null;
if (isGuildMessage && guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) return;
const memberRoleIds = Array.isArray(data.rawMember?.roles) ? data.rawMember.roles.map((roleId) => roleId) : [];
const reactionMode = guildInfo?.reactionNotifications ?? "own";
if (isGuildMessage && shouldSkipGuildReactionBeforeChannelFetch({
reactionMode,
guildInfo,
groupPolicy: params.groupPolicy,
memberRoleIds,
user,
botUserId,
allowNameMatching: params.allowNameMatching
})) return;
const channel = await client.fetchChannel(data.channel_id);
if (!channel) return;
const channelContext = await resolveFetchedDiscordThreadLikeChannelContext({
client,
channel,
channelIdFallback: data.channel_id
});
const channelName = channelContext.channelName;
const channelSlug = channelContext.channelSlug;
const channelType = channelContext.channelType;
const isDirectMessage = channelType === ChannelType.DM;
const isGroupDm = channelType === ChannelType.GroupDM;
const isThreadChannel = channelContext.isThreadChannel;
const reactionIngressBase = {
cfg: params.cfg,
accountId: params.accountId,
user,
memberRoleIds,
isDirectMessage,
isGroupDm,
isGuildMessage,
channelId: data.channel_id,
channelName,
channelSlug,
dmEnabled: params.dmEnabled,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels: params.groupDmChannels,
dmPolicy: params.dmPolicy,
allowFrom: params.allowFrom,
groupPolicy: params.groupPolicy,
allowNameMatching: params.allowNameMatching,
guildInfo
};
if (!isGuildMessage) {
const ingressAccess = await authorizeDiscordReactionIngress(reactionIngressBase);
if (!ingressAccess.allowed) {
logVerbose(`discord reaction blocked sender=${user.id} (reason=${ingressAccess.reason})`);
return;
}
}
const parentId = isThreadChannel ? channelContext.threadParentId : channelContext.parentId;
const parentName = isThreadChannel ? channelContext.threadParentName : void 0;
const parentSlug = isThreadChannel ? channelContext.threadParentSlug : "";
let reactionBase = null;
const resolveReactionBase = () => {
if (reactionBase) return reactionBase;
const emojiLabel = formatDiscordReactionEmoji(data.emoji);
reactionBase = {
baseText: `Discord reaction ${action}: ${emojiLabel} by ${formatDiscordUserTag(user)} on ${guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : data.guild_id ?? (isGroupDm ? "group-dm" : "dm"))} ${channelSlug ? `#${channelSlug}` : channelName ? `#${normalizeDiscordSlug(channelName)}` : `#${data.channel_id}`} msg ${data.message_id}`,
contextKey: `discord:reaction:${action}:${data.message_id}:${user.id}:${emojiLabel}`
};
return reactionBase;
};
const emitReaction = (text, parentPeerId) => {
const { contextKey } = resolveReactionBase();
enqueueSystemEvent(text, {
sessionKey: resolveAgentRoute({
cfg: params.cfg,
channel: "discord",
accountId: params.accountId,
guildId: data.guild_id ?? void 0,
memberRoleIds,
peer: {
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? user.id : data.channel_id
},
parentPeer: parentPeerId ? {
kind: "channel",
id: parentPeerId
} : void 0
}).sessionKey,
contextKey
});
};
const shouldNotifyReaction = (options) => shouldEmitDiscordReactionNotification({
mode: options.mode,
botId: botUserId,
messageAuthorId: options.messageAuthorId,
userId: user.id,
userName: user.username,
userTag: formatDiscordUserTag(user),
channelConfig: options.channelConfig,
guildInfo,
memberRoleIds,
allowNameMatching: params.allowNameMatching
});
const emitReactionWithAuthor = (message) => {
const { baseText } = resolveReactionBase();
const authorLabel = message?.author ? formatDiscordUserTag(message.author) : void 0;
emitReaction(authorLabel ? `${baseText} from ${authorLabel}` : baseText, parentId);
};
const resolveThreadChannelConfig = () => resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: data.channel_id,
channelName,
channelSlug,
parentId,
parentName,
parentSlug,
scope: "thread"
});
const authorizeReactionIngressForChannel = async (channelConfig) => await authorizeDiscordReactionIngress({
...reactionIngressBase,
channelConfig
});
const resolveThreadChannelAccess = async () => {
const channelConfig = resolveThreadChannelConfig();
return {
access: await authorizeReactionIngressForChannel(channelConfig),
channelConfig
};
};
if (isThreadChannel) {
await handleDiscordThreadReactionNotification({
reactionMode,
message: data.message,
parentId,
resolveThreadChannelAccess,
shouldNotifyReaction,
resolveReactionBase,
emitReaction,
emitReactionWithAuthor
});
return;
}
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: data.channel_id,
channelName,
channelSlug,
parentId,
parentName,
parentSlug,
scope: "channel"
});
await handleDiscordChannelReactionNotification({
isGuildMessage,
reactionMode,
message: data.message,
channelConfig,
parentId,
authorizeReactionIngressForChannel,
shouldNotifyReaction,
resolveReactionBase,
emitReaction,
emitReactionWithAuthor
});
} catch (err) {
params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));
}
}
//#endregion
//#region extensions/discord/src/monitor/thread-session-close.ts
/**
* Marks every session entry in the store whose key contains {@link threadId}
* as "reset" by setting `updatedAt` to 0.
*
* This mirrors how the daily / idle session reset works: zeroing `updatedAt`
* makes `evaluateSessionFreshness` treat the session as stale on the next
* inbound message, so the bot starts a fresh conversation without deleting
* any on-disk transcript history.
*/
async function closeDiscordThreadSessions(params) {
const { cfg, accountId, threadId } = params;
const normalizedThreadId = normalizeOptionalLowercaseString(threadId) ?? "";
if (!normalizedThreadId) return 0;
const segmentRe = new RegExp(`:${normalizedThreadId}(?::|$)`, "i");
function sessionKeyContainsThreadId(key) {
return segmentRe.test(key);
}
const storePath = resolveStorePath(cfg.session?.store, { agentId: accountId });
let resetCount = 0;
await updateSessionStore(storePath, (store) => {
for (const [key, entry] of Object.entries(store)) {
if (!entry || !sessionKeyContainsThreadId(key)) continue;
if (entry.updatedAt === 0) continue;
entry.updatedAt = 0;
resetCount += 1;
}
return resetCount;
});
return resetCount;
}
//#endregion
//#region extensions/discord/src/monitor/listeners.ts
function registerDiscordListener(listeners, listener) {
if (listeners.some((existing) => existing.constructor === listener.constructor)) return false;
listeners.push(listener);
return true;
}
var DiscordMessageListener = class extends MessageCreateListener {
constructor(handler, logger, onEvent) {
super();
this.handler = handler;
this.logger = logger;
this.onEvent = onEvent;
}
async handle(data, client) {
this.onEvent?.();
Promise.resolve().then(() => this.handler(data, client)).catch((err) => {
(this.logger ?? discordEventQueueLog).error(danger(`discord handler failed: ${String(err)}`));
});
}
};
var DiscordInteractionListener = class extends InteractionCreateListener {
constructor(logger, onEvent) {
super();
this.logger = logger;
this.onEvent = onEvent;
}
async handle(data, client) {
this.onEvent?.();
Promise.resolve().then(() => client.handleInteraction(data, {})).catch((err) => {
(this.logger ?? discordEventQueueLog).error(danger(`discord interaction handler failed: ${String(err)}`));
});
}
};
var DiscordPresenceListener = class extends PresenceUpdateListener {
constructor(params) {
super();
this.logger = params.logger;
this.accountId = params.accountId;
}
async handle(data) {
try {
const userId = "user" in data && data.user && typeof data.user === "object" && "id" in data.user ? data.user.id : void 0;
if (!userId) return;
setPresence(this.accountId, userId, data);
} catch (err) {
(this.logger ?? discordEventQueueLog).error(danger(`discord presence handler failed: ${String(err)}`));
}
}
};
var DiscordThreadUpdateListener = class extends ThreadUpdateListener {
constructor(cfg, accountId, logger) {
super();
this.cfg = cfg;
this.accountId = accountId;
this.logger = logger;
}
async handle(data) {
await runDiscordListenerWithSlowLog({
logger: this.logger,
listener: this.constructor.name,
event: this.type,
run: async () => {
if (!isThreadArchived(data)) return;
const threadId = "id" in data && typeof data.id === "string" ? data.id : void 0;
if (!threadId) return;
const logger = this.logger ?? discordEventQueueLog;
const count = await closeDiscordThreadSessions({
cfg: this.cfg,
accountId: this.accountId,
threadId
});
if (count > 0) logger.info("Discord thread archived — reset sessions", {
threadId,
count
});
},
onError: (err) => {
(this.logger ?? discordEventQueueLog).error(danger(`discord thread-update handler failed: ${String(err)}`));
}
});
}
};
//#endregion
//#region extensions/discord/src/monitor/native-command-reply.ts
const DISCORD_EMPTY_VISIBLE_REPLY_WARNING = "⚠️ Command produced no visible reply.";
function isDiscordUnknownInteraction(error) {
if (!error || typeof error !== "object") return false;
const err = error;
if (err.discordCode === 10062 || err.rawBody?.code === 10062) return true;
if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) return true;
if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) return true;
return false;
}
function hasRenderableReplyPayload(payload) {
if (resolveSendableOutboundReplyParts(payload).hasContent) return true;
const discordData = payload.channelData?.discord;
if (Array.isArray(discordData?.components) && discordData.components.length > 0) return true;
return false;
}
async function safeDiscordInteractionCall(label, fn) {
try {
return await fn();
} catch (error) {
if (isDiscordUnknownInteraction(error)) {
logVerbose(`discord: ${label} skipped (interaction expired)`);
return null;
}
throw error;
}
}
async function deliverDiscordInteractionReply(params) {
const { interaction, payload, textLimit, maxLinesPerMessage, preferFollowUp, chunkMode } = params;
const reply = resolveSendableOutboundReplyParts(payload);
const discordData = payload.channelData?.discord;
let firstMessageComponents = Array.isArray(discordData?.components) && discordData.components.length > 0 ? discordData.components : void 0;
let hasReplied = false;
const sendMessage = async (content, files, components) => {
const contentPayload = content ? { content } : {};
const payloadLocal = files && files.length > 0 ? {
...contentPayload,
...components ? { components } : {},
...params.responseEphemeral !== void 0 ? { ephemeral: params.responseEphemeral } : {},
files: files.map((file) => {
if (file.data instanceof Blob) return {
name: file.name,
data: file.data
};
const arrayBuffer = Uint8Array.from(file.data).buffer;
return {
name: file.name,
data: new Blob([arrayBuffer])
};
})
} : {
...contentPayload,
...components ? { components } : {},
...params.responseEphemeral !== void 0 ? { ephemeral: params.responseEphemeral } : {}
};
await safeDiscordInteractionCall("interaction send", async () => {
if (!preferFollowUp && !hasReplied) {
await interaction.reply(payloadLocal);
hasReplied = true;
firstMessageComponents = void 0;
return;
}
await interaction.followUp(payloadLocal);
hasReplied = true;
firstMessageComponents = void 0;
});
};
if (reply.hasMedia) {
const media = await Promise.all(reply.mediaUrls.map(async (url) => {
const loaded = await loadWebMedia(url, { localRoots: params.mediaLocalRoots });
return {
name: loaded.fileName ?? "upload",
data: loaded.buffer
};
}));
const chunks = resolveTextChunksWithFallback(reply.text, chunkDiscordTextWithMode(reply.text, {
maxChars: textLimit,
maxLines: maxLinesPerMessage,
chunkMode
}));
await sendMessage(chunks[0] ?? "", media, firstMessageComponents);
for (const chunk of chunks.slice(1)) {
if (!chunk.trim()) continue;
await sendMessage(chunk);
}
return;
}
if (!reply.hasText && !firstMessageComponents) return;
let chunks = reply.text || firstMessageComponents ? resolveTextChunksWithFallback(reply.text, chunkDiscordTextWithMode(reply.text, {
maxChars: textLimit,
maxLines: maxLinesPerMessage,
chunkMode
})) : [];
if (chunks.length === 0 && firstMessageComponents) chunks = [""];
for (const chunk of chunks) {
if (!chunk.trim() && !firstMessageComponents) continue;
await sendMessage(chunk, void 0, firstMessageComponents);
}
}
//#endregion
//#region extensions/discord/src/monitor/native-command-route.ts
async function resolveDiscordNativeInteractionRouteState(params) {
const route = resolveDiscordBoundConversationRoute({
cfg: params.cfg,
accountId: params.accountId,
guildId: params.guildId,
memberRoleIds: params.memberRoleIds,
isDirectMessage: params.isDirectMessage,
isGroupDm: params.isGroupDm,
directUserId: params.directUserId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId
});
const configuredRoute = params.threadBinding == null ? resolveConfiguredBindingRoute({
cfg: params.cfg,
route,
conversation: {
channel: "discord",
accountId: params.accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId
}
}) : null;
const configuredBinding = configuredRoute?.bindingResolution ?? null;
const configuredBoundSessionKey = normalizeOptionalString(configuredRoute?.boundSessionKey);
const boundSessionKey = normalizeOptionalString(params.threadBinding?.targetSessionKey) ?? configuredBoundSessionKey;
return {
route,
effectiveRoute: resolveDiscordEffectiveRoute({
route,
boundSessionKey,
configuredRoute,
matchedBy: configuredBinding ? "binding.channel" : void 0
}),
boundSessionKey,
configuredRoute,
configuredBinding,
bindingReadiness: params.enforceConfiguredBindingReadiness && configuredBinding ? await ensureConfiguredBindingRouteReady({
cfg: params.cfg,
bindingResolution: configuredBinding
}) : null
};
}
//#endregion
//#region extensions/discord/src/monitor/native-command.runtime.ts
const nativeCommandRuntime = {
matchPluginCommand,
executePluginCommand,
dispatchReplyWithDispatcher,
resolveDirectStatusReplyForSession,
resolveDiscordNativeInteractionRouteState,
getSessionEntry
};
//#endregion
//#region extensions/discord/src/monitor/native-command-agent-reply.ts
async function dispatchDiscordNativeAgentReply(params) {
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
cfg: params.cfg,
agentId: params.effectiveRoute.agentId,
channel: "discord",
accountId: params.effectiveRoute.accountId
});
const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(params.discordConfig);
let didReply = false;
const dispatchResult = await nativeCommandRuntime.dispatchReplyWithDispatcher({
ctx: params.ctxPayload,
cfg: params.cfg,
dispatcherOptions: {
...replyPipeline,
humanDelay: resolveHumanDelayConfig(params.cfg, params.effectiveRoute.agentId),
deliver: async (payload) => {
if (params.suppressReplies) return;
try {
await deliverDiscordInteractionReply({
interaction: params.interaction,
payload,
mediaLocalRoots: params.mediaLocalRoots,
textLimit: resolveTextChunkLimit(params.cfg, "discord", params.accountId, { fallbackLimit: 2e3 }),
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId
}),
preferFollowUp: params.preferFollowUp || didReply,
responseEphemeral: params.responseEphemeral,
chunkMode: resolveChunkMode(params.cfg, "discord", params.accountId)
});
} catch (error) {
if (isDiscordUnknownInteraction(error)) {
logVerbose("discord: interaction reply skipped (interaction expired)");
return;
}
throw error;
}
didReply = true;
},
onError: (err, info) => {
const message = err instanceof Error ? err.stack ?? err.message : String(err);
params.log.error(`discord slash ${info.kind} reply failed: ${message}`);
}
},
replyOptions: {
skillFilter: params.channelConfig?.skills,
disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0,
onModelSelected
}
});
if (params.suppressReplies || didReply || dispatchResult.queuedFinal || dispatchResult.counts.final !== 0 || dispatchResult.counts.block !== 0 || dispatchResult.counts.tool !== 0) return;
await safeDiscordInteractionCall("interaction empty fallback", async () => {
const payload = {
content: DISCORD_EMPTY_VISIBLE_REPLY_WARNING,
ephemeral: true
};
if (params.preferFollowUp) {
await params.interaction.followUp(payload);
return;
}
await params.interaction.reply(payload);
});
}
//#endregion
//#region extensions/discord/src/monitor/native-interaction-channel-context.ts
async function resolveDiscordNativeInteractionChannelContext(params) {
const channelContext = await resolveDiscordThreadLikeChannelContext({
client: params.client,
channel: params.channel,
channelIdFallback: params.channelIdFallback
});
const channelType = channelContext.channelType;
return {
channelType,
isDirectMessage: channelType === ChannelType.DM,
isGroupDm: channelType === ChannelType.GroupDM,
isThreadChannel: channelContext.isThreadChannel,
channelName: channelContext.channelName,
channelSlug: channelContext.channelSlug,
rawChannelId: channelContext.channelId,
threadParentId: params.hasGuild ? channelContext.threadParentId : void 0,
threadParentName: params.hasGuild ? channelContext.threadParentName : void 0,
threadParentSlug: params.hasGuild ? channelContext.threadParentSlug : ""
};
}
//#endregion
//#region extensions/discord/src/monitor/native-command-auth.ts
function resolveDiscordNativeCommandAllowlistAccess(params) {
const commandsAllowFrom = params.cfg.commands?.allowFrom;
if (!commandsAllowFrom || typeof commandsAllowFrom !== "object") return {
configured: false,
allowed: false
};
const rawAllowList = Array.isArray(commandsAllowFrom.discord) ? commandsAllowFrom.discord : commandsAllowFrom["*"];
if (!Array.isArray(rawAllowList)) return {
configured: false,
allowed: false
};
const guildId = normalizeOptionalString(params.guildId);
if (guildId) for (const entry of rawAllowList) {
const text = normalizeOptionalString(String(entry)) ?? "";
if (text.startsWith("guild:") && text.slice(6) === guildId) return {
configured: true,
allowed: true
};
}
const allowList = normalizeDiscordAllowList(rawAllowList.map(String), [
"discord:",
"user:",
"pk:"
]);
if (!allowList) return {
configured: true,
allowed: false
};
return {
configured: true,
allowed: resolveDiscordAllowListMatch({
allowList,
candidate: params.sender,
allowNameMatching: false
}).allowed
};
}
function resolveDiscordNativeCommandChannelAccessContext(params) {
const guild = params.guild ?? null;
const commandsAllowFromAccess = resolveDiscordNativeCommandAllowlistAccess({
cfg: params.cfg,
accountId: params.accountId,
sender: params.sender,
chatType: params.isDirectMessage ? "direct" : params.isThreadChannel ? "thread" : guild ? "channel" : "group",
conversationId: params.rawChannelId || void 0,
guildId: guild?.id
});
const guildInfo = resolveDiscordGuildEntry({
guild: guild ?? void 0,
guildId: guild?.id ?? void 0,
guildEntries: params.discordConfig?.guilds
});
return {
commandsAllowFromAccess,
guildInfo,
channelConfig: guild ? resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: params.rawChannelId,
channelName: params.channelName,
channelSlug: params.channelSlug,
parentId: params.threadParentId,
parentName: params.threadParentName,
parentSlug: params.threadParentSlug,
scope: params.isThreadChannel ? "thread" : "channel"
}) : null
};
}
function resolveDiscordCommandOwnerAllowFrom(cfg) {
const raw = cfg.commands?.ownerAllowFrom;
if (!Array.isArray(raw) || raw.length === 0) return;
const entries = [];
for (const entry of raw) {
const trimmed = normalizeOptionalString(String(entry ?? "")) ?? "";
if (!trimmed) continue;
const separatorIndex = trimmed.indexOf(":");
if (separatorIndex > 0) {
const prefix = trimmed.slice(0, separatorIndex).toLowerCase();
if (prefix === "discord") {
const remainder = normalizeOptionalString(trimmed.slice(separatorIndex + 1)) ?? "";
if (remainder) entries.push(remainder);
continue;
}
if (prefix !== "user" && prefix !== "pk") continue;
}
entries.push(trimmed);
}
return entries.length > 0 ? entries : void 0;
}
async function resolveDiscordGuildNativeCommandAuthorized(params) {
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.discord !== void 0,
groupPolicy: params.discordConfig?.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy
});
const policyAuthorizer = resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo: params.guildInfo,
channelConfig: params.channelConfig
});
if (!policyAuthorizer.allowed) return false;
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig: params.channelConfig,
guildInfo: params.guildInfo,
memberRoleIds: params.memberRoleIds,
sender: params.sender,
allowNameMatching: params.allowNameMatching
});
const commandAllowlistAuthorizer = {
configured: params.commandsAllowFromAccess.configured,
allowed: params.commandsAllowFromAccess.allowed
};
const ownerAuthorizer = {
configured: params.ownerAllowListConfigured,
allowed: params.ownerAllowed
};
const memberAuthorizer = {
configured: hasAccessRestrictions,
allowed: memberAllowed
};
const hasStricterAccessRestrictions = ownerAuthorizer.configured || memberAuthorizer.configured;
const fallbackAuthorizers = [
{
configured: policyAuthorizer.configured && !hasStricterAccessRestrictions,
allowed: policyAuthorizer.allowed
},
ownerAuthorizer,
memberAuthorizer
];
const authorizers = params.commandsAllowFromAccess.configured ? [commandAllowlistAuthorizer] : fallbackAuthorizers;
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups: params.useAccessGroups,
authorizers,
modeWhenAccessGroupsOff: "configured"
});
}
function resolveDiscordNativeGroupDmAccess(params) {
if (!params.isGroupDm) return { allowed: true };
if (params.groupEnabled === false) return {
allowed: false,
reason: "disabled"
};
if (!resolveGroupDmAllow({
channels: params.groupChannels,
channelId: params.channelId,
channelName: params.channelName,
channelSlug: params.channelSlug
})) return {
allowed: false,
reason: "not-allowlisted"
};
return { allowed: true };
}
async function resolveDiscordNativeAutocompleteAuthorized(params) {
const { interaction, cfg, discordConfig, accountId } = params;
const user = interaction.user;
if (!user) return false;
const sender = resolveDiscordSenderIdentity({
author: user,
pluralkitInfo: null
});
const { isDirectMessage, isGroupDm, isThreadChannel, channelName, channelSlug, rawChannelId, threadParentId, threadParentName, threadParentSlug } = await resolveDiscordNativeInteractionChannelContext({
channel: interaction.channel,
client: interaction.client,
hasGuild: Boolean(interaction.guild),
channelIdFallback: ""
});
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
const allowNameMatching = isDangerousNameMatchingEnabled(discordConfig);
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const configuredDmAllowFrom = resolveDiscordAccountAllowFrom({
cfg,
accountId
}) ?? [];
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
allowFrom: configuredDmAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching
});
const { commandsAllowFromAccess, guildInfo, channelConfig } = resolveDiscordNativeCommandChannelAccessContext({
cfg,
discordConfig,
accountId,
sender,
isDirectMessage,
isThreadChannel,
guild: interaction.guild ?? null,
rawChannelId,
channelName,
channelSlug,
threadParentId,
threadParentName,
threadParentSlug
});
if (channelConfig?.enabled === false) return false;
if (interaction.guild && channelConfig?.allowed === false) return false;
if (useAccessGroups && interaction.guild) {
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== void 0,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy
});
if (!resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo,
channelConfig
}).allowed) return false;
}
const dmEnabled = discordConfig?.dm?.enabled ?? true;
const dmPolicy = resolveDiscordAccountDmPolicy({
cfg,
accountId
}) ?? "pairing";
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") return false;
if ((await resolveDiscordDmCommandAccess({
accountId,
dmPolicy,
configuredAllowFrom: configuredDmAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching,
cfg,
rest: interaction.client.rest
})).senderAccess.decision !== "allow") return false;
}
if (!resolveDiscordNativeGroupDmAccess({
isGroupDm,
groupEnabled: discordConfig?.dm?.groupEnabled,
groupChannels: discordConfig?.dm?.groupChannels,
channelId: rawChannelId,
channelName,
channelSlug
}).allowed) return false;
if (params.skipCommandOwnerAllowFrom !== true) {
const commandOwnerAllowFrom = resolveDiscordCommandOwnerAllowFrom(cfg);
const { ownerAllowed: commandOwnerOk } = resolveDiscordOwnerAccess({
allowFrom: commandOwnerAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching
});
const commandOwnerAllowAll = commandOwnerAllowFrom?.includes("*") === true;
if (commandOwnerAllowFrom && !(commandOwnerOk || commandOwnerAllowAll) && !commandsAllowFromAccess.allowed) return false;
}
if (!isDirectMessage) return resolveDiscordGuildNativeCommandAuthorized({
cfg,
accountId,
discordConfig,
useAccessGroups,
commandsAllowFromAccess,
guildInfo,
channelConfig,
memberRoleIds,
sender,
allowNameMatching,
ownerAllowListConfigured: ownerAllowList != null,
ownerAllowed: ownerOk
});
return true;
}
//#endregion
//#region extensions/discord/src/monitor/native-command-bypass.ts
function shouldBypassConfiguredAcpEnsure(commandName) {
return normalizeLowercaseStringOrEmpty(commandName) === "acp";
}
function shouldBypassConfiguredAcpGuildGuards(commandName) {
const normalized = normalizeLowercaseStringOrEmpty(commandName);
return normalized === "new" || normalized === "reset";
}
//#endregion
//#region extensions/discord/src/monitor/native-command-context.ts
function buildDiscordNativeCommandContext(params) {
const conversationLabel = params.isDirectMessage ? params.user.globalName ?? params.user.username : params.channelId;
const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
channelConfig: params.channelConfig,
guildInfo: params.guildInfo,
sender: params.sender,
allowNameMatching: params.allowNameMatching,
isGuild: params.isGuild,
channelTopic: params.channelTopic
});
return finalizeInboundContext({
Body: params.prompt,
BodyForAgent: params.prompt,
RawBody: params.prompt,
CommandBody: params.prompt,
CommandArgs: params.commandArgs,
From: params.isDirectMessage ? `discord:${params.user.id}` : params.isGroupDm ? `discord:group:${params.channelId}` : `discord:channel:${params.channelId}`,
To: `slash:${params.user.id}`,
SessionKey: params.sessionKey,
CommandTargetSessionKey: params.commandTargetSessionKey,
AccountId: params.accountId ?? void 0,
ChatType: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
ConversationLabel: conversationLabel,
GroupSubject: params.isGuild ? params.guildName : void 0,
GroupSpace: params.isGuild ? params.guildInfo?.id ?? params.guildInfo?.slug ?? params.guildId : void 0,
MemberRoleIds: params.memberRoleIds,
GroupSystemPrompt: groupSystemPrompt,
UntrustedStructuredContext: untrustedContext,
OwnerAllowFrom: ownerAllowFrom,
SenderName: params.user.globalName ?? params.user.username,
SenderId: params.user.id,
SenderUsername: params.user.username,
SenderTag: params.sender.tag,
Provider: "discord",
Surface: "discord",
WasMentioned: true,
MessageSid: params.interactionId,
MessageThreadId: params.isThreadChannel ? params.channelId : void 0,
Timestamp: params.timestampMs ?? Date.now(),
CommandAuthorized: params.commandAuthorized,
CommandTurn: {
kind: "native",
source: "native",
authorized: params.commandAuthorized,
body: params.prompt
},
CommandSource: "native",
OriginatingChannel: "discord",
OriginatingTo: resolveDiscordConversationIdentity({
isDirectMessage: params.isDirectMessage,
userId: params.user.id,
channelId: params.channelId
}) ?? (params.isDirectMessage ? `user:${params.user.id}` : `channel:${params.channelId}`),
ThreadParentId: params.isThreadChannel ? params.threadParentId : void 0
});
}
//#endregion
//#region extensions/discord/src/monitor/native-command-status.ts
async function maybeDeliverDiscordDirectStatus(params) {
if (params.suppressReplies || params.commandName !== "status") return null;
const statusReply = await params.resolveDirectStatusReplyForSession({
cfg: params.cfg,
sessionKey: params.commandTargetSessionKey?.trim() || params.sessionKey,
channel: params.channel,
senderId: params.senderId,
senderIsOwner: params.senderIsOwner,
isAuthorizedSender: params.isAuthorizedSender,
isGroup: params.isGroup,
defaultGroupActivation: params.defaultGroupActivation
});
if (statusReply && hasRenderableReplyPayload(statusReply)) {
await deliverDiscordInteractionReply({
interaction: params.interaction,
payload: statusReply,
mediaLocalRoots: params.mediaLocalRoots,
textLimit: resolveTextChunkLimit(params.cfg, "discord", params.accountId, { fallbackLimit: 2e3 }),
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId
}),
preferFollowUp: params.preferFollowUp,
responseEphemeral: params.responseEphemeral,
chunkMode: resolveChunkMode(params.cfg, "discord", params.accountId)
});
return {
accepted: true,
effectiveRoute: params.effectiveRoute
};
}
await params.respond("Status unavailable.");
return {
accepted: true,
effectiveRoute: params.effectiveRoute
};
}
//#endregion
//#region extensions/discord/src/monitor/commands.ts
function resolveDiscordSlashCommandConfig(raw) {
return { ephemeral: raw?.ephemeral !== false };
}
//#endregion
//#region extensions/discord/src/monitor/native-command-arg-ui.ts
const DISCORD_COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg";
function createCommandArgsWithValue(params) {
return { values: { [params.argName]: params.value } };
}
function encodeDiscordCommandArgValue(value) {
return encodeURIComponent(value);
}
function decodeDiscordCommandArgValue(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function buildDiscordCommandArgCustomId(params) {
return [
`${DISCORD_COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeDiscordCommandArgValue(params.command)}`,
`arg=${encodeDiscordCommandArgValue(params.arg)}`,
`value=${encodeDiscordCommandArgValue(params.value)}`,
`user=${encodeDiscordCommandArgValue(params.userId)}`
].join(";");
}
function parseDiscordCommandArgData(data) {
if (!data || typeof data !== "object") return null;
const coerce = (value) => typeof value === "string" || typeof value === "number" ? String(value) : "";
const rawCommand = coerce(data.command);
const rawArg = coerce(data.arg);
const rawValue = coerce(data.value);
const rawUser = coerce(data.user);
if (!rawCommand || !rawArg || !rawValue || !rawUser) return null;
return {
command: decodeDiscordCommandArgValue(rawCommand),
arg: decodeDiscordCommandArgValue(rawArg),
value: decodeDiscordCommandArgValue(rawValue),
userId: decodeDiscordCommandArgValue(rawUser)
};
}
async function handleDiscordCommandArgInteraction(params) {
const { interaction, data, ctx } = params;
const clearWithMessage = async (content) => await params.safeInteractionCall("command arg update", () => interaction.update({
content,
components: []
}));
const parsed = parseDiscordCommandArgData(data);
if (!parsed) {
await clearWithMessage("Sorry, that selection is no longer available.");
return;
}
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
await params.safeInteractionCall("command arg ack", () => interaction.acknowledge());
return;
}
const commandDefinition = findCommandByNativeName(parsed.command, "discord") ?? listChatCommands().find((entry) => entry.key === parsed.command);
if (!commandDefinition) {
await clearWithMessage("Sorry, that command is no longer available.");
return;
}
if (await clearWithMessage(`⏳ Applying ${parsed.value}...`) === null) return;
const commandArgs = createCommandArgsWithValue({
argName: parsed.arg,
value: parsed.value
});
const commandArgsWithRaw = {
...commandArgs,
raw: serializeCommandArgs(commandDefinition, commandArgs)
};
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
await params.dispatchCommandInteraction({
interaction,
prompt,
command: commandDefinition,
commandArgs: commandArgsWithRaw,
cfg: ctx.cfg,
discordConfig: ctx.discordConfig,
accountId: ctx.accountId,
sessionPrefix: ctx.sessionPrefix,
preferFollowUp: true,
threadBindings: ctx.threadBindings,
responseEphemeral: resolveDiscordSlashCommandConfig(ctx.discordConfig?.slashCommand).ephemeral
});
}
async function runDiscordCommandArgButton(params) {
await handleDiscordCommandArgInteraction(params);
}
var DiscordCommandArgButton = class extends Button {
constructor(params) {
super();
this.style = ButtonStyle.Secondary;
this.label = params.label;
this.customId = params.customId;
this.params = params;
}
async run(interaction, data) {
await runDiscordCommandArgButton({
...this.params,
interaction,
data
});
}
};
function buildDiscordCommandArgMenu(params) {
const { command, menu, interaction } = params;
const commandLabel = command.nativeName ?? command.key;
const userId = interaction.user?.id ?? "";
const rows = chunkItems(menu.choices, 4).map((choices) => {
return new Row(choices.map((choice) => new DiscordCommandArgButton({
label: choice.label,
customId: buildDiscordCommandArgCustomId({
command: commandLabel,
arg: menu.arg.name,
value: choice.value,
userId
}),
ctx: params.ctx,
safeInteractionCall: params.safeInteractionCall,
dispatchCommandInteraction: params.dispatchCommandInteraction
})));
});
return {
content: formatCommandArgMenuTitle({
command,
menu
}),
components: rows
};
}
var DiscordCommandArgFallbackButton = class extends Button {
constructor(params) {
super();
this.params = params;
this.label = "cmdarg";
this.customId = "cmdarg:seed=1";
}
async run(interaction, data) {
await runDiscordCommandArgButton({
...this.params,
interaction,
data
});
}
};
function createDiscordCommandArgFallbackButton$1(params) {
return new DiscordCommandArgFallbackButton(params);
}
//#endregion
//#region extensions/discord/src/monitor/model-picker-preferences.ts
const DEFAULT_RECENT_LIMIT = 5;
const PREFERENCE_MAX_ENTRIES = 2e3;
let lastPreferenceTimestampMs = 0;
let lastPreferenceOrder = 0;
function openPreferenceStore(env) {
return getDiscordRuntime().state.openKeyedStore({
namespace: "model-picker-preferences",
maxEntries: PREFERENCE_MAX_ENTRIES,
...env ? { env } : {}
});
}
function normalizeId(value) {
return normalizeOptionalString(value) ?? "";
}
function buildDiscordModelPickerPreferenceKey(scope) {
const userId = normalizeId(scope.userId);
if (!userId) return null;
const accountId = normalizeAccountId(scope.accountId);
const guildId = normalizeId(scope.guildId);
if (guildId) return `discord:${accountId}:guild:${guildId}:user:${userId}`;
return `discord:${accountId}:dm:user:${userId}`;
}
function normalizeModelRef(raw) {
const value = raw?.trim();
if (!value) return null;
const slashIndex = value.indexOf("/");
if (slashIndex <= 0 || slashIndex >= value.length - 1) return null;
const provider = normalizeProviderId(value.slice(0, slashIndex));
const model = value.slice(slashIndex + 1).trim();
if (!provider || !model) return null;
return `${provider}/${model}`;
}
function sanitizeRecentModels(models, limit) {
const deduped = [];
const seen = /* @__PURE__ */ new Set();
for (const item of models ?? []) {
const normalized = normalizeModelRef(item);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
deduped.push(normalized);
if (deduped.length >= limit) break;
}
return deduped;
}
function sanitizeStoredPreferenceEntry(value) {
if (!value || typeof value !== "object") return;
const typedValue = value;
if (typeof typedValue.scopeKey !== "string" || typeof typedValue.modelRef !== "string") return;
const modelRef = normalizeModelRef(typedValue.modelRef);
if (!modelRef) return;
return {
scopeKey: typedValue.scopeKey,
modelRef,
updatedAt: typeof typedValue.updatedAt === "string" ? typedValue.updatedAt : "",
updatedOrder: typeof typedValue.updatedOrder === "number" && Number.isSafeInteger(typedValue.updatedOrder) ? typedValue.updatedOrder : void 0
};
}
function hashSegment(value, length) {
return createHash("sha256").update(value, "utf8").digest("hex").slice(0, length);
}
function buildPreferenceModelKey(scopeKey, modelRef) {
return `v1:${hashSegment(scopeKey, 32)}:${hashSegment(modelRef, 24)}`;
}
function timestampMs(value) {
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function timestampOrder(value) {
return value !== void 0 && value >= 0 ? value : 0;
}
function comparePreferenceEntries(left, right) {
return timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) || timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) || left.key.localeCompare(right.key);
}
function nextPreferenceTimestamp(existingEntries) {
const existingMaxTimestampMs = existingEntries.reduce((max, entry) => Math.max(max, timestampMs(entry.updatedAt)), 0);
lastPreferenceTimestampMs = Math.min(Math.max(resolveDateTimestampMs(Date.now(), 0), lastPreferenceTimestampMs + 1, existingMaxTimestampMs + 1), MAX_DATE_TIMESTAMP_MS);
const existingMaxOrder = existingEntries.reduce((max, entry) => Math.max(max, timestampOrder(entry.updatedOrder)), 0);
lastPreferenceOrder = Math.max(lastPreferenceOrder + 1, existingMaxOrder + 1);
return {
updatedAt: resolveTimestampMsToIsoString(lastPreferenceTimestampMs),
updatedOrder: lastPreferenceOrder
};
}
async function readDiscordModelPickerRecentModels(params) {
const key = buildDiscordModelPickerPreferenceKey(params.scope);
if (!key) return [];
const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
try {
const recent = (await openPreferenceStore(params.env).entries()).map((entry) => ({
key: entry.key,
value: sanitizeStoredPreferenceEntry(entry.value)
})).filter((entry) => entry.value?.scopeKey === key).toSorted(comparePreferenceEntries).map((entry) => entry.value.modelRef);
if (!params.allowedModelRefs || params.allowedModelRefs.size === 0) return sanitizeRecentModels(recent, limit);
return sanitizeRecentModels(recent.filter((modelRef) => params.allowedModelRefs?.has(modelRef)), limit);
} catch {
return [];
}
}
async function recordDiscordModelPickerRecentModel(params) {
const key = buildDiscordModelPickerPreferenceKey(params.scope);
const normalizedModelRef = normalizeModelRef(params.modelRef);
if (!key || !normalizedModelRef) return;
try {
const store = openPreferenceStore(params.env);
const timestamp = nextPreferenceTimestamp((await store.entries()).map((entry) => sanitizeStoredPreferenceEntry(entry.value)).filter((entry) => entry?.scopeKey === key));
await store.register(buildPreferenceModelKey(key, normalizedModelRef), {
scopeKey: key,
modelRef: normalizedModelRef,
...timestamp
});
const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
const scopedEntries = (await store.entries()).map((entry) => ({
key: entry.key,
value: sanitizeStoredPreferenceEntry(entry.value)
})).filter((entry) => entry.value?.scopeKey === key).toSorted(comparePreferenceEntries);
await Promise.all(scopedEntries.slice(limit).map((entry) => store.delete(entry.key)));
} catch {}
}
//#endregion
//#region extensions/discord/src/monitor/model-picker.state.ts
const DISCORD_MODEL_PICKER_CUSTOM_ID_KEY = "mdlpk";
function compareBucketItems(left, right) {
const normalized = left.toLowerCase().localeCompare(right.toLowerCase());
return normalized === 0 ? left.localeCompare(right) : normalized;
}
const COMMAND_CONTEXTS = ["model", "models"];
const PICKER_ACTIONS = [
"open",
"provider",
"model",
"runtime",
"submit",
"quick",
"back",
"reset",
"cancel",
"recents",
"nav",
"bucket"
];
const PICKER_VIEWS = [
"providers",
"models",
"recents"
];
let modelsProviderRuntimePromise;
async function loadModelsProviderRuntime() {
modelsProviderRuntimePromise ??= import("./plugin-sdk/models-provider-runtime.js");
return await modelsProviderRuntimePromise;
}
function encodeCustomIdValue(value) {
return encodeURIComponent(value);
}
function decodeCustomIdValue$1(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function isValidCommandContext(value) {
return COMMAND_CONTEXTS.includes(value);
}
function isValidPickerAction(value) {
return PICKER_ACTIONS.includes(value);
}
function isValidPickerView(value) {
return PICKER_VIEWS.includes(value);
}
function normalizeModelPickerPage(value) {
const numeric = typeof value === "number" ? value : NaN;
if (!Number.isFinite(numeric)) return 1;
return Math.max(1, Math.floor(numeric));
}
function parseRawPage(value) {
if (typeof value === "number") return normalizeModelPickerPage(value);
if (typeof value === "string") {
const parsed = parseStrictInteger(value);
if (parsed !== void 0) return normalizeModelPickerPage(parsed);
}
return 1;
}
function parseRawPositiveInt(value) {
return parseStrictPositiveInteger(value);
}
function coerceString(value) {
return typeof value === "string" || typeof value === "number" ? String(value) : "";
}
function clampPageSize(rawPageSize, max, fallback) {
if (!Number.isFinite(rawPageSize)) return fallback;
return Math.min(max, Math.max(1, Math.floor(rawPageSize ?? fallback)));
}
function paginateItems(params) {
const totalItems = params.items.length;
const totalPages = Math.max(1, Math.ceil(totalItems / params.pageSize));
const page = Math.max(1, Math.min(params.page, totalPages));
const startIndex = (page - 1) * params.pageSize;
const endIndexExclusive = Math.min(totalItems, startIndex + params.pageSize);
return {
items: params.items.slice(startIndex, endIndexExclusive),
page,
pageSize: params.pageSize,
totalPages,
totalItems,
hasPrev: page > 1,
hasNext: page < totalPages
};
}
async function loadDiscordModelPickerData(cfg, agentId) {
const { buildModelsProviderData } = await loadModelsProviderRuntime();
return buildModelsProviderData(cfg, agentId);
}
function buildDiscordModelPickerCustomId(params) {
const userId = params.userId.trim();
if (!userId) throw new Error("Discord model picker custom_id requires userId");
const page = normalizeModelPickerPage(params.page);
const providerPage = typeof params.providerPage === "number" && Number.isFinite(params.providerPage) ? Math.max(1, Math.floor(params.providerPage)) : void 0;
const normalizedProvider = params.provider ? normalizeProviderId(params.provider) : void 0;
const modelIndex = typeof params.modelIndex === "number" && Number.isFinite(params.modelIndex) ? Math.max(1, Math.floor(params.modelIndex)) : void 0;
const recentSlot = typeof params.recentSlot === "number" && Number.isFinite(params.recentSlot) ? Math.max(1, Math.floor(params.recentSlot)) : void 0;
const parts = [
`${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:c=${encodeCustomIdValue(params.command)}`,
`a=${encodeCustomIdValue(params.action)}`,
`v=${encodeCustomIdValue(params.view)}`,
`u=${encodeCustomIdValue(userId)}`,
`g=${String(page)}`
];
if (normalizedProvider) parts.push(`p=${encodeCustomIdValue(normalizedProvider)}`);
const runtime = params.runtime?.trim();
if (runtime) parts.push(`r=${encodeCustomIdValue(runtime)}`);
const runtimeIndex = typeof params.runtimeIndex === "number" && Number.isFinite(params.runtimeIndex) ? Math.max(1, Math.floor(params.runtimeIndex)) : void 0;
if (runtimeIndex) parts.push(`ri=${String(runtimeIndex)}`);
if (providerPage) parts.push(`pp=${String(providerPage)}`);
if (modelIndex) parts.push(`mi=${String(modelIndex)}`);
if (recentSlot) parts.push(`rs=${String(recentSlot)}`);
const providerBucket = params.providerBucket?.trim().toLowerCase();
if (providerBucket) parts.push(`pb=${encodeCustomIdValue(providerBucket)}`);
const modelBucket = params.modelBucket?.trim().toLowerCase();
if (modelBucket) parts.push(`mb=${encodeCustomIdValue(modelBucket)}`);
const customId = parts.join(";");
if (customId.length > 100) throw new Error(`Discord model picker custom_id exceeds 100 chars (${customId.length})`);
return customId;
}
function parseDiscordModelPickerData(data) {
if (!data || typeof data !== "object") return null;
const command = decodeCustomIdValue$1(coerceString(data.c ?? data.cmd));
const action = decodeCustomIdValue$1(coerceString(data.a ?? data.act));
const view = decodeCustomIdValue$1(coerceString(data.v ?? data.view));
const userId = decodeCustomIdValue$1(coerceString(data.u));
const providerRaw = decodeCustomIdValue$1(coerceString(data.p));
const runtimeRaw = decodeCustomIdValue$1(coerceString(data.r));
const runtimeIndex = parseRawPositiveInt(data.ri);
const page = parseRawPage(data.g ?? data.pg);
const providerPage = parseRawPositiveInt(data.pp);
const modelIndex = parseRawPositiveInt(data.mi);
const recentSlot = parseRawPositiveInt(data.rs);
const providerBucketRaw = decodeCustomIdValue$1(coerceString(data.pb)).trim().toLowerCase();
const modelBucketRaw = decodeCustomIdValue$1(coerceString(data.mb)).trim().toLowerCase();
if (!isValidCommandContext(command) || !isValidPickerAction(action) || !isValidPickerView(view)) return null;
const trimmedUserId = userId.trim();
if (!trimmedUserId) return null;
return {
command,
action,
view,
userId: trimmedUserId,
provider: providerRaw ? normalizeProviderId(providerRaw) : void 0,
runtime: runtimeRaw.trim() || void 0,
...typeof runtimeIndex === "number" ? { runtimeIndex } : {},
page,
...typeof providerPage === "number" ? { providerPage } : {},
...typeof modelIndex === "number" ? { modelIndex } : {},
...typeof recentSlot === "number" ? { recentSlot } : {},
...providerBucketRaw ? { providerBucket: providerBucketRaw } : {},
...modelBucketRaw ? { modelBucket: modelBucketRaw } : {}
};
}
/**
* Split a sorted item list into letter-range buckets when its length exceeds
* {@link DISCORD_MODEL_PICKER_BUCKET_THRESHOLD}. Items below the threshold
* return a single "All" bucket so callers can render the same code path.
*
* The boundary extender keeps items sharing the same starting letter inside
* the same bucket — selecting "A–G" never strands a stray "g" item in the
* next bucket. If every item shares a first letter (e.g. all `qwen3-*`),
* the function falls back to count-based numeric chunks so the user still
* gets a finite-cardinality picker.
*/
function computeAlphaBuckets(sortedItems) {
if (sortedItems.length === 0) return [];
if (sortedItems.length <= 25) return [{
id: "all",
label: `All (${sortedItems.length})`,
start: 0,
end: sortedItems.length
}];
const firstLetter = (value) => value.charAt(0).toLowerCase();
if (sortedItems.every((item) => firstLetter(item) === firstLetter(sortedItems[0]))) return chunkBucketsByCount(sortedItems);
const buckets = [];
const target = computeBucketTargetSize(sortedItems.length);
let start = 0;
while (start < sortedItems.length) {
let end = Math.min(sortedItems.length, start + target);
if (end < sortedItems.length) {
const last = firstLetter(sortedItems[end - 1]);
while (end < sortedItems.length && firstLetter(sortedItems[end]) === last) end += 1;
}
const startLetter = firstLetter(sortedItems[start]);
const endLetter = firstLetter(sortedItems[end - 1]);
const id = startLetter === endLetter ? startLetter : `${startLetter}-${endLetter}`;
const label = startLetter === endLetter ? `${startLetter.toUpperCase()} (${end - start})` : `${startLetter.toUpperCase()}–${endLetter.toUpperCase()} (${end - start})`;
buckets.push({
id,
label,
start,
end
});
start = end;
}
return buckets;
}
/**
* Pick the per-bucket target size such that the resulting bucket count never
* exceeds {@link DISCORD_COMPONENT_MAX_SELECT_OPTIONS} (Discord's hard select
* cap). Stays at the default {@link DISCORD_MODEL_PICKER_BUCKET_TARGET_SIZE}
* for typical inputs and grows linearly for very large lists.
*/
function computeBucketTargetSize(totalItems) {
const minTarget = 20;
const capByBucketCount = Math.ceil(totalItems / 25);
return Math.max(minTarget, capByBucketCount);
}
function chunkBucketsByCount(sortedItems) {
const buckets = [];
const target = computeBucketTargetSize(sortedItems.length);
for (let start = 0; start < sortedItems.length; start += target) {
const end = Math.min(sortedItems.length, start + target);
buckets.push({
id: `${start + 1}-${end}`,
label: `${start + 1}–${end} (${end - start})`,
start,
end
});
}
return buckets;
}
/**
* Resolve a bucket from a list given a (possibly user-supplied) bucket id.
* Falls back to the first bucket when the id does not match — mirrors the
* "bad customId → reset to defaults" semantics already used for other
* state fields.
*/
function resolveBucket(buckets, id) {
if (buckets.length === 0) return null;
if (!id) return buckets[0];
return buckets.find((bucket) => bucket.id === id) ?? buckets[0];
}
/**
* Derive the alpha-bucket id that contains a given provider id. Returns
* `undefined` when bucketing is inactive (all providers fit in one bucket)
* or the provider is unknown. Used by the interaction handler to recompute
* `providerBucket` at re-render time without forcing every customId to
* carry the bucket field — the bucket is a pure function of the provider
* list + provider id.
*/
function findProviderBucketId(data, provider) {
return findProviderBucketLocation(data, provider)?.bucket;
}
function findProviderBucketLocation(data, provider) {
const normalized = normalizeProviderId(provider);
const sorted = [...data.providers].toSorted();
const idx = sorted.indexOf(normalized);
if (idx < 0) return;
const containing = computeAlphaBuckets(sorted).find((bucket) => idx >= bucket.start && idx < bucket.end);
if (!containing) return;
const page = Math.floor((idx - containing.start) / 25) + 1;
return {
...containing.id !== "all" ? { bucket: containing.id } : {},
page
};
}
/**
* Derive the alpha-bucket id that contains a given model id within the
* named provider. Same rationale as {@link findProviderBucketId} — saves
* customId budget by recomputing the bucket from the durable state
* (provider + model) rather than carrying it as a parameter.
*/
function findModelBucketId(data, provider, model) {
const modelSet = data.byProvider.get(normalizeProviderId(provider));
if (!modelSet) return;
const sorted = [...modelSet].toSorted(compareBucketItems);
const idx = sorted.indexOf(model);
if (idx < 0) return;
const containing = computeAlphaBuckets(sorted).find((bucket) => idx >= bucket.start && idx < bucket.end);
return containing && containing.id !== "all" ? containing.id : void 0;
}
function buildDiscordModelPickerProviderItems(data) {
return [...data.providers].toSorted().map((provider) => ({
id: provider,
count: data.byProvider.get(provider)?.size ?? 0
}));
}
function getDiscordModelPickerProviderPage(params) {
const allItems = buildDiscordModelPickerProviderItems(params.data);
const buckets = computeAlphaBuckets(allItems.map((item) => item.id));
const bucket = resolveBucket(buckets, params.bucket);
const bucketItems = bucket ? allItems.slice(bucket.start, bucket.end) : allItems;
const pageSize = clampPageSize(params.pageSize, 25, 25);
return {
...paginateItems({
items: bucketItems,
page: normalizeModelPickerPage(params.page),
pageSize
}),
bucket,
buckets
};
}
function getDiscordModelPickerModelPage(params) {
const provider = normalizeProviderId(params.provider);
const modelSet = params.data.byProvider.get(provider);
if (!modelSet) return null;
const allModels = [...modelSet].toSorted(compareBucketItems);
const buckets = computeAlphaBuckets(allModels);
const bucket = resolveBucket(buckets, params.bucket);
const bucketItems = bucket ? allModels.slice(bucket.start, bucket.end) : allModels;
const pageSize = clampPageSize(params.pageSize, 25, 25);
return {
...paginateItems({
items: bucketItems,
page: normalizeModelPickerPage(params.page),
pageSize
}),
provider,
bucket,
buckets
};
}
function resolveDiscordModelPickerPageForModel(params) {
const provider = normalizeProviderId(params.provider);
const modelSet = params.data.byProvider.get(provider);
if (!modelSet) return { page: 1 };
const sorted = [...modelSet].toSorted(compareBucketItems);
const index = sorted.indexOf(params.model);
if (index < 0) return { page: 1 };
const pageSize = clampPageSize(params.pageSize, 25, 25);
const containingBucket = computeAlphaBuckets(sorted).find((bucket) => index >= bucket.start && index < bucket.end);
if (!containingBucket) return { page: Math.floor(index / pageSize) + 1 };
const offsetInBucket = index - containingBucket.start;
return {
page: Math.floor(offsetInBucket / pageSize) + 1,
bucket: containingBucket.id === "all" ? void 0 : containingBucket.id
};
}
//#endregion
//#region extensions/discord/src/monitor/model-picker.view.ts
const DISCORD_MODEL_PICKER_PAGE_INDICATOR_CUSTOM_ID = "mdlpk:nav-indicator";
function parseCurrentModelRef(raw) {
const match = (raw?.trim())?.match(/^([^/]+)\/(.+)$/u);
if (!match) return null;
const provider = normalizeProviderId(match[1]);
const model = match[2];
if (!provider || !model) return null;
return {
provider,
model
};
}
function formatCurrentModelLine(currentModel) {
const parsed = parseCurrentModelRef(currentModel);
if (!parsed) return "Current model: default";
return `Current model: ${parsed.provider}/${parsed.model}`;
}
function createModelPickerButton(params) {
class DiscordModelPickerButton extends Button {
constructor(..._args) {
super(..._args);
this.label = params.label;
this.customId = params.customId;
this.style = params.style ?? ButtonStyle.Secondary;
this.disabled = params.disabled ?? false;
}
}
return new DiscordModelPickerButton();
}
function createModelSelect(params) {
class DiscordModelPickerSelect extends StringSelectMenu {
constructor(..._args2) {
super(..._args2);
this.customId = params.customId;
this.options = params.options;
this.minValues = 1;
this.maxValues = 1;
this.placeholder = params.placeholder;
this.disabled = params.disabled ?? false;
}
}
return new DiscordModelPickerSelect();
}
/**
* Build the alpha-bucket select row that appears above the provider/model
* surface when the list exceeds {@link DISCORD_MODEL_PICKER_BUCKET_THRESHOLD}.
*
* Selecting a bucket emits action=bucket. The chosen bucket travels in the
* select value, while the custom_id carries only the stable context needed to
* rebuild the picker under Discord's 100-character custom_id limit.
*/
function buildBucketSelectRow(params) {
if (params.buckets.length <= 1) return null;
const options = params.buckets.map((bucket) => ({
label: bucket.label,
value: bucket.id,
default: bucket.id === params.currentBucketId
}));
return new Row([createModelSelect({
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "bucket",
view: params.view,
userId: params.userId,
page: 1,
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
providerPage: params.providerPage,
modelIndex: params.modelIndex
}),
options,
placeholder: params.view === "providers" ? "Filter providers by letter range" : "Filter models by letter range"
})]);
}
function getRuntimeChoices(params) {
const choices = params.data.runtimeChoicesByProvider?.get(normalizeProviderId(params.provider));
if (choices?.length) return choices;
return [{
id: "openclaw",
label: "OpenClaw Default",
description: "Use the built-in OpenClaw runtime."
}];
}
function resolveSelectedRuntime(params) {
const choices = getRuntimeChoices({
data: params.data,
provider: params.provider
});
const allowed = new Set(choices.map((choice) => choice.id));
const pending = params.pendingRuntime?.trim();
if (pending && allowed.has(pending)) return pending;
const current = params.currentRuntime?.trim();
if (current && allowed.has(current)) return current;
return choices[0]?.id ?? "openclaw";
}
function resolveExplicitRuntimeState(params) {
const allowed = new Set(params.choices.map((choice) => choice.id));
const pending = params.pendingRuntime?.trim();
if (pending && allowed.has(pending)) return pending;
const current = params.currentRuntime?.trim();
if (current && current !== "auto" && current !== "default" && allowed.has(current)) return current;
}
function getActiveBucketId(bucket) {
return bucket && bucket.id !== "all" ? bucket.id : void 0;
}
function resolveCompactRuntimeState(params) {
const stateRuntime = resolveExplicitRuntimeState(params);
const stateRuntimeIndex = stateRuntime ? params.choices.findIndex((choice) => choice.id === stateRuntime) : -1;
if (stateRuntimeIndex >= 0) return { runtimeIndex: stateRuntimeIndex + 1 };
return stateRuntime ? { runtime: stateRuntime } : {};
}
function buildRenderedShell(params) {
if (params.layout === "classic") return {
layout: "classic",
content: [
params.title,
...params.detailLines,
"",
params.footer
].filter(Boolean).join("\n"),
components: params.rows
};
const containerComponents = [new TextDisplay(`## ${params.title}`)];
if (params.detailLines.length > 0) containerComponents.push(new TextDisplay(params.detailLines.join("\n")));
containerComponents.push(new Separator({
divider: true,
spacing: "small"
}));
if (params.preRowText) containerComponents.push(new TextDisplay(params.preRowText));
containerComponents.push(...params.rows);
if (params.trailingRows && params.trailingRows.length > 0) {
containerComponents.push(new Separator({
divider: true,
spacing: "small"
}));
containerComponents.push(...params.trailingRows);
}
if (params.footer) {
containerComponents.push(new Separator({
divider: false,
spacing: "small"
}));
containerComponents.push(new TextDisplay(`-# ${params.footer}`));
}
return {
layout: "v2",
components: [new Container(containerComponents)]
};
}
function buildProviderSelectRow(params) {
if (params.page.items.length === 0) return null;
const options = params.page.items.map((provider) => ({
label: provider.id,
value: provider.id,
default: provider.id === params.currentProvider,
description: `${provider.count} ${provider.count === 1 ? "model" : "models"}`
}));
return new Row([createModelSelect({
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "provider",
view: "models",
page: params.page.page,
providerPage: params.page.page,
providerBucket: params.providerBucket,
userId: params.userId
}),
options,
placeholder: "Select provider"
})]);
}
function buildPaginationRow(params) {
if (params.totalPages <= 1) return null;
return new Row([
createModelPickerButton({
label: "◀ Prev",
style: ButtonStyle.Secondary,
disabled: !params.hasPrev,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "nav",
view: params.view,
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
page: Math.max(1, params.page - 1),
providerPage: params.providerPage,
modelIndex: params.modelIndex,
providerBucket: params.providerBucket,
modelBucket: params.modelBucket,
userId: params.userId
})
}),
createModelPickerButton({
label: `Page ${params.page}/${params.totalPages}`,
style: ButtonStyle.Secondary,
disabled: true,
customId: DISCORD_MODEL_PICKER_PAGE_INDICATOR_CUSTOM_ID
}),
createModelPickerButton({
label: "Next ▶",
style: ButtonStyle.Secondary,
disabled: !params.hasNext,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "nav",
view: params.view,
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
page: Math.min(params.totalPages, params.page + 1),
providerPage: params.providerPage,
modelIndex: params.modelIndex,
providerBucket: params.providerBucket,
modelBucket: params.modelBucket,
userId: params.userId
})
})
]);
}
function buildModelRows(params) {
const parsedCurrentModel = parseCurrentModelRef(params.currentModel);
const parsedPendingModel = parseCurrentModelRef(params.pendingModel);
const rows = [];
const hasQuickModels = (params.quickModels ?? []).length > 0;
const providerPage = getDiscordModelPickerProviderPage({
data: params.data,
page: params.providerPage,
bucket: params.providerBucket
});
const providerOptions = providerPage.items.map((provider) => ({
label: provider.id,
value: provider.id,
default: provider.id === params.modelPage.provider
}));
const activeProviderBucket = getActiveBucketId(providerPage.bucket);
const activeModelBucket = getActiveBucketId(params.modelPage.bucket);
if (!((params.modelPage.buckets?.length ?? 0) > 1)) rows.push(new Row([createModelSelect({
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "provider",
view: "models",
provider: params.modelPage.provider,
page: providerPage.page,
providerPage: providerPage.page,
providerBucket: activeProviderBucket,
userId: params.userId
}),
options: providerOptions,
placeholder: "Select provider"
})]));
const runtimeChoices = getRuntimeChoices({
data: params.data,
provider: params.modelPage.provider
});
const selectedRuntime = resolveSelectedRuntime({
data: params.data,
provider: params.modelPage.provider,
currentRuntime: params.currentRuntime,
pendingRuntime: params.pendingRuntime
});
const compactRuntime = resolveCompactRuntimeState({
choices: runtimeChoices,
currentRuntime: params.currentRuntime,
pendingRuntime: params.pendingRuntime
});
if (runtimeChoices.length > 1) rows.push(new Row([createModelSelect({
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "runtime",
view: "models",
provider: params.modelPage.provider,
page: params.modelPage.page,
providerPage: providerPage.page,
modelIndex: params.pendingModelIndex,
...params.pendingModelIndex === void 0 && activeModelBucket ? { modelBucket: activeModelBucket } : {},
userId: params.userId
}),
options: runtimeChoices.map((choice) => {
const option = {
label: choice.label,
value: choice.id,
default: choice.id === selectedRuntime
};
if (choice.description) option.description = choice.description;
return option;
}),
placeholder: "Select runtime"
})]));
const selectedModelRef = parsedPendingModel ?? parsedCurrentModel;
const modelOptions = params.modelPage.items.map((model) => ({
label: model,
value: model,
default: selectedModelRef ? selectedModelRef.provider === params.modelPage.provider && selectedModelRef.model === model : false
}));
rows.push(new Row([createModelSelect({
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "model",
view: "models",
provider: params.modelPage.provider,
...compactRuntime,
page: params.modelPage.page,
providerPage: providerPage.page,
userId: params.userId
}),
options: modelOptions,
placeholder: `Select ${params.modelPage.provider} model`
})]));
const modelNavRow = buildPaginationRow({
command: params.command,
userId: params.userId,
view: "models",
page: params.modelPage.page,
totalPages: params.modelPage.totalPages,
hasPrev: params.modelPage.hasPrev,
hasNext: params.modelPage.hasNext,
provider: params.modelPage.provider,
...compactRuntime,
providerPage: providerPage.page,
modelIndex: params.pendingModelIndex,
modelBucket: params.modelPage.bucket && params.modelPage.bucket.id !== "all" ? params.modelPage.bucket.id : void 0
});
if (modelNavRow) rows.push(modelNavRow);
const resolvedDefault = params.data.resolvedDefault;
const shouldDisableReset = Boolean(parsedCurrentModel) && parsedCurrentModel?.provider === resolvedDefault.provider && parsedCurrentModel?.model === resolvedDefault.model;
const hasPendingSelection = Boolean(parsedPendingModel) && parsedPendingModel?.provider === params.modelPage.provider && typeof params.pendingModelIndex === "number" && params.pendingModelIndex > 0;
const buttonRowItems = [
createModelPickerButton({
label: "Providers",
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "back",
view: "providers",
page: providerPage.page,
providerBucket: activeProviderBucket,
userId: params.userId
})
}),
createModelPickerButton({
label: "Cancel",
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "cancel",
view: "models",
provider: params.modelPage.provider,
...compactRuntime,
page: params.modelPage.page,
providerPage: providerPage.page,
userId: params.userId
})
}),
createModelPickerButton({
label: "Reset to default",
style: ButtonStyle.Secondary,
disabled: shouldDisableReset,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "reset",
view: "models",
provider: params.modelPage.provider,
...compactRuntime,
page: params.modelPage.page,
providerPage: providerPage.page,
userId: params.userId
})
})
];
if (hasQuickModels) buttonRowItems.push(createModelPickerButton({
label: "Recents",
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "recents",
view: "recents",
provider: params.modelPage.provider,
...compactRuntime,
page: params.modelPage.page,
providerPage: providerPage.page,
modelBucket: activeModelBucket,
userId: params.userId
})
}));
buttonRowItems.push(createModelPickerButton({
label: "Submit",
style: ButtonStyle.Primary,
disabled: !hasPendingSelection,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "submit",
view: "models",
provider: params.modelPage.provider,
...compactRuntime,
page: params.modelPage.page,
providerPage: providerPage.page,
modelIndex: params.pendingModelIndex,
userId: params.userId
})
}));
return {
rows,
buttonRow: new Row(buttonRowItems)
};
}
function renderDiscordModelPickerProvidersView(params) {
const page = getDiscordModelPickerProviderPage({
data: params.data,
page: params.page,
bucket: params.providerBucket
});
const parsedCurrent = parseCurrentModelRef(params.currentModel);
const rows = [];
const bucketRow = buildBucketSelectRow({
command: params.command,
userId: params.userId,
view: "providers",
buckets: page.buckets,
currentBucketId: page.bucket?.id
});
if (bucketRow) rows.push(bucketRow);
const activeProviderBucket = page.bucket && page.bucket.id !== "all" ? page.bucket.id : void 0;
const providerRow = buildProviderSelectRow({
command: params.command,
userId: params.userId,
page,
currentProvider: parsedCurrent?.provider,
providerBucket: activeProviderBucket
});
if (providerRow) rows.push(providerRow);
const navRow = buildPaginationRow({
command: params.command,
userId: params.userId,
view: "providers",
page: page.page,
totalPages: page.totalPages,
hasPrev: page.hasPrev,
hasNext: page.hasNext,
providerBucket: activeProviderBucket
});
if (navRow) rows.push(navRow);
const totalProviders = params.data.providers.length;
const detailLines = [formatCurrentModelLine(params.currentModel), page.bucket && page.bucket.id !== "all" ? `Select a provider (${page.totalItems} in ${page.bucket.label}, ${totalProviders} total).` : `Select a provider (${page.totalItems} available).`];
const footer = page.totalPages > 1 ? `Showing page ${page.page}/${page.totalPages} · ${page.totalItems} providers total` : `All ${page.totalItems} providers shown`;
return buildRenderedShell({
layout: params.layout ?? "v2",
title: "Model Picker",
detailLines,
rows,
footer
});
}
function renderDiscordModelPickerModelsView(params) {
const providerPage = normalizeModelPickerPage(params.providerPage);
const modelPage = getDiscordModelPickerModelPage({
data: params.data,
provider: params.provider,
page: params.page,
bucket: params.modelBucket
});
if (!modelPage) {
const rows = [new Row([createModelPickerButton({
label: "Back",
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "back",
view: "providers",
page: providerPage,
userId: params.userId
})
})])];
return buildRenderedShell({
layout: params.layout ?? "v2",
title: "Model Picker",
detailLines: [formatCurrentModelLine(params.currentModel), `Provider not found: ${normalizeProviderId(params.provider)}`],
rows,
footer: "Choose a different provider."
});
}
const { rows: modelRows, buttonRow } = buildModelRows({
command: params.command,
userId: params.userId,
data: params.data,
providerPage,
modelPage,
currentModel: params.currentModel,
currentRuntime: params.currentRuntime,
pendingModel: params.pendingModel,
pendingModelIndex: params.pendingModelIndex,
pendingRuntime: params.pendingRuntime,
quickModels: params.quickModels,
providerBucket: params.providerBucket
});
const runtimeChoices = getRuntimeChoices({
data: params.data,
provider: modelPage.provider
});
const pendingRuntime = params.pendingRuntime?.trim();
const pendingRuntimeIndex = pendingRuntime ? runtimeChoices.findIndex((choice) => choice.id === pendingRuntime) : -1;
const rows = [];
const bucketRow = buildBucketSelectRow({
command: params.command,
userId: params.userId,
view: "models",
buckets: modelPage.buckets,
currentBucketId: modelPage.bucket?.id,
provider: modelPage.provider,
runtimeIndex: pendingRuntimeIndex >= 0 ? pendingRuntimeIndex + 1 : void 0,
providerPage,
providerBucket: params.providerBucket
});
if (bucketRow) rows.push(bucketRow);
rows.push(...modelRows);
const defaultModel = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
const pendingLine = params.pendingModel ? `Selected: ${params.pendingModel} · runtime ${resolveSelectedRuntime({
data: params.data,
provider: modelPage.provider,
currentRuntime: params.currentRuntime,
pendingRuntime: params.pendingRuntime
})} (press Submit)` : "Select a model, then press Submit.";
const detailLines = [formatCurrentModelLine(params.currentModel), `Default: ${defaultModel}`];
if (modelPage.totalPages > 1) detailLines.push(`${modelPage.provider}: page ${modelPage.page}/${modelPage.totalPages} · ${modelPage.totalItems} models`);
return buildRenderedShell({
layout: params.layout ?? "v2",
title: "Model Picker",
detailLines,
preRowText: pendingLine,
rows,
trailingRows: [buttonRow]
});
}
function formatRecentsButtonLabel(modelRef, suffix) {
const maxLen = 80;
const label = suffix ? `${modelRef} ${suffix}` : modelRef;
if (label.length <= maxLen) return label;
return suffix ? `${modelRef.slice(0, maxLen - suffix.length - 2)}… ${suffix}` : `${modelRef.slice(0, maxLen - 1)}…`;
}
function renderDiscordModelPickerRecentsView(params) {
const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
const rows = [];
const dedupedQuickModels = params.quickModels.filter((modelRef) => modelRef !== defaultModelRef);
rows.push(new Row([createModelPickerButton({
label: formatRecentsButtonLabel(defaultModelRef, "(default)"),
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "submit",
view: "recents",
recentSlot: 1,
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
page: params.page,
providerPage: params.providerPage,
userId: params.userId
})
})]));
for (let i = 0; i < dedupedQuickModels.length; i++) {
const modelRef = dedupedQuickModels[i];
rows.push(new Row([createModelPickerButton({
label: formatRecentsButtonLabel(modelRef),
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "submit",
view: "recents",
recentSlot: i + 2,
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
page: params.page,
providerPage: params.providerPage,
userId: params.userId
})
})]));
}
const backRow = new Row([createModelPickerButton({
label: "Back",
style: ButtonStyle.Secondary,
customId: buildDiscordModelPickerCustomId({
command: params.command,
action: "back",
view: "models",
provider: params.provider,
runtime: params.runtime,
runtimeIndex: params.runtimeIndex,
page: params.page,
providerPage: params.providerPage,
modelBucket: params.modelBucket,
userId: params.userId
})
})]);
return buildRenderedShell({
layout: params.layout ?? "v2",
title: "Recents",
detailLines: ["Models you've previously selected appear here.", formatCurrentModelLine(params.currentModel)],
preRowText: "Tap a model to switch.",
rows,
trailingRows: [backRow]
});
}
function toDiscordModelPickerMessagePayload(view) {
if (view.layout === "classic") return {
content: view.content,
components: view.components
};
return { components: view.components };
}
//#endregion
//#region extensions/discord/src/monitor/native-command-model-picker-apply.ts
async function persistDiscordModelPickerOverride(params) {
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId });
let persisted = false;
await updateSessionStore(storePath, (store) => {
const entry = store[params.route.sessionKey] ?? {
sessionId: randomUUID(),
updatedAt: Date.now()
};
store[params.route.sessionKey] = entry;
persisted = applyModelOverrideToSessionEntry({
entry,
selection: {
provider: params.provider,
model: params.model,
isDefault: params.isDefault
},
markLiveSwitchPending: true
}).updated || persisted;
const runtime = params.runtime?.trim();
if (runtime && runtime !== "auto" && runtime !== "default") {
if (entry.agentRuntimeOverride !== runtime) {
entry.agentRuntimeOverride = runtime;
delete entry.agentHarnessId;
persisted = true;
}
} else if (runtime && entry.agentRuntimeOverride) {
delete entry.agentRuntimeOverride;
delete entry.agentHarnessId;
persisted = true;
}
});
return persisted;
}
async function applyDiscordModelPickerSelection(params) {
try {
const dispatchResult = await withTimeout(params.dispatchCommandInteraction({
interaction: params.interaction,
prompt: params.selectionCommand.prompt,
command: params.selectionCommand.command,
commandArgs: params.selectionCommand.args,
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
preferFollowUp: true,
threadBindings: params.threadBindings,
suppressReplies: true
}), 12e3);
if (!dispatchResult.accepted) return {
status: "rejected",
noticeMessage: `❌ Failed to apply ${params.resolvedModelRef}. Try /model ${params.resolvedModelRef} directly.`
};
const fallbackRoute = dispatchResult.effectiveRoute ?? params.route;
if (params.settleMs > 0) await new Promise((resolve) => {
setTimeout(resolve, params.settleMs);
});
let effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
let persisted = effectiveModelRef === params.resolvedModelRef;
if (params.selectedRuntime?.trim()) {
await persistDiscordModelPickerOverride({
cfg: params.cfg,
route: fallbackRoute,
provider: params.selectedProvider,
model: params.selectedModel,
isDefault: params.selectedProvider === params.defaultProvider && params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
}
if (!persisted) {
logVerbose(`discord: model picker override mismatch — expected ${params.resolvedModelRef} but read ${effectiveModelRef} from session key ${fallbackRoute.sessionKey}; attempting direct session override persist`);
try {
const directlyPersisted = await persistDiscordModelPickerOverride({
cfg: params.cfg,
route: fallbackRoute,
provider: params.selectedProvider,
model: params.selectedModel,
isDefault: params.selectedProvider === params.defaultProvider && params.selectedModel === params.defaultModel,
runtime: params.selectedRuntime
});
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
persisted = effectiveModelRef === params.resolvedModelRef;
if (!persisted) logVerbose(`discord: direct session override persist failed — expected ${params.resolvedModelRef} but read ${effectiveModelRef} from session key ${fallbackRoute.sessionKey}`);
else if (!directlyPersisted) logVerbose(`discord: direct session override persist became a no-op because ${params.resolvedModelRef} was already present on re-read for session key ${fallbackRoute.sessionKey}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logVerbose(`discord: direct session override persist threw for session key ${fallbackRoute.sessionKey}: ${message}`);
}
}
if (persisted) await recordDiscordModelPickerRecentModel({
scope: params.preferenceScope,
modelRef: params.resolvedModelRef,
limit: 5
}).catch(() => void 0);
return persisted ? {
status: "success",
effectiveModelRef,
noticeMessage: `✅ Model set to ${params.resolvedModelRef}.`
} : {
status: "mismatch",
effectiveModelRef,
noticeMessage: `⚠️ Tried to set ${params.resolvedModelRef}, but current model is ${effectiveModelRef}.`
};
} catch (error) {
if (error instanceof Error && error.message === "timeout") return {
status: "timeout",
noticeMessage: `⏳ Model change to ${params.resolvedModelRef} is still processing. Check /status in a few seconds.`
};
return {
status: "failed",
noticeMessage: `❌ Failed to apply ${params.resolvedModelRef}. Try /model ${params.resolvedModelRef} directly.`
};
}
}
//#endregion
//#region extensions/discord/src/monitor/native-command-model-picker-ui.ts
function resolveDiscordModelPickerCommandContext(command) {
const normalized = normalizeLowercaseStringOrEmpty(command.nativeName ?? command.key);
if (normalized === "model" || normalized === "models") return normalized;
return null;
}
function resolveCommandArgStringValue(args, key) {
const value = args?.values?.[key];
if (typeof value !== "string") return "";
return value.trim();
}
function shouldOpenDiscordModelPickerFromCommand(params) {
const context = resolveDiscordModelPickerCommandContext(params.command);
if (!context) return null;
const serializedArgs = normalizeOptionalString(serializeCommandArgs(params.command, params.commandArgs)) ?? "";
if (context === "model") return !resolveCommandArgStringValue(params.commandArgs, "model") && !serializedArgs ? context : null;
return serializedArgs ? null : context;
}
function buildDiscordModelPickerCurrentModel(defaultProvider, defaultModel) {
return `${defaultProvider}/${defaultModel}`;
}
function buildDiscordModelPickerAllowedModelRefs(data) {
const out = /* @__PURE__ */ new Set();
for (const provider of data.providers) {
const models = data.byProvider.get(provider);
if (!models) continue;
for (const model of models) out.add(`${provider}/${model}`);
}
return out;
}
function resolveDiscordModelPickerPreferenceScope(params) {
return {
accountId: params.accountId,
guildId: params.interaction.guild?.id ?? void 0,
userId: params.userId
};
}
function buildDiscordModelPickerNoticePayload(message) {
return { components: [new Container([new TextDisplay(message)])] };
}
async function resolveDiscordModelPickerRouteState(params) {
const { interaction, cfg, accountId } = params;
const { isDirectMessage, isGroupDm, isThreadChannel, rawChannelId, threadParentId } = await resolveDiscordNativeInteractionChannelContext({
channel: interaction.channel,
client: interaction.client,
hasGuild: Boolean(interaction.guild),
channelIdFallback: "unknown"
});
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
const threadBinding = isThreadChannel ? params.threadBindings.getByThreadId(rawChannelId) : void 0;
return await resolveDiscordNativeInteractionRouteState({
cfg,
accountId,
guildId: interaction.guild?.id ?? void 0,
memberRoleIds,
isDirectMessage,
isGroupDm,
directUserId: interaction.user?.id ?? rawChannelId,
conversationId: rawChannelId,
parentConversationId: threadParentId,
threadBinding,
enforceConfiguredBindingReadiness: params.enforceConfiguredBindingReadiness
});
}
async function resolveDiscordModelPickerRoute(params) {
return (await resolveDiscordModelPickerRouteState(params)).effectiveRoute;
}
async function resolveDiscordNativeChoiceContext(params) {
try {
const resolved = await resolveDiscordModelPickerRouteState({
interaction: params.interaction,
cfg: params.cfg,
accountId: params.accountId,
threadBindings: params.threadBindings,
enforceConfiguredBindingReadiness: true
});
if (resolved.bindingReadiness && !resolved.bindingReadiness.ok) return null;
const route = resolved.effectiveRoute;
const fallback = resolveDefaultModelForAgent({
cfg: params.cfg,
agentId: route.agentId
});
const sessionStore = loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId: route.agentId }));
const sessionEntry = sessionStore[route.sessionKey];
const override = resolveStoredModelOverride({
sessionEntry,
sessionStore,
sessionKey: route.sessionKey,
defaultProvider: fallback.provider
});
if (!override?.model) return {
provider: fallback.provider,
model: fallback.model
};
return {
provider: override.provider || fallback.provider,
model: override.model
};
} catch {
return null;
}
}
function resolveDiscordModelPickerCurrentModel(params) {
const fallback = buildDiscordModelPickerCurrentModel(params.data.resolvedDefault.provider, params.data.resolvedDefault.model);
try {
const sessionStore = loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId }), { skipCache: true });
const sessionEntry = sessionStore[params.route.sessionKey];
const override = resolveStoredModelOverride({
sessionEntry,
sessionStore,
sessionKey: params.route.sessionKey,
defaultProvider: params.data.resolvedDefault.provider
});
if (!override?.model) return fallback;
const provider = (override.provider || params.data.resolvedDefault.provider).trim();
if (!provider) return fallback;
return `${provider}/${override.model}`;
} catch {
return fallback;
}
}
function resolveDiscordModelPickerCurrentRuntime(params) {
try {
const sessionRuntime = normalizeOptionalString(loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId }), { skipCache: true })[params.route.sessionKey]?.agentRuntimeOverride);
if (sessionRuntime) return sessionRuntime;
} catch {}
return "auto";
}
async function replyWithDiscordModelPickerProviders(params) {
const route = await resolveDiscordModelPickerRoute({
interaction: params.interaction,
cfg: params.cfg,
accountId: params.accountId,
threadBindings: params.threadBindings
});
const data = await loadDiscordModelPickerData(params.cfg, route.agentId);
const currentModel = resolveDiscordModelPickerCurrentModel({
cfg: params.cfg,
route,
data
});
const currentRuntime = resolveDiscordModelPickerCurrentRuntime({
cfg: params.cfg,
route
});
const quickModels = await readDiscordModelPickerRecentModels({
scope: resolveDiscordModelPickerPreferenceScope({
interaction: params.interaction,
accountId: params.accountId,
userId: params.userId
}),
allowedModelRefs: buildDiscordModelPickerAllowedModelRefs(data),
limit: 5
});
const parsedCurrentRef = splitDiscordModelRef(currentModel ?? "");
const initialProvider = parsedCurrentRef && data.byProvider.has(parsedCurrentRef.provider) ? parsedCurrentRef.provider : data.providers[0] ?? data.resolvedDefault.provider;
const initialResolved = parsedCurrentRef && parsedCurrentRef.provider === initialProvider ? resolveDiscordModelPickerPageForModel({
data,
provider: initialProvider,
model: parsedCurrentRef.model
}) : { page: 1 };
const initialPage = initialResolved.page;
const initialModelBucket = initialResolved.bucket;
const initialProviderLocation = findProviderBucketLocation(data, initialProvider);
const payload = {
...toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: params.command,
userId: params.userId,
data,
provider: initialProvider,
page: initialPage,
providerPage: initialProviderLocation?.page ?? 1,
providerBucket: initialProviderLocation?.bucket,
modelBucket: initialModelBucket,
currentModel,
currentRuntime,
quickModels
})),
ephemeral: true
};
await params.safeInteractionCall("model picker reply", async () => {
if (params.preferFollowUp) {
await params.interaction.followUp(payload);
return;
}
await params.interaction.reply(payload);
});
}
function splitDiscordModelRef(modelRef) {
const trimmed = modelRef.trim();
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) return null;
const provider = trimmed.slice(0, slashIndex).trim();
const model = trimmed.slice(slashIndex + 1).trim();
if (!provider || !model) return null;
return {
provider,
model
};
}
//#endregion
//#region extensions/discord/src/monitor/native-command-model-picker-interaction.ts
function resolveModelPickerSelectionValue(interaction) {
const rawValues = interaction.values;
if (!Array.isArray(rawValues) || rawValues.length === 0) return null;
const first = rawValues[0];
if (typeof first !== "string") return null;
return first.trim() || null;
}
function resolveModelPickerRuntimeByIndex(params) {
if (!params.provider || typeof params.runtimeIndex !== "number") return;
return (params.data.runtimeChoicesByProvider?.get(params.provider))?.[params.runtimeIndex - 1]?.id;
}
function resolveModelPickerProvider(params) {
return params.parsedProvider ?? splitDiscordModelRef(params.currentModelRef ?? "")?.provider ?? params.data.resolvedDefault.provider;
}
function resolveSelectedBucket(interaction) {
const raw = resolveModelPickerSelectionValue(interaction)?.toLowerCase();
return raw && raw !== "all" ? raw : void 0;
}
function resolvePendingRuntime(params) {
return params.parsed.runtime ?? resolveModelPickerRuntimeByIndex({
data: params.data,
provider: params.provider,
runtimeIndex: params.parsed.runtimeIndex
});
}
function resolveParsedRuntimeForSubmission(params) {
if (params.parsed.runtime) return params.parsed.runtime;
if (params.parsed.provider !== params.selectedProvider) return;
return resolveModelPickerRuntimeByIndex({
data: params.data,
provider: params.selectedProvider,
runtimeIndex: params.parsed.runtimeIndex
});
}
function resolveSubmittedModelRef(params) {
if (params.parsed.action === "reset") return `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
if (params.parsed.action === "quick") {
const slot = params.parsed.recentSlot ?? 0;
return slot >= 1 ? params.quickModels[slot - 1] ?? null : null;
}
if (params.parsed.view === "recents") {
const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
const dedupedRecents = params.quickModels.filter((ref) => ref !== defaultModelRef);
const slot = params.parsed.recentSlot ?? 0;
if (slot === 1) return defaultModelRef;
return slot >= 2 ? dedupedRecents[slot - 2] ?? null : null;
}
const provider = params.parsed.provider;
const selectedModel = resolveDiscordModelPickerModelByIndex({
data: params.data,
provider: provider ?? "",
modelIndex: params.parsed.modelIndex
});
return provider && selectedModel ? `${provider}/${selectedModel}` : null;
}
function buildDiscordModelPickerSelectionCommand(params) {
const commandDefinition = findCommandByNativeName("model", "discord") ?? listChatCommands().find((entry) => entry.key === "model");
if (!commandDefinition) return null;
const commandArgs = {
values: { model: params.modelRef },
raw: params.modelRef
};
return {
command: commandDefinition,
args: commandArgs,
prompt: buildCommandTextFromArgs(commandDefinition, commandArgs)
};
}
function listDiscordModelPickerProviderModels(data, provider) {
const modelSet = data.byProvider.get(provider);
if (!modelSet) return [];
return [...modelSet].toSorted();
}
function resolveDiscordModelPickerModelIndex(params) {
const models = listDiscordModelPickerProviderModels(params.data, params.provider);
if (!models.length) return null;
const index = models.indexOf(params.model);
if (index < 0) return null;
return index + 1;
}
function resolveDiscordModelPickerModelByIndex(params) {
if (!params.modelIndex || params.modelIndex < 1) return null;
const models = listDiscordModelPickerProviderModels(params.data, params.provider);
if (!models.length) return null;
return models[params.modelIndex - 1] ?? null;
}
function resolveDiscordModelPickerRuntimeForProvider(params) {
const runtime = normalizeOptionalString(params.runtime);
if (!runtime) return;
if (runtime === "auto" || runtime === "default") return params.allowResetRuntime ? runtime : void 0;
const choices = params.data.runtimeChoicesByProvider?.get(params.provider);
if (!choices?.length) return runtime === "openclaw" ? runtime : void 0;
return choices.some((choice) => choice.id === runtime) ? runtime : void 0;
}
function resolveDiscordModelPickerSubmissionRuntime(params) {
return resolveDiscordModelPickerRuntimeForProvider({
data: params.data,
provider: params.provider,
runtime: params.parsedRuntime,
allowResetRuntime: true
}) ?? resolveDiscordModelPickerRuntimeForProvider({
data: params.data,
provider: params.provider,
runtime: params.currentRuntime
});
}
async function handleDiscordModelPickerInteraction(params) {
const { interaction, data, ctx } = params;
const parsed = parseDiscordModelPickerData(data);
if (!parsed) {
await params.safeInteractionCall("model picker update", () => interaction.update(buildDiscordModelPickerNoticePayload("Sorry, that model picker interaction is no longer available.")));
return;
}
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
await params.safeInteractionCall("model picker ack", () => interaction.acknowledge());
return;
}
let deferredUpdate = interaction.acknowledged;
if (!deferredUpdate) {
if (await params.safeInteractionCall("model picker defer", () => interaction.acknowledge()) === null) return;
deferredUpdate = true;
}
const route = await resolveDiscordModelPickerRoute({
interaction,
cfg: ctx.cfg,
accountId: ctx.accountId,
threadBindings: ctx.threadBindings
});
const pickerData = await loadDiscordModelPickerData(ctx.cfg, route.agentId);
const currentModelRef = resolveDiscordModelPickerCurrentModel({
cfg: ctx.cfg,
route,
data: pickerData
});
const currentRuntime = resolveDiscordModelPickerCurrentRuntime({
cfg: ctx.cfg,
route
});
const allowedModelRefs = buildDiscordModelPickerAllowedModelRefs(pickerData);
const preferenceScope = resolveDiscordModelPickerPreferenceScope({
interaction,
accountId: ctx.accountId,
userId: parsed.userId
});
const quickModels = await readDiscordModelPickerRecentModels({
scope: preferenceScope,
allowedModelRefs,
limit: 5
});
const updatePicker = async (payload) => await params.safeInteractionCall("model picker update", () => deferredUpdate ? interaction.editReply(payload) : interaction.update(payload));
const showNotice = async (message) => await updatePicker(buildDiscordModelPickerNoticePayload(message));
if (parsed.action === "recents") {
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerRecentsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
quickModels,
currentModel: currentModelRef,
runtime: parsed.runtime,
runtimeIndex: parsed.runtimeIndex,
provider: parsed.provider,
page: parsed.page,
providerPage: parsed.providerPage,
modelBucket: parsed.modelBucket
})));
return;
}
if (parsed.action === "back" && parsed.view === "providers") {
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
page: parsed.page,
providerBucket: parsed.providerBucket,
currentModel: currentModelRef
})));
return;
}
if (parsed.action === "nav" && parsed.view === "providers") {
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
page: parsed.page,
providerBucket: parsed.providerBucket,
currentModel: currentModelRef
})));
return;
}
if (parsed.action === "bucket" && parsed.view === "providers") {
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
page: 1,
providerBucket: resolveSelectedBucket(interaction),
currentModel: currentModelRef
})));
return;
}
if (parsed.action === "bucket" && parsed.view === "models") {
const provider = resolveModelPickerProvider({
parsedProvider: parsed.provider,
currentModelRef,
data: pickerData
});
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: 1,
providerPage: parsed.providerPage ?? 1,
providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
modelBucket: resolveSelectedBucket(interaction),
currentModel: currentModelRef,
currentRuntime,
pendingRuntime: resolvePendingRuntime({
data: pickerData,
provider,
parsed
}),
quickModels
})));
return;
}
if (parsed.action === "nav" && parsed.view === "models") {
const provider = resolveModelPickerProvider({
parsedProvider: parsed.provider,
currentModelRef,
data: pickerData
});
const pendingModel = resolveDiscordModelPickerModelByIndex({
data: pickerData,
provider,
modelIndex: parsed.modelIndex
});
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page,
providerPage: parsed.providerPage ?? 1,
providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
modelBucket: parsed.modelBucket,
currentModel: currentModelRef,
currentRuntime,
...pendingModel ? { pendingModel: `${provider}/${pendingModel}` } : {},
pendingModelIndex: parsed.modelIndex,
pendingRuntime: resolvePendingRuntime({
data: pickerData,
provider,
parsed
}),
quickModels
})));
return;
}
if (parsed.action === "back" && parsed.view === "models") {
const provider = resolveModelPickerProvider({
parsedProvider: parsed.provider,
currentModelRef,
data: pickerData
});
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page ?? 1,
providerPage: parsed.providerPage ?? 1,
providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
modelBucket: parsed.modelBucket,
currentModel: currentModelRef,
currentRuntime,
pendingRuntime: resolvePendingRuntime({
data: pickerData,
provider,
parsed
}),
quickModels
})));
return;
}
if (parsed.action === "provider") {
const selectedProvider = resolveModelPickerSelectionValue(interaction) ?? parsed.provider;
if (!selectedProvider || !pickerData.byProvider.has(selectedProvider)) {
await showNotice("Sorry, that provider isn't available anymore.");
return;
}
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider: selectedProvider,
page: 1,
providerPage: parsed.providerPage ?? parsed.page,
providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, selectedProvider),
currentModel: currentModelRef,
currentRuntime,
quickModels
})));
return;
}
if (parsed.action === "model") {
const selectedModel = resolveModelPickerSelectionValue(interaction);
const provider = parsed.provider;
if (!provider || !selectedModel) {
await showNotice("Sorry, I couldn't read that model selection.");
return;
}
const modelIndex = resolveDiscordModelPickerModelIndex({
data: pickerData,
provider,
model: selectedModel
});
if (!modelIndex) {
await showNotice("Sorry, that model isn't available anymore.");
return;
}
const modelRef = `${provider}/${selectedModel}`;
const derivedProviderBucket = parsed.providerBucket ?? findProviderBucketId(pickerData, provider);
const derivedModelBucket = parsed.modelBucket ?? findModelBucketId(pickerData, provider, selectedModel);
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page,
providerPage: parsed.providerPage ?? 1,
providerBucket: derivedProviderBucket,
modelBucket: derivedModelBucket,
currentModel: currentModelRef,
currentRuntime,
pendingModel: modelRef,
pendingModelIndex: modelIndex,
pendingRuntime: resolvePendingRuntime({
data: pickerData,
provider,
parsed
}),
quickModels
})));
return;
}
if (parsed.action === "runtime") {
const selectedRuntime = resolveModelPickerSelectionValue(interaction) ?? parsed.runtime ?? "auto";
const provider = parsed.provider;
if (!provider || !pickerData.byProvider.has(provider)) {
await showNotice("Sorry, that provider isn't available anymore.");
return;
}
const selectedModel = resolveDiscordModelPickerModelByIndex({
data: pickerData,
provider,
modelIndex: parsed.modelIndex
});
const pendingModel = selectedModel ? `${provider}/${selectedModel}` : void 0;
const derivedProviderBucket = parsed.providerBucket ?? findProviderBucketId(pickerData, provider);
const currentModelOnly = splitDiscordModelRef(currentModelRef ?? "");
const derivedModelBucket = parsed.modelBucket ?? (selectedModel ? findModelBucketId(pickerData, provider, selectedModel) : currentModelOnly && currentModelOnly.provider === provider ? findModelBucketId(pickerData, provider, currentModelOnly.model) : void 0);
await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page,
providerPage: parsed.providerPage ?? 1,
providerBucket: derivedProviderBucket,
modelBucket: derivedModelBucket,
currentModel: currentModelRef,
currentRuntime,
...pendingModel ? { pendingModel } : {},
pendingModelIndex: parsed.modelIndex,
pendingRuntime: selectedRuntime,
quickModels
})));
return;
}
if (parsed.action === "submit" || parsed.action === "reset" || parsed.action === "quick") {
const modelRef = resolveSubmittedModelRef({
data: pickerData,
parsed,
quickModels
});
const parsedModelRef = modelRef ? splitDiscordModelRef(modelRef) : null;
if (!parsedModelRef || !pickerData.byProvider.get(parsedModelRef.provider)?.has(parsedModelRef.model)) {
await showNotice("That selection expired. Please choose a model again.");
return;
}
const resolvedModelRef = `${parsedModelRef.provider}/${parsedModelRef.model}`;
const selectedRuntime = resolveDiscordModelPickerSubmissionRuntime({
data: pickerData,
provider: parsedModelRef.provider,
parsedRuntime: resolveParsedRuntimeForSubmission({
data: pickerData,
parsed,
selectedProvider: parsedModelRef.provider
}),
currentRuntime
});
const selectionCommand = buildDiscordModelPickerSelectionCommand({ modelRef: resolvedModelRef });
if (!selectionCommand) {
await showNotice("Sorry, /model is unavailable right now.");
return;
}
if (await showNotice(`Applying model change to ${resolvedModelRef}...`) === null) return;
const applyResult = await applyDiscordModelPickerSelection({
interaction,
selectionCommand,
dispatchCommandInteraction: params.dispatchCommandInteraction,
cfg: ctx.cfg,
discordConfig: ctx.discordConfig,
accountId: ctx.accountId,
sessionPrefix: ctx.sessionPrefix,
threadBindings: ctx.threadBindings,
route,
resolvedModelRef,
selectedProvider: parsedModelRef.provider,
selectedModel: parsedModelRef.model,
selectedRuntime,
defaultProvider: pickerData.resolvedDefault.provider,
defaultModel: pickerData.resolvedDefault.model,
preferenceScope,
settleMs: ctx.postApplySettleMs ?? 250,
resolveCurrentModel: (currentRoute) => resolveDiscordModelPickerCurrentModel({
cfg: ctx.cfg,
route: currentRoute,
data: pickerData
})
});
await params.safeInteractionCall("model picker follow-up", () => interaction.followUp({
...buildDiscordModelPickerNoticePayload(applyResult.noticeMessage),
ephemeral: true
}));
return;
}
if (parsed.action === "cancel") await showNotice(`ℹ️ Model kept as ${currentModelRef ?? "default"}.`);
}
async function runDiscordModelPickerFallback(params) {
await handleDiscordModelPickerInteraction(params);
}
var DiscordModelPickerFallbackButton = class extends Button {
constructor(params) {
super();
this.params = params;
this.label = "modelpick";
this.customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=btn`;
}
async run(interaction, data) {
await runDiscordModelPickerFallback({
...this.params,
interaction,
data
});
}
};
var DiscordModelPickerFallbackSelect = class extends StringSelectMenu {
constructor(params) {
super();
this.params = params;
this.customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=sel`;
this.options = [];
}
async run(interaction, data) {
await runDiscordModelPickerFallback({
...this.params,
interaction,
data
});
}
};
function createDiscordModelPickerFallbackButton$1(params) {
return new DiscordModelPickerFallbackButton(params);
}
function createDiscordModelPickerFallbackSelect$1(params) {
return new DiscordModelPickerFallbackSelect(params);
}
//#endregion
//#region extensions/discord/src/monitor/native-command.args.ts
function readDiscordCommandArgs(interaction, definitions) {
if (!definitions || definitions.length === 0) return;
const values = {};
for (const definition of definitions) {
let value;
if (definition.type === "number") value = interaction.options.getNumber(definition.name) ?? null;
else if (definition.type === "boolean") value = interaction.options.getBoolean(definition.name) ?? null;
else value = interaction.options.getString(definition.name) ?? null;
if (value != null) values[definition.name] = value;
}
return Object.keys(values).length > 0 ? { values } : void 0;
}
function createNativeCommandDefinition(command) {
return {
key: command.name,
nativeName: command.name,
description: command.description,
textAliases: [],
acceptsArgs: command.acceptsArgs,
args: command.args,
argsParsing: "none",
scope: "native"
};
}
//#endregion
//#region extensions/discord/src/monitor/native-command.options.ts
const log$1 = createSubsystemLogger("discord/native-command");
const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
function truncateDiscordCommandDescription(params) {
const { value, label } = params;
if (value.length <= DISCORD_COMMAND_DESCRIPTION_MAX) return value;
log$1.warn(`discord: truncating native command description (${label}) from ${value.length} to ${DISCORD_COMMAND_DESCRIPTION_MAX}: ${JSON.stringify(value)}`);
return value.slice(0, DISCORD_COMMAND_DESCRIPTION_MAX);
}
function truncateDiscordCommandDescriptionLocalizations(params) {
const entries = Object.entries(params.value ?? {});
if (entries.length === 0) return;
return Object.fromEntries(entries.map(([locale, description]) => [locale, truncateDiscordCommandDescription({
value: description,
label: `${params.label} locale:${locale}`
})]));
}
function resolveDiscordCommandLogLabel(command) {
if (typeof command.nativeName === "string" && command.nativeName.trim().length > 0) return command.nativeName;
return command.key;
}
function buildDiscordCommandOptions(params) {
const { command, cfg, authorizeChoiceContext, resolveChoiceContext } = params;
const commandLabel = resolveDiscordCommandLogLabel(command);
const args = command.args;
if (!args || args.length === 0) return;
return args.map((arg) => {
const required = arg.required ?? false;
if (arg.type === "number") return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`
}),
type: ApplicationCommandOptionType.Number,
required
};
if (arg.type === "boolean") return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`
}),
type: ApplicationCommandOptionType.Boolean,
required
};
const resolvedChoices = resolveCommandArgChoices({
command,
arg,
cfg
});
const autocomplete = arg.preferAutocomplete === true || resolvedChoices.length > 0 && (typeof arg.choices === "function" || resolvedChoices.length > 25) ? async (interaction) => {
if (typeof arg.choices === "function" && resolveChoiceContext && authorizeChoiceContext && !await authorizeChoiceContext(interaction)) {
await interaction.respond([]);
return;
}
const focusValue = normalizeLowercaseStringOrEmpty(interaction.options.getFocused()?.value);
const context = typeof arg.choices === "function" && resolveChoiceContext ? await resolveChoiceContext(interaction) : null;
const choices = resolveCommandArgChoices({
command,
arg,
cfg,
provider: context?.provider,
model: context?.model
});
const filtered = focusValue ? choices.filter((choice) => normalizeLowercaseStringOrEmpty(choice.label).includes(focusValue)) : choices;
await interaction.respond(filtered.slice(0, 25).map((choice) => ({
name: choice.label,
value: choice.value
})));
} : void 0;
const choices = resolvedChoices.length > 0 && !autocomplete ? resolvedChoices.slice(0, 25).map((choice) => ({
name: choice.label,
value: choice.value
})) : void 0;
return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`
}),
type: ApplicationCommandOptionType.String,
required,
choices,
autocomplete
};
});
}
//#endregion
//#region extensions/discord/src/monitor/native-command.ts
const log = createSubsystemLogger("discord/native-command");
function createDiscordNativeCommand(params) {
const { command, cfg, discordConfig, accountId, sessionPrefix, ephemeralDefault, threadBindings } = params;
const fallbackCommandDefinition = createNativeCommandDefinition(command);
const pluginCommandMatch = nativeCommandRuntime.matchPluginCommand(`/${command.name}`);
const commandDefinition = pluginCommandMatch !== null ? fallbackCommandDefinition : findCommandByNativeName(command.name, "discord", { includeBundledChannelFallback: false }) ?? fallbackCommandDefinition;
const argDefinitions = commandDefinition.args ?? command.args;
const commandOptions = buildDiscordCommandOptions({
command: commandDefinition,
cfg,
authorizeChoiceContext: async (interaction) => await resolveDiscordNativeAutocompleteAuthorized({
interaction,
cfg,
discordConfig,
accountId,
skipCommandOwnerAllowFrom: pluginCommandMatch !== null
}),
resolveChoiceContext: async (interaction) => resolveDiscordNativeChoiceContext({
interaction,
cfg,
accountId,
threadBindings
})
});
const options = commandOptions ? commandOptions : command.acceptsArgs ? [{
name: "input",
description: "Command input",
type: ApplicationCommandOptionType.String,
required: false
}] : void 0;
return new class extends Command {
constructor(..._args) {
super(..._args);
this.name = command.name;
this.description = truncateDiscordCommandDescription({
value: command.description,
label: `command:${command.name}`
});
this.descriptionLocalizations = truncateDiscordCommandDescriptionLocalizations({
value: command.descriptionLocalizations,
label: `command:${command.name}`
});
this.defer = false;
this.ephemeral = ephemeralDefault;
this.options = options;
}
async run(interaction) {
if (await safeDiscordInteractionCall("interaction defer", () => interaction.defer({ ephemeral: this.ephemeral })) === null) return;
const commandArgs = argDefinitions?.length ? readDiscordCommandArgs(interaction, argDefinitions) : command.acceptsArgs ? parseCommandArgs(commandDefinition, interaction.options.getString("input") ?? "") : void 0;
const commandArgsWithRaw = commandArgs ? {
...commandArgs,
raw: serializeCommandArgs(commandDefinition, commandArgs) ?? commandArgs.raw
} : void 0;
await dispatchDiscordCommandInteraction({
interaction,
prompt: buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw),
command: commandDefinition,
commandArgs: commandArgsWithRaw,
cfg,
discordConfig,
accountId,
sessionPrefix,
preferFollowUp: true,
threadBindings,
responseEphemeral: ephemeralDefault
});
}
}();
}
async function dispatchDiscordCommandInteraction(params) {
const { interaction, prompt, command, commandArgs, cfg, discordConfig, accountId, sessionPrefix, preferFollowUp, threadBindings, responseEphemeral, suppressReplies } = params;
const commandName = command.nativeName ?? command.key;
const respond = async (content, options) => {
const ephemeral = options?.ephemeral ?? responseEphemeral;
const payload = {
content,
...ephemeral !== void 0 ? { ephemeral } : {}
};
await safeDiscordInteractionCall("interaction reply", async () => {
if (preferFollowUp) {
await interaction.followUp(payload);
return;
}
await interaction.reply(payload);
});
};
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const user = interaction.user;
if (!user) return { accepted: false };
const sender = resolveDiscordSenderIdentity({
author: user,
pluralkitInfo: null
});
const channel = interaction.channel;
const { isDirectMessage, isGroupDm, isThreadChannel, channelName, channelSlug, rawChannelId, threadParentId, threadParentName, threadParentSlug } = await resolveDiscordNativeInteractionChannelContext({
channel,
client: interaction.client,
hasGuild: Boolean(interaction.guild),
channelIdFallback: ""
});
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
const allowNameMatching = isDangerousNameMatchingEnabled(discordConfig);
const configuredDmAllowFrom = resolveDiscordAccountAllowFrom({
cfg,
accountId
}) ?? [];
const commandOwnerAllowFrom = resolveDiscordCommandOwnerAllowFrom(cfg);
const { ownerAllowList: discordOwnerAllowList, ownerAllowed: discordOwnerOk } = resolveDiscordOwnerAccess({
allowFrom: configuredDmAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching
});
const { ownerAllowed: commandOwnerOk } = resolveDiscordOwnerAccess({
allowFrom: commandOwnerAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching
});
const commandOwnerAllowAll = commandOwnerAllowFrom?.includes("*") === true;
const senderIsCommandOwner = commandOwnerOk || commandOwnerAllowAll;
const ownerAllowListConfigured = discordOwnerAllowList != null;
const ownerOk = discordOwnerOk;
const { commandsAllowFromAccess, guildInfo, channelConfig } = resolveDiscordNativeCommandChannelAccessContext({
cfg,
discordConfig,
accountId,
sender,
isDirectMessage,
isThreadChannel,
guild: interaction.guild ?? null,
rawChannelId,
channelName,
channelSlug,
threadParentId,
threadParentName,
threadParentSlug
});
let nativeRouteStatePromise;
const getNativeRouteState = () => nativeRouteStatePromise ??= nativeCommandRuntime.resolveDiscordNativeInteractionRouteState({
cfg,
accountId,
guildId: interaction.guild?.id ?? void 0,
memberRoleIds,
isDirectMessage,
isGroupDm,
directUserId: user.id,
conversationId: rawChannelId || "unknown",
parentConversationId: threadParentId,
threadBinding: isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : void 0,
enforceConfiguredBindingReadiness: !shouldBypassConfiguredAcpEnsure(commandName)
});
const canBypassConfiguredAcpGuildGuards = async () => {
if (!interaction.guild || !shouldBypassConfiguredAcpGuildGuards(commandName)) return false;
const routeState = await getNativeRouteState();
return routeState.effectiveRoute.matchedBy === "binding.channel" || routeState.boundSessionKey != null || routeState.configuredBinding != null || routeState.configuredRoute != null;
};
if (channelConfig?.enabled === false && !await canBypassConfiguredAcpGuildGuards()) {
await respond("This channel is disabled.");
return { accepted: false };
}
if (interaction.guild && channelConfig?.allowed === false && !await canBypassConfiguredAcpGuildGuards()) {
await respond("This channel is not allowed.");
return { accepted: false };
}
if (useAccessGroups && interaction.guild) {
const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== void 0,
groupPolicy: discordConfig?.groupPolicy,
defaultGroupPolicy: cfg.channels?.defaults?.groupPolicy
});
if (!resolveDiscordChannelPolicyCommandAuthorizer({
groupPolicy,
guildInfo,
channelConfig
}).allowed && !await canBypassConfiguredAcpGuildGuards()) {
await respond("This channel is not allowed.");
return { accepted: false };
}
}
const dmEnabled = discordConfig?.dm?.enabled ?? true;
const dmPolicy = resolveDiscordAccountDmPolicy({
cfg,
accountId
}) ?? "pairing";
let commandAuthorized = true;
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") {
await respond("Discord DMs are disabled.");
return { accepted: false };
}
const dmAccess = await resolveDiscordDmCommandAccess({
accountId,
dmPolicy,
configuredAllowFrom: configuredDmAllowFrom,
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
},
allowNameMatching,
cfg,
rest: interaction.client.rest
});
commandAuthorized = dmAccess.senderAccess.allowed ? dmAccess.commandAccess.authorized : false;
if (dmAccess.senderAccess.decision !== "allow") {
await handleDiscordDmCommandDecision({
senderAccess: dmAccess.senderAccess,
accountId,
sender: {
id: user.id,
tag: sender.tag,
name: sender.name
},
onPairingCreated: async (code) => {
await respond(buildPairingReply({
channel: "discord",
idLine: `Your Discord user id: ${user.id}`,
code
}), { ephemeral: true });
},
onUnauthorized: async () => {
await respond("You are not authorized to use this command.", { ephemeral: true });
}
});
return { accepted: false };
}
}
const groupDmAccess = resolveDiscordNativeGroupDmAccess({
isGroupDm,
groupEnabled: discordConfig?.dm?.groupEnabled,
groupChannels: discordConfig?.dm?.groupChannels,
channelId: rawChannelId,
channelName,
channelSlug
});
if (!groupDmAccess.allowed) {
await respond(groupDmAccess.reason === "disabled" ? "Discord group DMs are disabled." : "This group DM is not allowed.");
return { accepted: false };
}
if (!isDirectMessage) {
commandAuthorized = await resolveDiscordGuildNativeCommandAuthorized({
cfg,
accountId,
discordConfig,
useAccessGroups,
commandsAllowFromAccess,
guildInfo,
channelConfig,
memberRoleIds,
sender,
allowNameMatching,
ownerAllowListConfigured,
ownerAllowed: ownerOk
});
if (!commandAuthorized && !await canBypassConfiguredAcpGuildGuards()) {
await respond("You are not authorized to use this command.", { ephemeral: true });
return { accepted: false };
}
}
const pluginMatch = nativeCommandRuntime.matchPluginCommand(prompt);
if (commandOwnerAllowFrom && !senderIsCommandOwner && !commandsAllowFromAccess.allowed && commandName !== "status" && !pluginMatch) {
await respond("You are not authorized to use this command.", { ephemeral: true });
return { accepted: false };
}
const isGuild = Boolean(interaction.guild);
const channelId = rawChannelId || "unknown";
const menuModelContext = !(commandArgs?.raw && !commandArgs.values) && command.args?.some((arg) => typeof arg.choices === "function" && commandArgs?.values?.[arg.name] == null) ? await resolveDiscordNativeChoiceContext({
interaction,
cfg,
accountId,
threadBindings
}) : null;
const menu = resolveCommandArgMenu({
command,
args: commandArgs,
cfg,
provider: menuModelContext?.provider,
model: menuModelContext?.model
});
if (menu) {
const menuPayload = buildDiscordCommandArgMenu({
command,
menu,
interaction,
ctx: {
cfg,
discordConfig,
accountId,
sessionPrefix,
threadBindings
},
safeInteractionCall: safeDiscordInteractionCall,
dispatchCommandInteraction: dispatchDiscordCommandInteraction
});
if (preferFollowUp) {
await safeDiscordInteractionCall("interaction follow-up", () => interaction.followUp({
content: menuPayload.content,
components: menuPayload.components,
ephemeral: true
}));
return { accepted: true };
}
await safeDiscordInteractionCall("interaction reply", () => interaction.reply({
content: menuPayload.content,
components: menuPayload.components,
ephemeral: true
}));
return { accepted: true };
}
if (pluginMatch && commandName !== "status") {
if (suppressReplies) return { accepted: true };
const messageThreadId = !isDirectMessage && isThreadChannel ? channelId : void 0;
const pluginThreadParentId = !isDirectMessage && isThreadChannel ? threadParentId : void 0;
const routeState = await getNativeRouteState();
const { effectiveRoute } = routeState;
const pluginCommandAgentId = (isThreadChannel ? threadBindings.getByThreadId(rawChannelId)?.agentId : void 0) || routeState.configuredBinding?.statefulTarget.agentId || effectiveRoute.agentId;
const targetSessionEntry = nativeCommandRuntime.getSessionEntry({
agentId: pluginCommandAgentId,
sessionKey: effectiveRoute.sessionKey
});
const pluginReply = await nativeCommandRuntime.executePluginCommand({
command: pluginMatch.command,
args: pluginMatch.args,
senderId: sender.id,
channel: "discord",
channelId,
isAuthorizedSender: commandAuthorized,
senderIsOwner: senderIsCommandOwner,
agentId: pluginCommandAgentId,
sessionKey: effectiveRoute.sessionKey,
authProfileId: targetSessionEntry?.authProfileOverride,
commandBody: prompt,
config: cfg,
from: isDirectMessage ? `discord:${user.id}` : isGroupDm ? `discord:group:${channelId}` : `discord:channel:${channelId}`,
to: `slash:${user.id}`,
accountId,
messageThreadId,
threadParentId: pluginThreadParentId
});
if (!hasRenderableReplyPayload(pluginReply)) {
await respond(DISCORD_EMPTY_VISIBLE_REPLY_WARNING);
return {
accepted: true,
effectiveRoute
};
}
await deliverDiscordInteractionReply({
interaction,
payload: pluginReply,
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, { fallbackLimit: 2e3 }),
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
cfg,
discordConfig,
accountId
}),
preferFollowUp,
responseEphemeral,
chunkMode: resolveChunkMode(cfg, "discord", accountId)
});
return {
accepted: true,
effectiveRoute
};
}
const pickerCommandContext = shouldOpenDiscordModelPickerFromCommand({
command,
commandArgs
});
if (pickerCommandContext) {
await replyWithDiscordModelPickerProviders({
interaction,
cfg,
command: pickerCommandContext,
userId: user.id,
accountId,
threadBindings,
preferFollowUp,
safeInteractionCall: safeDiscordInteractionCall
});
return { accepted: true };
}
const interactionId = interaction.rawData.id;
const routeState = await getNativeRouteState();
if (routeState.bindingReadiness && !routeState.bindingReadiness.ok) {
const configuredBinding = routeState.configuredBinding;
if (configuredBinding) {
logVerbose(`discord native command: configured ACP binding unavailable for channel ${configuredBinding.record.conversation.conversationId}: ${routeState.bindingReadiness.error}`);
await respond("Configured ACP binding is unavailable right now. Please try again.");
return { accepted: false };
}
}
const boundSessionKey = routeState.boundSessionKey;
const effectiveRoute = routeState.effectiveRoute;
const { sessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({
agentId: effectiveRoute.agentId,
sessionPrefix,
userId: user.id,
targetSessionKey: effectiveRoute.sessionKey,
boundSessionKey
});
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId);
const ctxPayload = buildDiscordNativeCommandContext({
prompt,
commandArgs: commandArgs ?? {},
sessionKey,
commandTargetSessionKey,
accountId: effectiveRoute.accountId,
interactionId,
channelId,
threadParentId,
memberRoleIds,
guildId: interaction.guild?.id,
guildName: interaction.guild?.name,
channelTopic: resolveDiscordChannelTopicSafe(channel),
channelConfig,
guildInfo,
allowNameMatching,
commandAuthorized,
isDirectMessage,
isGroupDm,
isGuild,
isThreadChannel,
user: {
id: user.id,
username: user.username,
globalName: user.globalName
},
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
}
});
const directStatusResult = await maybeDeliverDiscordDirectStatus({
commandName,
suppressReplies,
resolveDirectStatusReplyForSession: nativeCommandRuntime.resolveDirectStatusReplyForSession,
cfg,
discordConfig,
accountId,
sessionKey,
commandTargetSessionKey,
channel: "discord",
senderId: sender.id,
senderIsOwner: senderIsCommandOwner,
isAuthorizedSender: commandAuthorized,
isGroup: isGuild || isGroupDm,
defaultGroupActivation: () => !isGuild ? "always" : channelConfig?.requireMention === false ? "always" : "mention",
interaction,
mediaLocalRoots,
preferFollowUp,
responseEphemeral,
effectiveRoute,
respond
});
if (directStatusResult) return directStatusResult;
await dispatchDiscordNativeAgentReply({
cfg,
discordConfig,
accountId,
interaction,
ctxPayload,
effectiveRoute,
channelConfig,
mediaLocalRoots,
preferFollowUp,
responseEphemeral,
suppressReplies,
log
});
return {
accepted: true,
effectiveRoute
};
}
function createDiscordCommandArgFallbackButton(params) {
return createDiscordCommandArgFallbackButton$1({
ctx: params,
safeInteractionCall: safeDiscordInteractionCall,
dispatchCommandInteraction: dispatchDiscordCommandInteraction
});
}
function createDiscordModelPickerFallbackButton(params) {
return createDiscordModelPickerFallbackButton$1({
ctx: params,
safeInteractionCall: safeDiscordInteractionCall,
dispatchCommandInteraction: dispatchDiscordCommandInteraction
});
}
function createDiscordModelPickerFallbackSelect(params) {
return createDiscordModelPickerFallbackSelect$1({
ctx: params,
safeInteractionCall: safeDiscordInteractionCall,
dispatchCommandInteraction: dispatchDiscordCommandInteraction
});
}
//#endregion
//#region extensions/discord/src/internal/gateway-close-codes.ts
const fatalGatewayCloseCodes = new Set([
GatewayCloseCodes.AuthenticationFailed,
GatewayCloseCodes.InvalidShard,
GatewayCloseCodes.ShardingRequired,
GatewayCloseCodes.InvalidAPIVersion,
GatewayCloseCodes.InvalidIntents,
GatewayCloseCodes.DisallowedIntents
]);
const nonResumableGatewayCloseCodes = new Set([
GatewayCloseCodes.NotAuthenticated,
GatewayCloseCodes.InvalidSeq,
GatewayCloseCodes.SessionTimedOut,
GatewayCloseCodes.AlreadyAuthenticated
]);
function isFatalGatewayCloseCode(code) {
return fatalGatewayCloseCodes.has(code);
}
function canResumeAfterGatewayClose(code) {
return !nonResumableGatewayCloseCodes.has(code);
}
//#endregion
//#region extensions/discord/src/internal/gateway-dispatch.ts
function dispatchVoiceGatewayEvent(client, type, data) {
const guildId = readGuildId(data);
if (!guildId) return;
const adapter = (client.getPlugin("voice")?.adapters)?.get(guildId);
const voiceServerUpdate = GatewayDispatchEvents.VoiceServerUpdate;
const voiceStateUpdate = GatewayDispatchEvents.VoiceStateUpdate;
if (type === voiceServerUpdate) adapter?.onVoiceServerUpdate?.(data);
if (type === voiceStateUpdate) adapter?.onVoiceStateUpdate?.(data);
}
function mapGatewayDispatchData(client, type, data) {
const messageCreate = GatewayDispatchEvents.MessageCreate;
const reactionAdd = GatewayDispatchEvents.MessageReactionAdd;
const reactionRemove = GatewayDispatchEvents.MessageReactionRemove;
if (type === messageCreate) return createMessageDispatchData(client, data);
if (type === reactionAdd || type === reactionRemove) return createReactionDispatchData(client, data);
return data;
}
function createMessageDispatchData(client, data) {
const message = new Message(client, data);
return {
...data,
id: data.id,
channel_id: data.channel_id,
channelId: data.channel_id,
message,
author: message.author ?? (data.author ? new User(client, data.author) : null),
member: data.member,
rawMember: data.member,
guild: data.guild_id ? new Guild(client, data.guild_id) : null
};
}
function createReactionDispatchData(client, data) {
const userRaw = data.member?.user && typeof data.member.user === "object" ? {
id: data.user_id,
username: "",
...data.member.user
} : {
id: data.user_id,
username: ""
};
return {
...data,
user: new User(client, userRaw),
rawMember: data.member,
guild: data.guild_id ? new Guild(client, data.guild_id) : null,
message: new Message(client, {
id: data.message_id,
channelId: data.channel_id
})
};
}
function readGuildId(data) {
return data && typeof data === "object" && typeof data.guild_id === "string" ? data.guild_id : void 0;
}
//#endregion
//#region extensions/discord/src/internal/gateway-identify-limiter.ts
const IDENTIFY_WINDOW_MS = 5e3;
function normalizeMaxConcurrency(value) {
const parsed = parseFiniteNumber(value);
return parsed === void 0 ? 1 : Math.max(1, Math.floor(parsed));
}
var GatewayIdentifyLimiter = class {
constructor() {
this.stateByKey = /* @__PURE__ */ new Map();
}
async wait(params) {
const maxConcurrency = normalizeMaxConcurrency(params.maxConcurrency);
const rateKey = (params.shardId ?? 0) % maxConcurrency;
const now = Date.now();
const state = this.stateByKey.get(rateKey);
const clockMovedBackward = state !== void 0 && now < state.lastObservedAt;
const nextAllowedAt = state === void 0 ? now : clockMovedBackward ? now + IDENTIFY_WINDOW_MS : state.nextAllowedAt;
const waitMs = Math.max(0, nextAllowedAt - now);
this.stateByKey.set(rateKey, {
lastObservedAt: now,
nextAllowedAt: Math.max(now, nextAllowedAt) + IDENTIFY_WINDOW_MS
});
if (waitMs > 0) await new Promise((resolve) => {
setTimeout(resolve, waitMs).unref?.();
});
}
reset() {
this.stateByKey.clear();
}
};
const sharedGatewayIdentifyLimiter = new GatewayIdentifyLimiter();
//#endregion
//#region extensions/discord/src/internal/gateway-lifecycle.ts
var GatewayHeartbeatTimers = class {
scheduleHeartbeatCycle(params) {
this.heartbeatInterval = setTimeout(() => {
this.heartbeatInterval = void 0;
if (!params.isAcked()) {
params.onAckTimeout();
return;
}
params.onHeartbeat();
this.scheduleHeartbeatCycle(params);
}, params.intervalMs);
this.heartbeatInterval.unref?.();
}
start(params) {
this.stop();
const random = params.random ?? Math.random;
this.firstHeartbeatTimeout = setTimeout(() => {
this.firstHeartbeatTimeout = void 0;
params.onHeartbeat();
this.scheduleHeartbeatCycle(params);
}, Math.max(0, params.intervalMs * random()));
this.firstHeartbeatTimeout.unref?.();
}
stop() {
if (this.heartbeatInterval) {
clearTimeout(this.heartbeatInterval);
this.heartbeatInterval = void 0;
}
if (this.firstHeartbeatTimeout) {
clearTimeout(this.firstHeartbeatTimeout);
this.firstHeartbeatTimeout = void 0;
}
}
};
var GatewayReconnectTimer = class {
stop() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = void 0;
}
}
schedule(delayMs, callback) {
this.stop();
this.timeout = setTimeout(() => {
this.timeout = void 0;
callback();
}, delayMs);
this.timeout.unref?.();
}
};
//#endregion
//#region extensions/discord/src/internal/gateway-rate-limit.ts
const GATEWAY_SEND_LIMIT = 120;
const GATEWAY_SEND_WINDOW_MS = 6e4;
var GatewaySendLimiter = class {
constructor(sendNow, emitError) {
this.sendNow = sendNow;
this.emitError = emitError;
this.outboundSendTimestamps = [];
this.outboundQueue = [];
}
send(serialized, options) {
if (options?.critical || this.canSend(Date.now())) {
this.sendSerialized(serialized);
return;
}
this.outboundQueue.push({ payload: serialized });
this.scheduleFlush();
}
clear() {
if (this.outboundFlushTimer) {
clearTimeout(this.outboundFlushTimer);
this.outboundFlushTimer = void 0;
}
this.outboundQueue = [];
}
getStatus() {
const now = Date.now();
this.pruneWindow(now);
const oldest = this.outboundSendTimestamps[0] ?? now;
return {
remainingEvents: Math.max(0, GATEWAY_SEND_LIMIT - this.outboundSendTimestamps.length),
resetTime: this.outboundSendTimestamps.length > 0 ? oldest + GATEWAY_SEND_WINDOW_MS : now + GATEWAY_SEND_WINDOW_MS,
currentEventCount: this.outboundSendTimestamps.length,
queuedEvents: this.outboundQueue.length
};
}
pruneWindow(now) {
const windowStart = now - GATEWAY_SEND_WINDOW_MS;
while (this.outboundSendTimestamps.length > 0 && (this.outboundSendTimestamps[0] ?? 0) <= windowStart) this.outboundSendTimestamps.shift();
}
canSend(now) {
this.pruneWindow(now);
return this.outboundSendTimestamps.length < GATEWAY_SEND_LIMIT;
}
sendSerialized(serialized) {
this.outboundSendTimestamps.push(Date.now());
this.sendNow(serialized);
}
scheduleFlush() {
if (this.outboundFlushTimer || this.outboundQueue.length === 0) return;
const now = Date.now();
this.pruneWindow(now);
const oldest = this.outboundSendTimestamps[0] ?? now;
const delayMs = this.outboundSendTimestamps.length >= GATEWAY_SEND_LIMIT ? Math.max(0, oldest + GATEWAY_SEND_WINDOW_MS - now) : 0;
this.outboundFlushTimer = setTimeout(() => {
this.outboundFlushTimer = void 0;
this.flush();
}, delayMs);
this.outboundFlushTimer.unref?.();
}
flush() {
while (this.outboundQueue.length > 0 && this.canSend(Date.now())) {
const queued = this.outboundQueue.shift();
if (!queued) continue;
try {
this.sendSerialized(queued.payload);
} catch (error) {
this.emitError(error instanceof Error ? error : new Error(String(error), { cause: error }));
this.clear();
return;
}
}
this.scheduleFlush();
}
};
//#endregion
//#region extensions/discord/src/internal/gateway.ts
const GatewayIntents = GatewayIntentBits;
const READY_STATE_OPEN = 1;
const DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/";
const DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES = 4096;
const INVALID_SESSION_MIN_DELAY_MS = 1e3;
const INVALID_SESSION_JITTER_MS = 4e3;
function ensureGatewayParams(url) {
const parsed = new URL(url);
parsed.searchParams.set("v", parsed.searchParams.get("v") ?? "10");
parsed.searchParams.set("encoding", parsed.searchParams.get("encoding") ?? "json");
return parsed.toString();
}
function decodeGatewayMessage(incoming) {
const text = Buffer.isBuffer(incoming) ? incoming.toString("utf8") : incoming instanceof ArrayBuffer ? Buffer.from(incoming).toString("utf8") : Array.isArray(incoming) ? Buffer.concat(incoming.map((entry) => Buffer.from(entry))).toString("utf8") : String(incoming);
try {
return JSON.parse(text);
} catch {
return null;
}
}
var GatewayPlugin = class extends Plugin {
constructor(options, gatewayInfo) {
super();
this.id = "gateway";
this.ws = null;
this.sequence = null;
this.lastHeartbeatAck = true;
this.emitter = new EventEmitter();
this.isConnected = false;
this.sessionId = null;
this.resumeGatewayUrl = null;
this.reconnectAttempts = 0;
this.shouldReconnect = false;
this.isConnecting = false;
this.heartbeatTimers = new GatewayHeartbeatTimers();
this.reconnectTimer = new GatewayReconnectTimer();
this.outboundLimiter = new GatewaySendLimiter((payload) => this.sendSerializedGatewayEvent(payload), (error) => this.emitter.emit("error", error));
this.options = {
...options,
reconnect: {
maxAttempts: 50,
...options.reconnect
},
autoInteractions: options.autoInteractions ?? true,
intents: options.intents ?? 0
};
this.gatewayInfo = gatewayInfo;
}
get ping() {
return null;
}
get heartbeatInterval() {
return this.heartbeatTimers.heartbeatInterval;
}
set heartbeatInterval(timer) {
this.heartbeatTimers.heartbeatInterval = timer;
}
get firstHeartbeatTimeout() {
return this.heartbeatTimers.firstHeartbeatTimeout;
}
set firstHeartbeatTimeout(timer) {
this.heartbeatTimers.firstHeartbeatTimeout = timer;
}
async registerClient(client) {
this.client = client;
if (this.options.shard) {
client.shardId = this.options.shard[0];
client.totalShards = this.options.shard[1];
this.shardId = this.options.shard[0];
this.totalShards = this.options.shard[1];
}
this.shouldReconnect = true;
this.connect(false);
}
connect(resume = false) {
this.stopReconnectTimer();
this.stopHeartbeat();
if (this.isConnecting) return;
this.shouldReconnect = true;
this.lastHeartbeatAck = true;
this.ws?.close(1e3, "Reconnecting");
const baseUrl = resume && this.resumeGatewayUrl ? this.resumeGatewayUrl : this.gatewayInfo?.url ?? this.options.url ?? DEFAULT_GATEWAY_URL;
this.ws = this.createWebSocket(ensureGatewayParams(baseUrl));
this.isConnecting = true;
this.isConnected = false;
this.setupWebSocket(resume);
}
disconnect() {
this.shouldReconnect = false;
this.stopReconnectTimer();
this.stopHeartbeat();
this.outboundLimiter.clear();
this.ws?.close(1e3, "Client disconnect");
this.ws = null;
this.isConnecting = false;
this.isConnected = false;
this.reconnectAttempts = 0;
}
createWebSocket(url) {
return new ws.WebSocket(url);
}
setupWebSocket(resume) {
const socket = this.ws;
if (!socket) return;
socket.on("open", () => {
if (socket !== this.ws) return;
this.isConnecting = false;
this.emitter.emit("debug", "Gateway websocket opened");
});
socket.on("message", (incoming) => {
if (socket !== this.ws) return;
const payload = decodeGatewayMessage(incoming);
if (!payload) {
this.emitter.emit("error", /* @__PURE__ */ new Error("Invalid gateway payload"));
return;
}
this.handlePayload(payload, resume, socket);
});
socket.on("close", (code) => {
if (socket !== this.ws) return;
const closeCode = code;
this.stopHeartbeat();
this.outboundLimiter.clear();
this.isConnecting = false;
this.isConnected = false;
this.emitter.emit("debug", `Gateway websocket closed: ${code}`);
if (!this.shouldReconnect) return;
if (isFatalGatewayCloseCode(closeCode)) {
this.shouldReconnect = false;
this.emitter.emit("error", /* @__PURE__ */ new Error(`Fatal gateway close code: ${code}`));
return;
}
const canResume = canResumeAfterGatewayClose(closeCode);
if (!canResume) this.resetSessionState();
this.scheduleReconnect(canResume, closeCode);
});
socket.on("error", (error) => {
if (socket !== this.ws) return;
this.emitter.emit("error", error);
});
}
handlePayload(payload, resume, sourceSocket) {
if (payload.s !== null && payload.s !== void 0) this.sequence = payload.s;
switch (payload.op) {
case GatewayOpcodes.Hello:
this.startHeartbeat(payload.d.heartbeat_interval ?? 45e3);
if (resume && this.sessionId) this.send({
op: GatewayOpcodes.Resume,
d: {
token: this.client?.options.token ?? "",
session_id: this.sessionId,
seq: this.sequence ?? 0
}
}, true);
else this.identifyWithConcurrency(sourceSocket).catch((error) => {
this.emitter.emit("error", error instanceof Error ? error : new Error(String(error), { cause: error }));
});
break;
case GatewayOpcodes.HeartbeatAck:
this.lastHeartbeatAck = true;
break;
case GatewayOpcodes.Heartbeat:
this.sendHeartbeat();
break;
case GatewayOpcodes.Dispatch:
this.handleDispatch(payload).catch((error) => {
this.emitter.emit("error", error instanceof Error ? error : new Error(String(error), { cause: error }));
});
break;
case GatewayOpcodes.InvalidSession:
if (!payload.d) this.resetSessionState();
this.scheduleReconnect(payload.d, void 0, INVALID_SESSION_MIN_DELAY_MS + Math.floor(Math.random() * INVALID_SESSION_JITTER_MS));
break;
case GatewayOpcodes.Reconnect:
this.scheduleReconnect(true);
break;
}
}
startHeartbeat(intervalMs) {
this.heartbeatTimers.start({
intervalMs,
isAcked: () => this.lastHeartbeatAck,
onHeartbeat: () => this.sendHeartbeat(),
onAckTimeout: () => {
this.emitter.emit("error", /* @__PURE__ */ new Error("Gateway heartbeat ACK timeout"));
this.scheduleReconnect(true);
}
});
}
stopHeartbeat() {
this.heartbeatTimers.stop();
}
stopReconnectTimer() {
this.reconnectTimer.stop();
}
sendHeartbeat() {
if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) return;
this.lastHeartbeatAck = false;
this.send({
op: GatewayOpcodes.Heartbeat,
d: this.sequence
}, true);
}
identify() {
this.send({
op: GatewayOpcodes.Identify,
d: {
token: this.client?.options.token ?? "",
intents: this.options.intents ?? 0,
properties: {
os: process.platform,
browser: "openclaw",
device: "openclaw"
},
shard: this.options.shard
}
}, true);
}
async identifyWithConcurrency(sourceSocket) {
await sharedGatewayIdentifyLimiter.wait({
shardId: this.shardId,
maxConcurrency: this.gatewayInfo?.session_start_limit.max_concurrency
});
const socket = sourceSocket ?? this.ws;
if (!socket || socket !== this.ws) return;
if (socket.readyState !== READY_STATE_OPEN) {
this.scheduleReconnect(false);
return;
}
this.identify();
}
send(payload, skipRateLimit = false) {
if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) throw new Error("Discord gateway socket is not open");
const serialized = JSON.stringify(payload);
if ((typeof Buffer !== "undefined" ? Buffer.byteLength(serialized, "utf8") : new TextEncoder().encode(serialized).byteLength) > DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES) throw new Error(`Discord gateway payload exceeds ${DISCORD_GATEWAY_PAYLOAD_LIMIT_BYTES}-byte limit`);
this.outboundLimiter.send(serialized, { critical: skipRateLimit });
}
sendSerializedGatewayEvent(serialized) {
if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) throw new Error("Discord gateway socket is not open");
this.ws.send(serialized);
}
async handleDispatch(payload) {
if (!this.client || !payload.t) return;
if (payload.t === GatewayDispatchEvents.Ready) {
const ready = payload.d;
this.sessionId = ready.session_id ?? null;
this.resumeGatewayUrl = ready.resume_gateway_url ?? null;
this.reconnectAttempts = 0;
this.isConnected = true;
}
if (payload.t === GatewayDispatchEvents.Resumed) {
this.reconnectAttempts = 0;
this.isConnected = true;
}
dispatchVoiceGatewayEvent(this.client, payload.t, payload.d);
const data = mapGatewayDispatchData(this.client, payload.t, payload.d);
await this.client.dispatchGatewayEvent(payload.t, data);
if (payload.t === GatewayDispatchEvents.InteractionCreate && this.options.autoInteractions) await this.client.handleInteraction(payload.d);
}
resetSessionState() {
this.sessionId = null;
this.resumeGatewayUrl = null;
this.sequence = null;
}
scheduleReconnect(resume, closeCode, minDelayMs = 0) {
if (!this.shouldReconnect) return;
this.stopHeartbeat();
this.stopReconnectTimer();
this.ws?.close();
this.ws = null;
this.isConnecting = false;
this.isConnected = false;
this.outboundLimiter.clear();
this.reconnectAttempts += 1;
if (this.reconnectAttempts > (this.options.reconnect?.maxAttempts ?? 50)) {
const maxAttempts = this.options.reconnect?.maxAttempts ?? 50;
this.emitter.emit("error", /* @__PURE__ */ new Error(`Max reconnect attempts (${maxAttempts}) reached${closeCode !== void 0 ? ` after close code ${closeCode}` : ""}`));
return;
}
const delay = Math.max(minDelayMs, Math.min(3e4, 1e3 * 2 ** Math.min(this.reconnectAttempts, 5)));
this.reconnectTimer.schedule(delay, () => {
this.connect(resume);
});
}
updatePresence(data) {
this.send({
op: GatewayOpcodes.PresenceUpdate,
d: data
});
}
updateVoiceState(data) {
this.send({
op: GatewayOpcodes.VoiceStateUpdate,
d: data
}, true);
}
requestGuildMembers(data) {
if (!this.hasIntent(GatewayIntentBits.GuildMembers)) throw new Error("GUILD_MEMBERS intent is required for requestGuildMembers");
if (data.presences && !this.hasIntent(GatewayIntentBits.GuildPresences)) throw new Error("GUILD_PRESENCES intent is required when requesting presences");
if (!data.query && data.query !== "" && !data.user_ids) throw new Error("Either query or user_ids is required for requestGuildMembers");
this.send({
op: GatewayOpcodes.RequestGuildMembers,
d: data
});
}
getRateLimitStatus() {
return this.outboundLimiter.getStatus();
}
getIntentsInfo() {
return {
intents: this.options.intents ?? 0,
hasGuilds: this.hasIntent(GatewayIntentBits.Guilds),
hasGuildMembers: this.hasIntent(GatewayIntentBits.GuildMembers),
hasGuildPresences: this.hasIntent(GatewayIntentBits.GuildPresences),
hasGuildMessages: this.hasIntent(GatewayIntentBits.GuildMessages),
hasMessageContent: this.hasIntent(GatewayIntentBits.MessageContent)
};
}
hasIntent(intent) {
return Boolean((this.options.intents ?? 0) & intent);
}
};
//#endregion
//#region extensions/discord/src/monitor/presence.ts
const DEFAULT_CUSTOM_ACTIVITY_TYPE$1 = 4;
const CUSTOM_STATUS_NAME$1 = "Custom Status";
function resolveDiscordPresenceUpdate(config) {
const activityText = normalizeOptionalString(config.activity) ?? "";
const status = normalizeOptionalString(config.status) ?? "";
const activityType = config.activityType;
const activityUrl = normalizeOptionalString(config.activityUrl) ?? "";
const hasActivity = Boolean(activityText);
if (!hasActivity && !Boolean(status)) return {
since: null,
activities: [],
status: "online",
afk: false
};
const activities = [];
if (hasActivity) {
const resolvedType = activityType ?? DEFAULT_CUSTOM_ACTIVITY_TYPE$1;
const activity = resolvedType === DEFAULT_CUSTOM_ACTIVITY_TYPE$1 ? {
name: CUSTOM_STATUS_NAME$1,
type: resolvedType,
state: activityText
} : {
name: activityText,
type: resolvedType
};
if (resolvedType === 1 && activityUrl) activity.url = activityUrl;
activities.push(activity);
}
return {
since: null,
activities,
status: status || "online",
afk: false
};
}
//#endregion
//#region extensions/discord/src/monitor/auto-presence.ts
const DEFAULT_CUSTOM_ACTIVITY_TYPE = 4;
const CUSTOM_STATUS_NAME = "Custom Status";
const DEFAULT_INTERVAL_MS = 3e4;
const DEFAULT_MIN_UPDATE_INTERVAL_MS = 15e3;
const MIN_INTERVAL_MS = 5e3;
const MIN_UPDATE_INTERVAL_MS = 1e3;
function normalizeOptionalText(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function clampPositiveInt(value, fallback, minValue) {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
const rounded = Math.round(value);
if (rounded <= 0) return fallback;
return Math.max(minValue, rounded);
}
function resolveAutoPresenceConfig(config) {
const intervalMs = clampPositiveInt(config?.intervalMs, DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
const minUpdateIntervalMs = clampPositiveInt(config?.minUpdateIntervalMs, DEFAULT_MIN_UPDATE_INTERVAL_MS, MIN_UPDATE_INTERVAL_MS);
return {
enabled: config?.enabled === true,
intervalMs,
minUpdateIntervalMs,
healthyText: normalizeOptionalText(config?.healthyText),
degradedText: normalizeOptionalText(config?.degradedText),
exhaustedText: normalizeOptionalText(config?.exhaustedText)
};
}
function buildCustomStatusActivity(text) {
return {
name: CUSTOM_STATUS_NAME,
type: DEFAULT_CUSTOM_ACTIVITY_TYPE,
state: text
};
}
function renderTemplate(template, vars) {
const rendered = template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_full, key) => vars[key] ?? "").replace(/\s+/g, " ").trim();
return rendered.length > 0 ? rendered : void 0;
}
function isExhaustedUnavailableReason(reason) {
if (!reason) return false;
return reason === "rate_limit" || reason === "overloaded" || reason === "billing" || reason === "auth" || reason === "auth_permanent";
}
function formatUnavailableReason(reason) {
if (!reason) return "unknown";
return reason.replace(/_/g, " ");
}
function resolveAuthAvailability(params) {
const profileIds = Object.keys(params.store.profiles);
if (profileIds.length === 0) return {
state: "degraded",
unavailableReason: null
};
clearExpiredCooldowns(params.store, params.now);
if (profileIds.some((profileId) => !isProfileInCooldown(params.store, profileId, params.now))) return {
state: "healthy",
unavailableReason: null
};
const unavailableReason = resolveProfilesUnavailableReason({
store: params.store,
profileIds,
now: params.now
});
if (isExhaustedUnavailableReason(unavailableReason)) return {
state: "exhausted",
unavailableReason
};
return {
state: "degraded",
unavailableReason
};
}
function resolvePresenceActivities(params) {
const reasonLabel = formatUnavailableReason(params.unavailableReason ?? null);
if (params.state === "healthy") {
if (params.cfg.healthyText) return [buildCustomStatusActivity(params.cfg.healthyText)];
return params.basePresence?.activities ?? [];
}
if (params.state === "degraded") {
const text = renderTemplate(params.cfg.degradedText ?? "runtime degraded", { reason: reasonLabel });
return text ? [buildCustomStatusActivity(text)] : [];
}
const defaultTemplate = isExhaustedUnavailableReason(params.unavailableReason ?? null) ? "token exhausted" : "model unavailable ({reason})";
const text = renderTemplate(params.cfg.exhaustedText ?? defaultTemplate, { reason: reasonLabel });
return text ? [buildCustomStatusActivity(text)] : [];
}
function resolvePresenceStatus(state) {
if (state === "healthy") return "online";
if (state === "exhausted") return "dnd";
return "idle";
}
function resolveDiscordAutoPresenceDecision(params) {
const autoPresence = resolveAutoPresenceConfig(params.discordConfig.autoPresence);
if (!autoPresence.enabled) return null;
const now = params.now ?? Date.now();
const basePresence = resolveDiscordPresenceUpdate(params.discordConfig);
const availability = resolveAuthAvailability({
store: params.authStore,
now
});
const state = params.gatewayConnected ? availability.state : "degraded";
const unavailableReason = params.gatewayConnected ? availability.unavailableReason : availability.unavailableReason ?? "unknown";
return {
state,
unavailableReason,
presence: {
since: null,
activities: resolvePresenceActivities({
state,
cfg: autoPresence,
basePresence,
unavailableReason
}),
status: resolvePresenceStatus(state),
afk: false
}
};
}
function stablePresenceSignature(payload) {
return JSON.stringify({
status: payload.status,
afk: payload.afk,
since: payload.since,
activities: payload.activities.map((activity) => ({
type: activity.type,
name: activity.name,
state: activity.state,
url: activity.url
}))
});
}
function createDiscordAutoPresenceController(params) {
const autoCfg = resolveAutoPresenceConfig(params.discordConfig.autoPresence);
if (!autoCfg.enabled) return {
enabled: false,
start: () => void 0,
stop: () => void 0,
refresh: () => void 0,
runNow: () => void 0
};
const loadAuthStore = params.loadAuthStore ?? (() => ensureAuthProfileStore());
const now = params.now ?? (() => Date.now());
const setIntervalFn = params.setIntervalFn ?? setInterval;
const clearIntervalFn = params.clearIntervalFn ?? clearInterval;
let timer;
let lastAppliedSignature = null;
let lastAppliedAt = 0;
const runEvaluation = (options) => {
let decision;
try {
decision = resolveDiscordAutoPresenceDecision({
discordConfig: params.discordConfig,
authStore: loadAuthStore(),
gatewayConnected: params.gateway.isConnected,
now: now()
});
} catch (err) {
params.log?.(warn(`discord: auto-presence evaluation failed for account ${params.accountId}: ${String(err)}`));
return;
}
if (!decision || !params.gateway.isConnected) return;
const forceApply = options?.force === true;
const ts = now();
const signature = stablePresenceSignature(decision.presence);
if (!forceApply && signature === lastAppliedSignature) return;
if (!forceApply && lastAppliedAt > 0 && ts - lastAppliedAt < autoCfg.minUpdateIntervalMs) return;
params.gateway.updatePresence(decision.presence);
lastAppliedSignature = signature;
lastAppliedAt = ts;
};
return {
enabled: true,
runNow: () => runEvaluation(),
refresh: () => runEvaluation({ force: true }),
start: () => {
if (timer) return;
runEvaluation({ force: true });
timer = setIntervalFn(() => runEvaluation(), autoCfg.intervalMs);
},
stop: () => {
if (!timer) return;
clearIntervalFn(timer);
timer = void 0;
}
};
}
//#endregion
//#region extensions/discord/src/network-config.ts
const DISCORD_DNS_HOSTS = [
"discord.com",
"discord.gg",
"gateway.discord.gg"
];
function normalizeHostname(hostname) {
return hostname.trim().toLowerCase();
}
function isDiscordTransportHostname(hostname) {
const normalized = normalizeHostname(hostname);
if (!normalized) return false;
return DISCORD_DNS_HOSTS.some((target) => normalized === target || normalized.endsWith(`.${target}`));
}
function reorderLookupAddresses(addresses) {
if (!Array.isArray(addresses) || addresses.length < 2) return addresses;
const ipv4 = addresses.filter((entry) => entry.family === 4);
const ipv6 = addresses.filter((entry) => entry.family === 6);
if (ipv4.length === 0) return ipv6;
if (ipv6.length === 0) return ipv4;
return [...ipv4, ...ipv6];
}
function createDiscordDnsLookup() {
return (hostname, options, callback) => {
if (!isDiscordTransportHostname(hostname)) return dns.lookup(hostname, options, callback);
const lookupOptions = typeof options === "number" ? { family: options } : options === void 0 ? {} : { ...options };
if (lookupOptions.family === 4 || lookupOptions.family === 6) return dns.lookup(hostname, lookupOptions, callback);
dns.lookup(hostname, {
...lookupOptions,
all: true
}, (err, addresses) => {
if (err) {
callback(err, "", 4);
return;
}
if (!Array.isArray(addresses)) {
callback(/* @__PURE__ */ new Error("Expected all lookup addresses to be an array"), "", 4);
return;
}
const reordered = reorderLookupAddresses(addresses);
if (lookupOptions.all === true) {
callback(null, reordered);
return;
}
const first = reordered[0];
if (!first) {
callback(/* @__PURE__ */ new Error("No Discord DNS addresses resolved"), "", 4);
return;
}
callback(null, first.address, first.family);
});
};
}
//#endregion
//#region extensions/discord/src/monitor/gateway-handle.ts
const DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT = "openclaw:discord-gateway-transport-activity";
//#endregion
//#region extensions/discord/src/monitor/gateway-metadata.ts
const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
const DISCORD_API_HOST = "discord.com";
const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/";
const DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 3e4;
const MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 12e4;
const DISCORD_GATEWAY_INFO_TIMEOUT_ENV = "OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS";
const DISCORD_GATEWAY_METADATA_FALLBACK_LOG_INTERVAL_MS = 6e4;
const discordGatewayBotInfoSchema = Type.Object({
url: Type.String({ minLength: 1 }),
shards: Type.Integer({ minimum: 1 }),
session_start_limit: Type.Object({
total: Type.Integer({ minimum: 0 }),
remaining: Type.Integer({ minimum: 0 }),
reset_after: Type.Number({ minimum: 0 }),
max_concurrency: Type.Integer({ minimum: 1 })
})
});
const gatewayMetadataFallbackLogLastAt = /* @__PURE__ */ new WeakMap();
function resolveFetchInputUrl(input) {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
return input.url;
}
async function materializeGuardedResponse(response) {
const body = await response.arrayBuffer();
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
function normalizeGatewayInfoTimeoutMs(value) {
const numeric = parseStrictPositiveInteger(value);
if (numeric === void 0) return;
return Math.min(numeric, MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS);
}
function resolveDiscordGatewayInfoTimeoutMs(params) {
return normalizeGatewayInfoTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayInfoTimeoutMs(params?.env?.[DISCORD_GATEWAY_INFO_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS;
}
function summarizeGatewayResponseBody(body) {
return summarizeDiscordResponseBody(body, { emptyText: "<empty>" }) ?? "<empty>";
}
function isDiscordGatewayRateLimitResponse(status, body) {
return status === 429 && isDiscordRateLimitResponseBody(body);
}
function isTransientDiscordGatewayResponse(status, body) {
if (status >= 500) return true;
if (isDiscordGatewayRateLimitResponse(status, body)) return true;
const normalized = body.toLowerCase();
return normalized.includes("upstream connect error") || normalized.includes("disconnect/reset before headers") || normalized.includes("reset reason:");
}
function createGatewayMetadataError(params) {
const error = new Error(params.transient ? "Failed to get gateway information from Discord: fetch failed" : `Failed to get gateway information from Discord: ${params.detail}`, { cause: params.cause ?? (params.transient ? new Error(params.detail) : void 0) });
Object.defineProperty(error, "transient", {
value: params.transient,
enumerable: false
});
return error;
}
function isTransientGatewayMetadataError(error) {
return Boolean(error?.transient);
}
function createDefaultGatewayInfo() {
return {
url: DEFAULT_DISCORD_GATEWAY_URL,
shards: 1,
session_start_limit: {
total: 1,
remaining: 1,
reset_after: 0,
max_concurrency: 1
}
};
}
function summarizeGatewaySchemaErrors(value) {
const errors = Errors(discordGatewayBotInfoSchema, value);
if (errors.length === 0) return "unknown schema mismatch";
return errors.slice(0, 3).map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
}
function parseDiscordGatewayInfoBody(body) {
const parsed = JSON.parse(body);
if (!Check(discordGatewayBotInfoSchema, parsed)) throw new Error(summarizeGatewaySchemaErrors(parsed));
return parsed;
}
async function fetchDiscordGatewayInfo(params) {
let response;
try {
response = await params.fetchImpl(DISCORD_GATEWAY_BOT_URL, {
...params.fetchInit,
headers: {
...params.fetchInit?.headers,
Authorization: `Bot ${params.token}`
}
});
} catch (error) {
throw createGatewayMetadataError({
detail: formatErrorMessage(error),
transient: true,
cause: error
});
}
let body;
try {
body = await response.text();
} catch (error) {
throw createGatewayMetadataError({
detail: formatErrorMessage(error),
transient: true,
cause: error
});
}
const summary = summarizeGatewayResponseBody(body);
const transient = isTransientDiscordGatewayResponse(response.status, body);
if (!response.ok) throw createGatewayMetadataError({
detail: `Discord API /gateway/bot failed (${response.status}): ${summary}`,
transient
});
try {
return parseDiscordGatewayInfoBody(body);
} catch (error) {
throw createGatewayMetadataError({
detail: `Discord API /gateway/bot returned invalid metadata: ${formatErrorMessage(error)} (${summary})`,
transient,
cause: error
});
}
}
async function fetchDiscordGatewayInfoWithTimeout(params) {
const timeoutMs = Math.max(1, params.timeoutMs ?? DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS);
return await withAbortTimeout({
timeoutMs,
createTimeoutError: () => createGatewayMetadataError({
detail: `Discord API /gateway/bot timed out after ${timeoutMs}ms`,
transient: true,
cause: /* @__PURE__ */ new Error("gateway metadata timeout")
}),
run: async (signal) => await fetchDiscordGatewayInfo({
token: params.token,
fetchImpl: params.fetchImpl,
fetchInit: {
...params.fetchInit,
signal
}
})
});
}
function resolveGatewayInfoWithFallback(params) {
if (!isTransientGatewayMetadataError(params.error)) throw params.error;
const message = formatErrorMessage(params.error);
const now = Date.now();
if (params.runtime) {
const previous = gatewayMetadataFallbackLogLastAt.get(params.runtime);
if (previous === void 0 || now - previous >= DISCORD_GATEWAY_METADATA_FALLBACK_LOG_INTERVAL_MS) {
params.runtime.log?.(`discord: gateway metadata lookup failed transiently; using default gateway url (${message})`);
gatewayMetadataFallbackLogLastAt.set(params.runtime, now);
}
}
return {
info: createDefaultGatewayInfo(),
usedFallback: true
};
}
async function fetchDiscordGatewayMetadataGuarded(input, init, options) {
const guarded = await fetchWithSsrFGuard({
url: resolveFetchInputUrl(input),
init,
policy: { allowedHostnames: [DISCORD_API_HOST] },
capture: false,
auditContext: "discord.gateway.metadata",
...options?.proxyUrl ? {
mode: "trusted_explicit_proxy",
dispatcherPolicy: {
mode: "explicit-proxy",
proxyUrl: options.proxyUrl,
allowPrivateProxy: true
}
} : {}
});
let response;
try {
response = await materializeGuardedResponse(guarded.response);
} finally {
await guarded.release();
}
if (options?.capture) captureHttpExchange({
url: input,
method: init?.method ?? "GET",
requestHeaders: init?.headers,
requestBody: init?.body ?? null,
response,
flowId: options.capture.flowId,
meta: options.capture.meta
});
return response;
}
//#endregion
//#region extensions/discord/src/monitor/gateway-plugin.ts
const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 3e4;
const DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE = 1008;
const DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE = "WS_ERR_TOO_MANY_BUFFERED_PARTS";
const DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS = 240;
const discordDnsLookup$1 = createDiscordDnsLookup();
const registrationPromises = /* @__PURE__ */ new WeakMap();
function assignGatewayClient(plugin, client) {
plugin.client = client;
}
function hasGatewaySocketStarted(plugin) {
const state = plugin;
return state.ws != null || state.isConnecting === true;
}
function readStringProperty(value, key) {
const property = value[key];
return typeof property === "string" && property ? property : void 0;
}
function readNumberProperty(value, key) {
const property = value[key];
return typeof property === "number" && Number.isFinite(property) ? property : void 0;
}
function describeDiscordGatewayTransportError(error) {
const code = readStringProperty(error, "code");
const closeCode = readNumberProperty(error, "closeCode");
const statusCode = readNumberProperty(error, "statusCode");
return {
...error.name ? { name: error.name } : {},
message: error.message,
...code ? { code } : {},
...closeCode !== void 0 ? { closeCode } : {},
...statusCode !== void 0 ? { statusCode } : {}
};
}
function formatDiscordGatewayCloseReason(reason) {
if (!reason.length) return "<empty>";
const text = reason.toString("utf8").replaceAll(/\s+/g, " ").trim();
if (!text) return `<${reason.length} bytes>`;
if (text.length <= DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS) return text;
return `${text.slice(0, DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS)}...`;
}
function formatDiscordGatewayTransportErrorLog(params) {
return `discord: gateway websocket error ${[
`flow=${params.flowId}`,
params.error.name ? `name=${params.error.name}` : void 0,
params.error.code ? `code=${params.error.code}` : void 0,
typeof params.error.closeCode === "number" ? `closeCode=${params.error.closeCode}` : void 0,
typeof params.error.statusCode === "number" ? `statusCode=${params.error.statusCode}` : void 0,
`message=${params.error.message}`
].filter(Boolean).join(" ")}`;
}
function formatDiscordGatewayTransportCloseLog(params) {
const receiverLimit = params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE || params.lastError?.code === DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE;
return `discord: gateway websocket closed ${[
`flow=${params.flowId}`,
`code=${params.code}`,
`reasonBytes=${params.reason.length}`,
`reason=${formatDiscordGatewayCloseReason(params.reason)}`,
params.lastError?.code ? `lastErrorCode=${params.lastError.code}` : void 0,
params.lastError?.message ? `lastError=${params.lastError.message}` : void 0,
receiverLimit ? "hint=possible ws receiver buffered-parts limit" : void 0
].filter(Boolean).join(" ")}`;
}
function shouldLogDiscordGatewayTransportClose(params) {
return params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE || params.code !== 1e3 && params.code !== 1001 || params.reason.length > 0 || params.lastError !== void 0;
}
function resolveDiscordGatewayIntents(params) {
const intentsConfig = params?.intentsConfig;
const voiceEnabled = params?.voiceEnabled;
const voiceStatesEnabled = intentsConfig?.voiceStates ?? voiceEnabled ?? false;
let intents = GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.MessageContent | GatewayIntents.DirectMessages | GatewayIntents.GuildMessageReactions | GatewayIntents.DirectMessageReactions;
if (voiceStatesEnabled) intents |= GatewayIntents.GuildVoiceStates;
if (intentsConfig?.presence) intents |= GatewayIntents.GuildPresences;
if (intentsConfig?.guildMembers) intents |= GatewayIntents.GuildMembers;
return intents;
}
function createGatewayPlugin(params) {
class OpenClawGatewayPlugin extends GatewayPlugin {
constructor() {
super(params.options);
this.gatewayInfoUsedFallback = false;
}
registerClient(client) {
const registration = this.registerClientInternal(client);
registration.catch(() => {});
registrationPromises.set(this, registration);
return registration;
}
async registerClientInternal(client) {
assignGatewayClient(this, client);
if (!this.gatewayInfo || this.gatewayInfoUsedFallback) {
const resolved = await fetchDiscordGatewayInfoWithTimeout({
token: client.options.token,
fetchImpl: params.fetchImpl,
fetchInit: params.fetchInit,
timeoutMs: params.gatewayInfoTimeoutMs
}).then((info) => ({
info,
usedFallback: false
})).catch((error) => resolveGatewayInfoWithFallback({
runtime: params.runtime,
error
}));
this.gatewayInfo = resolved.info;
this.gatewayInfoUsedFallback = resolved.usedFallback;
}
if (params.testing?.registerClient) {
await params.testing.registerClient(this, client);
return;
}
if (hasGatewaySocketStarted(this)) return;
return super.registerClient(client);
}
createWebSocket(url) {
if (!url) throw new Error("Gateway URL is required");
const wsFlowId = randomUUID();
const socket = new (params.testing?.webSocketCtor ?? ws.default)(url, {
handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
...params.wsAgent ? { agent: params.wsAgent } : {}
});
let lastTransportError;
const emitTransportActivity = () => {
if (this.ws !== socket) return;
this.emitter.emit(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, { at: Date.now() });
};
captureWsEvent({
url,
direction: "local",
kind: "ws-open",
flowId: wsFlowId,
meta: { subsystem: "discord-gateway" }
});
socket.on?.("message", (data) => {
emitTransportActivity();
captureWsEvent({
url,
direction: "inbound",
kind: "ws-frame",
flowId: wsFlowId,
payload: Buffer.isBuffer(data) ? data : Buffer.from(String(data)),
meta: { subsystem: "discord-gateway" }
});
});
socket.on?.("close", (code, reason) => {
const closeReason = Buffer.isBuffer(reason) ? reason : Buffer.from(String(reason ?? ""));
captureWsEvent({
url,
direction: "local",
kind: "ws-close",
flowId: wsFlowId,
closeCode: code,
payload: closeReason,
meta: { subsystem: "discord-gateway" }
});
if (shouldLogDiscordGatewayTransportClose({
code,
reason: closeReason,
lastError: lastTransportError
})) params.runtime?.log?.(warn(formatDiscordGatewayTransportCloseLog({
flowId: wsFlowId,
code,
reason: closeReason,
lastError: lastTransportError
})));
});
socket.on?.("error", (error) => {
lastTransportError = describeDiscordGatewayTransportError(error);
captureWsEvent({
url,
direction: "local",
kind: "error",
flowId: wsFlowId,
errorText: error.message,
meta: { subsystem: "discord-gateway" }
});
params.runtime?.log?.(warn(formatDiscordGatewayTransportErrorLog({
flowId: wsFlowId,
error: lastTransportError
})));
});
if ("binaryType" in socket) try {
socket.binaryType = "arraybuffer";
} catch {}
return socket;
}
}
return new OpenClawGatewayPlugin();
}
function createDiscordGatewayMetadataFetch(debugCaptureEnabled, proxyUrl) {
return (input, init) => fetchDiscordGatewayMetadataGuarded(input, init, {
...debugCaptureEnabled ? {} : { capture: {
flowId: randomUUID(),
meta: { subsystem: "discord-gateway-metadata" }
} },
...proxyUrl ? { proxyUrl } : {}
});
}
function waitForDiscordGatewayPluginRegistration(plugin) {
if (typeof plugin !== "object" || plugin === null) return;
return registrationPromises.get(plugin);
}
function createDiscordGatewayPlugin(params) {
const intents = resolveDiscordGatewayIntents({
intentsConfig: params.discordConfig?.intents,
voiceEnabled: resolveDiscordVoiceEnabled(params.discordConfig?.voice)
});
const proxy = resolveEffectiveDebugProxyUrl(params.discordConfig?.proxy);
const debugProxySettings = resolveDebugProxySettings();
const gatewayInfoTimeoutMs = resolveDiscordGatewayInfoTimeoutMs({
configuredTimeoutMs: params.discordConfig?.gatewayInfoTimeoutMs,
env: process.env
});
let fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled);
let wsAgent = new Agent({ lookup: discordDnsLookup$1 });
if (proxy) try {
validateDiscordProxyUrl(proxy);
wsAgent = params.testing?.createProxyAgent?.(proxy) ?? createNodeProxyAgent({
mode: "explicit",
proxyUrl: proxy,
protocol: "https"
});
fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled, proxy);
params.runtime.log?.("discord: gateway proxy enabled");
} catch (err) {
params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
fetchImpl = (input, init) => fetchDiscordGatewayMetadataGuarded(input, init, { capture: false });
}
return createGatewayPlugin({
options: {
reconnect: { maxAttempts: 50 },
intents,
autoInteractions: false
},
gatewayInfoTimeoutMs,
fetchImpl,
runtime: params.runtime,
testing: params.testing,
...wsAgent ? { wsAgent } : {}
});
}
//#endregion
//#region extensions/discord/src/monitor/gateway-supervisor.ts
var DiscordGatewayLifecycleError = class extends Error {
constructor(event) {
super(`discord gateway ${event.type}: ${event.message}`, { cause: event.err instanceof Error ? event.err : void 0 });
this.name = "DiscordGatewayLifecycleError";
this.eventType = event.type;
}
};
function getDiscordGatewayEmitter(gateway) {
return gateway?.emitter;
}
function readFirstStackFrame(err) {
const stack = err.stack;
if (!stack) return;
const frame = stack.split("\n").slice(1).map((line) => line.trim()).find(Boolean);
return frame ? frame.replace(/^at\s+/, "") : void 0;
}
function formatDiscordGatewayErrorMessage(err) {
if (!(err instanceof Error)) return formatErrorMessage(err);
if (err.message) {
const detail = formatErrorMessage(err);
return err.name ? `${err.name}: ${detail}` : detail;
}
const detail = formatErrorMessage(err);
const firstFrame = readFirstStackFrame(err);
if (firstFrame && detail === (err.name || "Error")) return `${detail} @ ${firstFrame}`;
return detail;
}
function classifyDiscordGatewayEvent(params) {
const message = formatDiscordGatewayErrorMessage(params.err);
if (params.isDisallowedIntentsError(params.err)) return {
type: "disallowed-intents",
err: params.err,
message,
shouldStopLifecycle: true
};
if (message.includes("Max reconnect attempts")) return {
type: "reconnect-exhausted",
err: params.err,
message,
shouldStopLifecycle: true
};
if (params.err instanceof TypeError || message.includes("Fatal Gateway error") || message.includes("Fatal gateway close code") || message.includes("Gateway HELLO missing heartbeat") || message.includes("Invalid gateway payload") || message.includes("Gateway socket emitted an unknown error")) return {
type: "fatal",
err: params.err,
message,
shouldStopLifecycle: true
};
return {
type: "other",
err: params.err,
message,
shouldStopLifecycle: false
};
}
function createDiscordGatewaySupervisor(params) {
const emitter = getDiscordGatewayEmitter(params.gateway);
const pending = [];
if (!emitter) return {
attachLifecycle: () => {},
detachLifecycle: () => {},
drainPending: () => "continue",
dispose: () => {},
emitter
};
let lifecycleHandler;
let phase = "buffering";
const seenLateEventKeys = /* @__PURE__ */ new Set();
const logLateEvent = (state) => (event) => {
const key = `${state}:${event.type}:${event.message}`;
if (seenLateEventKeys.has(key)) return;
seenLateEventKeys.add(key);
params.runtime.error?.(danger(`discord: suppressed late gateway ${event.type} error ${state === "disposed" ? "after dispose" : "during teardown"}: ${event.message}`));
};
const onGatewayError = (err) => {
const event = classifyDiscordGatewayEvent({
err,
isDisallowedIntentsError: params.isDisallowedIntentsError
});
switch (phase) {
case "disposed":
logLateEvent("disposed")(event);
return;
case "active":
lifecycleHandler?.(event);
return;
case "teardown":
logLateEvent("teardown")(event);
return;
case "buffering": pending.push(event);
}
};
emitter.on("error", onGatewayError);
return {
emitter,
attachLifecycle: (handler) => {
lifecycleHandler = handler;
phase = "active";
},
detachLifecycle: () => {
lifecycleHandler = void 0;
phase = "teardown";
},
drainPending: (handler) => {
if (pending.length === 0) return "continue";
const queued = [...pending];
pending.length = 0;
for (const event of queued) if (handler(event) === "stop") return "stop";
return "continue";
},
dispose: () => {
if (phase === "disposed") return;
lifecycleHandler = void 0;
phase = "disposed";
pending.length = 0;
}
};
}
//#endregion
//#region extensions/discord/src/monitor/provider.acp.ts
const DISCORD_ACP_STATUS_PROBE_TIMEOUT_MS = 8e3;
const DISCORD_ACP_STALE_RUNNING_ACTIVITY_MS = 120 * 1e3;
function isLegacyMissingSessionError(message) {
return message.includes("Session is not ACP-enabled") || message.includes("ACP session metadata missing");
}
function classifyAcpStatusProbeError(params) {
if (params.isAcpRuntimeError(params.error) && params.error.code === "ACP_SESSION_INIT_FAILED") return {
status: "stale",
reason: "session-init-failed"
};
if (isLegacyMissingSessionError(formatErrorMessage(params.error))) return {
status: "stale",
reason: "session-missing"
};
return params.isStaleRunning ? {
status: "stale",
reason: "status-error-running-stale"
} : {
status: "uncertain",
reason: "status-error"
};
}
function resolveRunningActivityAgeMs(params) {
if (params.storedState !== "running") return 0;
const nowMs = asDateTimestampMs(Date.now());
if (nowMs === void 0) return 0;
const activityAtMs = asDateTimestampMs(params.lastActivityAt);
return Math.max(0, nowMs - (activityAtMs === void 0 ? 0 : Math.max(0, Math.floor(activityAtMs))));
}
async function probeDiscordAcpBindingHealth(params) {
const { getAcpSessionManager, isAcpRuntimeError } = params.providerSessionRuntime;
const manager = getAcpSessionManager();
const statusProbeAbortController = new AbortController();
const result = await raceWithTimeout({
promise: manager.getSessionStatus({
cfg: params.cfg,
sessionKey: params.sessionKey,
signal: statusProbeAbortController.signal
}).then((status) => ({
kind: "status",
status
})).catch((error) => ({
kind: "error",
error
})),
timeoutMs: DISCORD_ACP_STATUS_PROBE_TIMEOUT_MS,
onTimeout: () => ({ kind: "timeout" })
});
if (result.kind === "timeout") statusProbeAbortController.abort();
const runningForMs = resolveRunningActivityAgeMs(params);
const isStaleRunning = params.storedState === "running" && runningForMs >= DISCORD_ACP_STALE_RUNNING_ACTIVITY_MS;
if (result.kind === "timeout") return isStaleRunning ? {
status: "stale",
reason: "status-timeout-running-stale"
} : {
status: "uncertain",
reason: "status-timeout"
};
if (result.kind === "error") return classifyAcpStatusProbeError({
error: result.error,
isStaleRunning,
isAcpRuntimeError
});
if (result.status.state === "error") return {
status: "uncertain",
reason: "status-error-state"
};
return { status: "healthy" };
}
//#endregion
//#region extensions/discord/src/monitor/provider.allowlist.ts
function formatResolutionLogDetails(base, details) {
const nonEmpty = details.map((value) => value?.trim()).filter((value) => Boolean(value));
return nonEmpty.length > 0 ? `${base} (${nonEmpty.join("; ")})` : base;
}
function formatResolvedBase(input, target) {
if (!target) return input;
return input === target ? input : `${input}→${target}`;
}
function formatAliasSummary(aliases) {
if (aliases.length === 0) return;
const preview = aliases.slice(0, 3).join(", ");
if (aliases.length <= 3) return preview;
return `${preview}, +${aliases.length - 3} more`;
}
function formatDiscordChannelResolvedGroup(entry) {
const aliasSummary = formatAliasSummary(entry.aliases);
return formatResolutionLogDetails(entry.target, [
entry.guildName ? `guild:${entry.guildName}` : void 0,
entry.channelName ? `channel:${entry.channelName}` : void 0,
entry.note,
aliasSummary ? `aliases:${aliasSummary}` : void 0
]);
}
function formatDiscordChannelUnresolved(entry) {
return formatResolutionLogDetails(entry.input, [
entry.guildName ? `guild:${entry.guildName}` : entry.guildId ? `guildId:${entry.guildId}` : void 0,
entry.channelName ? `channel:${entry.channelName}` : entry.channelId ? `channelId:${entry.channelId}` : void 0,
entry.note
]);
}
function formatDiscordUserResolved(entry) {
const displayName = entry.name?.trim();
const target = displayName || entry.id;
return formatResolutionLogDetails(formatResolvedBase(entry.input, target), [
displayName && entry.id ? `id:${entry.id}` : void 0,
entry.guildName ? `guild:${entry.guildName}` : void 0,
entry.note
]);
}
function formatDiscordUserUnresolved(entry) {
return formatResolutionLogDetails(entry.input, [
entry.name ? `name:${entry.name}` : void 0,
entry.guildName ? `guild:${entry.guildName}` : void 0,
entry.note
]);
}
function toGuildEntries(value) {
if (!value || typeof value !== "object") return {};
const out = {};
for (const [key, entry] of Object.entries(value)) {
if (!entry || typeof entry !== "object") continue;
out[key] = entry;
}
return out;
}
function toAllowlistEntries(value) {
if (!Array.isArray(value)) return;
return normalizeStringEntries(value);
}
function hasGuildEntries(value) {
return Object.keys(value).length > 0;
}
function collectChannelResolutionInputs(guildEntries) {
const entries = [];
for (const [guildKey, guildCfg] of Object.entries(guildEntries)) {
if (guildKey === "*") continue;
const channels = guildCfg?.channels ?? {};
const channelKeys = Object.keys(channels).filter((key) => key !== "*");
if (channelKeys.length === 0) {
const input = /^\d+$/.test(guildKey) ? `guild:${guildKey}` : guildKey;
entries.push({
input,
guildKey
});
continue;
}
for (const channelKey of channelKeys) entries.push({
input: `${guildKey}/${channelKey}`,
guildKey,
channelKey
});
}
return entries;
}
async function resolveGuildEntriesByChannelAllowlist(params) {
const entries = collectChannelResolutionInputs(params.guildEntries);
if (entries.length === 0) return params.guildEntries;
try {
const resolved = await resolveDiscordChannelAllowlist({
token: params.token,
entries: entries.map((entry) => entry.input),
fetcher: params.fetcher
});
const sourceByInput = new Map(entries.map((entry) => [entry.input, entry]));
const nextGuilds = { ...params.guildEntries };
const mappingByTarget = /* @__PURE__ */ new Map();
const unresolved = [];
for (const entry of resolved) {
const source = sourceByInput.get(entry.input);
if (!source) continue;
const sourceGuild = params.guildEntries[source.guildKey] ?? {};
if (!entry.resolved || !entry.guildId) {
unresolved.push(formatDiscordChannelUnresolved(entry));
continue;
}
const target = entry.channelId ? `${entry.guildId}/${entry.channelId}` : entry.guildId;
const existingGroup = mappingByTarget.get(target) ?? {
target,
aliases: [],
guildName: entry.guildName,
channelName: entry.channelName,
note: entry.note
};
if (entry.input !== target && !existingGroup.aliases.includes(entry.input)) existingGroup.aliases.push(entry.input);
if (!existingGroup.guildName && entry.guildName) existingGroup.guildName = entry.guildName;
if (!existingGroup.channelName && entry.channelName) existingGroup.channelName = entry.channelName;
if (!existingGroup.note && entry.note) existingGroup.note = entry.note;
mappingByTarget.set(target, existingGroup);
const existing = nextGuilds[entry.guildId] ?? {};
const mergedChannels = {
...sourceGuild.channels,
...existing.channels
};
const mergedGuild = {
...sourceGuild,
...existing,
channels: mergedChannels
};
nextGuilds[entry.guildId] = mergedGuild;
if (source.channelKey && entry.channelId) {
const sourceChannel = sourceGuild.channels?.[source.channelKey];
if (sourceChannel) nextGuilds[entry.guildId] = {
...mergedGuild,
channels: {
...mergedChannels,
[entry.channelId]: {
...sourceChannel,
...mergedChannels[entry.channelId]
}
}
};
}
}
summarizeMapping("discord channels", [...mappingByTarget.values()].map((group) => formatDiscordChannelResolvedGroup(group)), unresolved, params.runtime);
return nextGuilds;
} catch (err) {
params.runtime.log?.(`discord channel resolve failed; using config entries. ${formatErrorMessage(err)}`);
return params.guildEntries;
}
}
async function resolveAllowFromByUserAllowlist(params) {
const allowEntries = normalizeStringEntries(params.allowFrom).filter((entry) => entry !== "*");
if (allowEntries.length === 0) return params.allowFrom;
try {
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(await resolveDiscordUserAllowlist({
token: params.token,
entries: allowEntries,
fetcher: params.fetcher
}), {
formatResolved: formatDiscordUserResolved,
formatUnresolved: formatDiscordUserUnresolved
});
const allowFrom = canonicalizeAllowlistWithResolvedIds({
existing: params.allowFrom,
resolvedMap
});
summarizeMapping("discord users", mapping, unresolved, params.runtime);
return allowFrom;
} catch (err) {
params.runtime.log?.(`discord user resolve failed; using config entries. ${formatErrorMessage(err)}`);
return params.allowFrom;
}
}
function collectGuildUserEntries(guildEntries) {
const userEntries = /* @__PURE__ */ new Set();
for (const guild of Object.values(guildEntries)) {
if (!guild || typeof guild !== "object") continue;
addAllowlistUserEntriesFromConfigEntry(userEntries, guild);
const channels = guild.channels ?? {};
for (const channel of Object.values(channels)) addAllowlistUserEntriesFromConfigEntry(userEntries, channel);
}
return userEntries;
}
async function resolveGuildEntriesByUserAllowlist(params) {
const userEntries = collectGuildUserEntries(params.guildEntries);
if (userEntries.size === 0) return params.guildEntries;
try {
const { resolvedMap, mapping, unresolved } = buildAllowlistResolutionSummary(await resolveDiscordUserAllowlist({
token: params.token,
entries: Array.from(userEntries),
fetcher: params.fetcher
}), {
formatResolved: formatDiscordUserResolved,
formatUnresolved: formatDiscordUserUnresolved
});
const nextGuilds = { ...params.guildEntries };
for (const [guildKey, guildConfig] of Object.entries(params.guildEntries)) {
if (!guildConfig || typeof guildConfig !== "object") continue;
const nextGuild = { ...guildConfig };
const users = guildConfig.users;
if (Array.isArray(users) && users.length > 0) nextGuild.users = canonicalizeAllowlistWithResolvedIds({
existing: users,
resolvedMap
});
const channels = guildConfig.channels ?? {};
if (channels && typeof channels === "object") nextGuild.channels = patchAllowlistUsersInConfigEntries({
entries: channels,
resolvedMap,
strategy: "canonicalize"
});
nextGuilds[guildKey] = nextGuild;
}
summarizeMapping("discord channel users", mapping, unresolved, params.runtime);
return nextGuilds;
} catch (err) {
params.runtime.log?.(`discord channel user resolve failed; using config entries. ${formatErrorMessage(err)}`);
return params.guildEntries;
}
}
async function resolveDiscordAllowlistConfig(params) {
let guildEntries = toGuildEntries(params.guildEntries);
let allowFrom = toAllowlistEntries(params.allowFrom);
if (hasGuildEntries(guildEntries)) guildEntries = await resolveGuildEntriesByChannelAllowlist({
token: params.token,
guildEntries,
fetcher: params.fetcher,
runtime: params.runtime
});
if (isDangerousNameMatchingEnabled(params.discordConfig)) {
allowFrom = await resolveAllowFromByUserAllowlist({
token: params.token,
allowFrom,
fetcher: params.fetcher,
runtime: params.runtime
});
if (hasGuildEntries(guildEntries)) guildEntries = await resolveGuildEntriesByUserAllowlist({
token: params.token,
guildEntries,
fetcher: params.fetcher,
runtime: params.runtime
});
}
return {
guildEntries: hasGuildEntries(guildEntries) ? guildEntries : void 0,
allowFrom
};
}
//#endregion
//#region extensions/discord/src/monitor/provider.cleanup.ts
function cleanupDiscordProviderStartup(params) {
params.deactivateMessageHandler?.();
params.autoPresenceController?.stop();
params.setStatus?.({ connected: false });
if (params.onEarlyGatewayDebug) params.earlyGatewayEmitter?.removeListener("debug", params.onEarlyGatewayDebug);
if (!params.lifecycleStarted) try {
params.lifecycleGateway?.disconnect();
} catch (err) {
params.runtime.error?.(danger(`discord: failed to disconnect gateway during startup cleanup: ${String(err)}`));
}
params.gatewaySupervisor?.dispose();
if (!params.lifecycleStarted) params.threadBindings.stop();
}
//#endregion
//#region extensions/discord/src/monitor/provider.commands.ts
let pluginRuntimePromise;
async function loadPluginRuntime() {
const promise = pluginRuntimePromise ?? import("./plugin-sdk/plugin-runtime.js");
pluginRuntimePromise = promise;
try {
return await promise;
} catch (error) {
if (pluginRuntimePromise === promise) pluginRuntimePromise = void 0;
throw error;
}
}
async function appendPluginCommandSpecs(params) {
const merged = [...params.commandSpecs];
const existingNames = new Set(normalizeStringEntriesLower(merged.map((spec) => spec.name)));
const getPluginCommandSpecs = params.getPluginCommandSpecs ?? (await loadPluginRuntime()).getPluginCommandSpecs;
for (const pluginCommand of getPluginCommandSpecs("discord", { config: params.cfg })) {
const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name);
if (!normalizedName) continue;
if (existingNames.has(normalizedName)) {
params.runtime.error?.(danger(`discord: plugin command "/${normalizedName}" duplicates an existing native command. Skipping.`));
continue;
}
existingNames.add(normalizedName);
merged.push({
name: pluginCommand.name,
description: pluginCommand.description,
acceptsArgs: pluginCommand.acceptsArgs
});
}
return merged;
}
async function resolveDiscordProviderCommandSpecs(params) {
const listSkillCommands = params.listSkillCommandsForAgents ?? listSkillCommandsForAgents;
const listNativeCommandSpecs = params.listNativeCommandSpecsForConfig ?? listNativeCommandSpecsForConfig;
const maxDiscordCommands = params.maxDiscordCommands ?? 100;
let skillCommands = params.nativeEnabled && params.nativeSkillsEnabled ? listSkillCommands({ cfg: params.cfg }) : [];
let commandSpecs = params.nativeEnabled ? listNativeCommandSpecs(params.cfg, {
skillCommands,
provider: "discord"
}) : [];
if (params.nativeEnabled) commandSpecs = await appendPluginCommandSpecs({
commandSpecs,
runtime: params.runtime,
cfg: params.cfg,
getPluginCommandSpecs: params.getPluginCommandSpecs
});
const initialCommandCount = commandSpecs.length;
if (params.nativeEnabled && params.nativeSkillsEnabled && commandSpecs.length > maxDiscordCommands) {
skillCommands = [];
commandSpecs = listNativeCommandSpecs(params.cfg, {
skillCommands: [],
provider: "discord"
});
commandSpecs = await appendPluginCommandSpecs({
commandSpecs,
runtime: params.runtime,
cfg: params.cfg,
getPluginCommandSpecs: params.getPluginCommandSpecs
});
params.runtime.log?.(warn(`discord: ${initialCommandCount} commands exceeds limit; removing per-skill commands and keeping /skill.`));
}
if (params.nativeEnabled && commandSpecs.length > maxDiscordCommands) params.runtime.log?.(warn(`discord: ${commandSpecs.length} commands exceeds limit; some commands may fail to deploy.`));
return {
skillCommands,
commandSpecs
};
}
//#endregion
//#region extensions/discord/src/monitor/provider.config-log.ts
function formatThreadBindingDurationForConfigLabel(durationMs) {
const label = formatThreadBindingDurationLabel(durationMs);
return label === "disabled" ? "off" : label;
}
function logDiscordResolvedConfig(params) {
const allowFromSummary = summarizeStringEntries({
entries: params.allowFrom ?? [],
limit: 4,
emptyText: "any"
});
const groupDmChannelSummary = summarizeStringEntries({
entries: params.groupDmChannels ?? [],
limit: 4,
emptyText: "any"
});
const guildSummary = summarizeStringEntries({
entries: Object.keys(params.guildEntries ?? {}),
limit: 4,
emptyText: "any"
});
logVerbose(`discord: config dm=${params.dmEnabled ? "on" : "off"} dmPolicy=${params.dmPolicy} allowFrom=${allowFromSummary} groupDm=${params.groupDmEnabled ? "on" : "off"} groupDmChannels=${groupDmChannelSummary} groupPolicy=${params.groupPolicy} guilds=${guildSummary} historyLimit=${params.historyLimit} mediaMaxMb=${Math.round(params.mediaMaxBytes / (1024 * 1024))} native=${params.nativeEnabled ? "on" : "off"} nativeSkills=${params.nativeSkillsEnabled ? "on" : "off"} accessGroups=${params.useAccessGroups ? "on" : "off"} threadBindings=${params.threadBindingsEnabled ? "on" : "off"} threadIdleTimeout=${formatThreadBindingDurationForConfigLabel(params.threadBindingIdleTimeoutMs)} threadMaxAge=${formatThreadBindingDurationForConfigLabel(params.threadBindingMaxAgeMs)}`);
}
//#endregion
//#region extensions/discord/src/monitor/provider.deploy-errors.ts
const DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT = 3;
function attachDiscordDeployRequestBody(err, body) {
if (!err || typeof err !== "object" || body === void 0) return;
const deployErr = err;
if (deployErr.deployRequestBody === void 0) deployErr.deployRequestBody = body;
}
function attachDiscordDeployRestContext(err, context) {
if (!err || typeof err !== "object") return;
const deployErr = err;
deployErr.deployRestMethod = context.method;
deployErr.deployRestPath = context.path;
deployErr.deployRequestMs = context.requestMs;
if (typeof context.timeoutMs === "number" && Number.isFinite(context.timeoutMs)) deployErr.deployTimeoutMs = context.timeoutMs;
}
function stringifyDiscordDeployField(value) {
if (typeof value === "string") return JSON.stringify(value);
try {
return JSON.stringify(value);
} catch {
return inspect(value, {
depth: 2,
breakLength: 120
});
}
}
function readDiscordDeployRejectedFields(value) {
if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string").slice(0, 6);
if (!value || typeof value !== "object") return [];
return Object.keys(value).slice(0, 6);
}
function resolveDiscordRejectedDeployEntriesSource(rawBody) {
if (!rawBody || typeof rawBody !== "object") return null;
const payload = rawBody;
const source = (payload.errors && typeof payload.errors === "object" ? payload.errors : void 0) ?? rawBody;
return source && typeof source === "object" ? source : null;
}
function readDiscordDeployObjectField(value, field) {
return value && typeof value === "object" && field in value ? value[field] : void 0;
}
function readFiniteNumber(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) return parseStrictFiniteNumber(value);
}
function readNonNegativeInteger(value) {
return parseStrictNonNegativeInteger(value);
}
function formatDurationMs(ms) {
return formatDurationSeconds(ms, { decimals: ms >= 1e3 ? 1 : 0 });
}
function isAbortLikeError(err) {
if (!err || typeof err !== "object") return false;
const name = "name" in err && typeof err.name === "string" ? err.name : void 0;
const message = formatErrorMessage(err);
return name === "AbortError" || message === "This operation was aborted" || message === "The operation was aborted" || /\boperation was aborted\b/i.test(message);
}
function formatDiscordDeployRestOperation(err) {
const method = typeof err.deployRestMethod === "string" && err.deployRestMethod.trim().length > 0 ? err.deployRestMethod.toUpperCase() : void 0;
const path = typeof err.deployRestPath === "string" && err.deployRestPath.trim().length > 0 ? err.deployRestPath : void 0;
if (method && path) return `${method} ${path}`;
if (method) return method;
if (path) return path;
return "request";
}
function formatDiscordDeployErrorMessage(err) {
if (!isAbortLikeError(err)) return formatErrorMessage(err);
const deployErr = err && typeof err === "object" ? err : {};
const requestMs = readFiniteNumber(deployErr.deployRequestMs);
const timeoutMs = readFiniteNumber(deployErr.deployTimeoutMs);
const operation = formatDiscordDeployRestOperation(deployErr);
if (!(requestMs !== void 0 || timeoutMs !== void 0 || deployErr.deployRestMethod !== void 0 || deployErr.deployRestPath !== void 0)) return "Discord REST request was aborted";
const timing = [];
if (timeoutMs !== void 0) timing.push(`timeout=${formatDurationMs(timeoutMs)}`);
if (requestMs !== void 0) timing.push(`observed=${formatDurationMs(requestMs)}`);
const timingText = timing.length > 0 ? ` (${timing.join(", ")})` : "";
if (timeoutMs !== void 0 && requestMs !== void 0 && requestMs >= timeoutMs) return `Discord REST ${operation} timed out${timingText}`;
return `Discord REST ${operation} was aborted${timingText}`;
}
function resolveDiscordDeployRateLimitDetails(err) {
if (!err || typeof err !== "object") return;
const deployErr = err;
const status = readNonNegativeInteger(deployErr.status) ?? readNonNegativeInteger(deployErr.statusCode);
const retryAfterSeconds = readFiniteNumber(deployErr.retryAfter) ?? readFiniteNumber(readDiscordDeployObjectField(deployErr.rawBody, "retry_after"));
if (!(err instanceof RateLimitError || status === 429 || retryAfterSeconds !== void 0)) return;
const rawGlobal = readDiscordDeployObjectField(deployErr.rawBody, "global");
const scope = typeof deployErr.scope === "string" && deployErr.scope.trim().length > 0 ? deployErr.scope : rawGlobal === true ? "global" : rawGlobal === false ? "route" : void 0;
const discordCode = typeof deployErr.discordCode === "number" || typeof deployErr.discordCode === "string" ? deployErr.discordCode : void 0;
return {
status,
retryAfterMs: retryAfterSeconds === void 0 ? void 0 : Math.max(0, retryAfterSeconds * 1e3),
scope,
discordCode
};
}
function formatDiscordDeployRateLimitDetails(err) {
const rateLimit = resolveDiscordDeployRateLimitDetails(err);
if (!rateLimit) return "";
const details = [];
if (typeof rateLimit.status === "number") details.push(`status=${rateLimit.status}`);
if (typeof rateLimit.retryAfterMs === "number") details.push(`retryAfter=${formatDurationSeconds(rateLimit.retryAfterMs, { decimals: 1 })}`);
if (rateLimit.scope) details.push(`scope=${rateLimit.scope}`);
if (typeof rateLimit.discordCode === "number" || typeof rateLimit.discordCode === "string") details.push(`code=${rateLimit.discordCode}`);
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
function formatDiscordDeployRateLimitWarning(err, accountId) {
const rateLimit = resolveDiscordDeployRateLimitDetails(err);
if (!rateLimit) return;
const parts = [`discord: native slash command deploy rate limited for ${accountId}`];
if (typeof rateLimit.retryAfterMs === "number") parts.push(`retry after ${formatDurationSeconds(rateLimit.retryAfterMs, { decimals: 1 })}`);
if (rateLimit.scope) parts.push(`scope=${rateLimit.scope}`);
if (typeof rateLimit.discordCode === "number" || typeof rateLimit.discordCode === "string") parts.push(`code=${rateLimit.discordCode}`);
return `${parts.join("; ")}. Existing slash commands stay active. Message send/receive is unaffected.`;
}
function formatDiscordRejectedDeployEntries(params) {
const requestBody = Array.isArray(params.requestBody) ? params.requestBody : null;
const rejectedEntriesSource = resolveDiscordRejectedDeployEntriesSource(params.rawBody);
if (!rejectedEntriesSource || !requestBody || requestBody.length === 0) return [];
return Object.entries(rejectedEntriesSource).filter(([key]) => /^\d+$/.test(key)).slice(0, DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT).flatMap(([key, value]) => {
const index = Number.parseInt(key, 10);
if (!Number.isFinite(index) || index < 0 || index >= requestBody.length) return [];
const command = requestBody[index];
if (!command || typeof command !== "object") return [`#${index} fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`];
const payload = command;
const parts = [`#${index}`, `fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`];
if (typeof payload.name === "string" && payload.name.trim().length > 0) parts.push(`name=${payload.name}`);
if (payload.description !== void 0) parts.push(`description=${stringifyDiscordDeployField(payload.description)}`);
if (Array.isArray(payload.options) && payload.options.length > 0) parts.push(`options=${payload.options.length}`);
return [parts.join(" ")];
});
}
function formatDiscordDeployErrorDetails(err) {
if (!err || typeof err !== "object") return "";
const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
if (rateLimitDetails) return rateLimitDetails;
const status = err.status;
const discordCode = err.discordCode;
const rawBody = err.rawBody;
const requestBody = err.deployRequestBody;
const details = [];
if (typeof status === "number") details.push(`status=${status}`);
if (typeof discordCode === "number" || typeof discordCode === "string") details.push(`code=${discordCode}`);
if (rawBody !== void 0) {
let bodyText;
try {
bodyText = JSON.stringify(rawBody);
} catch {
bodyText = typeof rawBody === "string" ? rawBody : inspect(rawBody, {
depth: 3,
breakLength: 120
});
}
if (bodyText) {
const maxLen = 800;
const trimmed = bodyText.length > maxLen ? `${bodyText.slice(0, maxLen)}...` : bodyText;
details.push(`body=${trimmed}`);
}
}
const rejectedEntries = formatDiscordRejectedDeployEntries({
rawBody,
requestBody
});
if (rejectedEntries.length > 0) details.push(`rejected=${rejectedEntries.join("; ")}`);
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
function isDiscordDeployDailyCreateLimit(err) {
if (!err || typeof err !== "object") return false;
const deployErr = err;
const discordCode = readNonNegativeInteger(deployErr.discordCode);
const rawCode = readNonNegativeInteger(readDiscordDeployObjectField(deployErr.rawBody, "code"));
return (discordCode === 30034 || rawCode === 30034) && /daily application command creates/i.test(formatErrorMessage(err));
}
//#endregion
//#region extensions/discord/src/monitor/provider.startup-log.ts
function formatDiscordStartupGatewayState(gateway) {
if (!gateway) return "gateway=missing";
const reconnectAttempts = gateway.reconnectAttempts;
return `gatewayConnected=${gateway.isConnected ? "true" : "false"} reconnectAttempts=${typeof reconnectAttempts === "number" ? reconnectAttempts : "na"}`;
}
function logDiscordStartupPhase$1(params) {
if (!(params.isVerbose ?? isVerbose)()) return;
const elapsedMs = Math.max(0, Date.now() - params.startAt);
const suffix = [params.details, formatDiscordStartupGatewayState(params.gateway)].filter((value) => Boolean(value)).join(" ");
params.runtime.log?.(`discord startup [${params.accountId}] ${params.phase} ${elapsedMs}ms${suffix ? ` ${suffix}` : ""}`);
}
//#endregion
//#region extensions/discord/src/monitor/provider.deploy.ts
function readDeployRequestBody(data) {
return data && typeof data === "object" && "body" in data ? data.body : void 0;
}
function wrapDeployRestMethod(params) {
return async (path, data, query) => {
const startedAt = Date.now();
const body = readDeployRequestBody(data);
const commandCount = Array.isArray(body) ? body.length : void 0;
const bodyBytes = body === void 0 ? void 0 : Buffer.byteLength(typeof body === "string" ? body : JSON.stringify(body), "utf8");
if (params.shouldLogVerbose()) params.runtime.log?.(`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:start ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path}${typeof commandCount === "number" ? ` commands=${commandCount}` : ""}${typeof bodyBytes === "number" ? ` bytes=${bodyBytes}` : ""}`);
try {
const result = await params.original[params.method](path, data, query);
if (params.shouldLogVerbose()) params.runtime.log?.(`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:done ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}`);
return result;
} catch (err) {
const requestMs = Date.now() - startedAt;
attachDiscordDeployRequestBody(err, body);
attachDiscordDeployRestContext(err, {
method: params.method,
path,
requestMs,
timeoutMs: params.timeoutMs
});
const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
if (rateLimitDetails) {
if (params.shouldLogVerbose()) params.runtime.log?.(warn(`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs}${rateLimitDetails}`));
} else {
const details = formatDiscordDeployErrorDetails(err);
params.runtime.error?.(`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs} error=${formatDiscordDeployErrorMessage(err)}${details}`);
}
throw err;
}
};
}
function installDeployRestLogging(params) {
const original = {
get: params.rest.get.bind(params.rest),
post: params.rest.post.bind(params.rest),
put: params.rest.put.bind(params.rest),
patch: params.rest.patch.bind(params.rest),
delete: params.rest.delete.bind(params.rest)
};
for (const method of Object.keys(original)) {
const timeout = params.rest.options?.timeout;
params.rest[method] = wrapDeployRestMethod({
method,
original,
runtime: params.runtime,
accountId: params.accountId,
startupStartedAt: params.startupStartedAt,
timeoutMs: typeof timeout === "number" ? timeout : void 0,
shouldLogVerbose: params.shouldLogVerbose
});
}
return () => {
params.rest.get = original.get;
params.rest.post = original.post;
params.rest.put = original.put;
params.rest.patch = original.patch;
params.rest.delete = original.delete;
};
}
async function deployDiscordCommands(params) {
if (!params.enabled) return;
const startupStartedAt = params.startupStartedAt ?? Date.now();
const accountId = params.accountId ?? "default";
const restoreDeployRestLogging = installDeployRestLogging({
rest: params.client.rest,
runtime: params.runtime,
accountId,
startupStartedAt,
shouldLogVerbose: params.shouldLogVerbose
});
try {
try {
await params.client.deployCommands({ mode: "reconcile" });
} catch (err) {
if (isDiscordDeployDailyCreateLimit(err)) {
params.runtime.log?.(warn(`discord: native slash command deploy skipped for ${accountId}; daily application command create limit reached. Existing slash commands stay active until Discord resets the quota. Message send/receive is unaffected.`));
return;
}
const rateLimitWarning = formatDiscordDeployRateLimitWarning(err, accountId);
if (rateLimitWarning) {
params.runtime.log?.(warn(rateLimitWarning));
return;
}
throw err;
}
} catch (err) {
params.runtime.log?.(warn(`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`));
} finally {
restoreDeployRestLogging();
}
}
function runDiscordCommandDeployInBackground(params) {
if (!params.enabled) return;
logDiscordStartupPhase$1({
runtime: params.runtime,
accountId: params.accountId,
phase: "deploy-commands:scheduled",
startAt: params.startupStartedAt,
details: "mode=reconcile background=true",
isVerbose: params.isVerbose
});
deployDiscordCommands(params).then(() => {
logDiscordStartupPhase$1({
runtime: params.runtime,
accountId: params.accountId,
phase: "deploy-commands:done",
startAt: params.startupStartedAt,
details: "background=true",
isVerbose: params.isVerbose
});
}).catch((err) => {
params.runtime.log?.(warn(`discord: native slash command deploy background warning (not message send): ${formatErrorMessage(err)}`));
});
}
//#endregion
//#region extensions/discord/src/voice/command.ts
const VOICE_CHANNEL_TYPES = [ChannelType.GuildVoice, ChannelType.GuildStageVoice];
async function authorizeVoiceCommand(interaction, params, options) {
const channelOverride = options?.channelOverride;
const channel = channelOverride ? void 0 : interaction.channel;
if (!interaction.guild) return {
ok: false,
message: "Voice commands are only available in guilds."
};
const user = interaction.user;
if (!user) return {
ok: false,
message: "Unable to resolve command user."
};
const channelId = channelOverride?.id ?? channel?.id ?? "";
const channelContext = await resolveDiscordThreadLikeChannelContext({
client: interaction.client,
channel: channelOverride ?? channel,
channelIdFallback: channelId
});
const channelName = channelOverride?.name ?? channelContext.channelName;
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
const sender = resolveDiscordSenderIdentity({
author: user,
member: interaction.rawData.member
});
const access = await authorizeDiscordVoiceIngress({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
groupPolicy: params.groupPolicy,
useAccessGroups: params.useAccessGroups,
guild: interaction.guild,
guildId: interaction.guild.id,
channelId,
channelName,
channelSlug: channelContext.channelSlug,
parentId: channelOverride?.parentId ?? channelContext.threadParentId,
parentName: channelContext.threadParentName,
parentSlug: channelContext.threadParentSlug,
scope: channelContext.isThreadChannel ? "thread" : "channel",
channelLabel: channelId ? formatMention({ channelId }) : "This channel",
memberRoleIds,
ownerAllowFrom: resolveDiscordAccountAllowFrom({
cfg: params.cfg,
accountId: params.accountId
}),
sender: {
id: sender.id,
name: sender.name,
tag: sender.tag
}
});
if (!access.ok) return {
ok: false,
message: access.message
};
return {
ok: true,
guildId: interaction.guild.id
};
}
async function resolveVoiceCommandRuntimeContext(interaction, params) {
const guildId = interaction.guild?.id;
if (!guildId) {
await interaction.reply({
content: "Unable to resolve guild for this command.",
ephemeral: true
});
return null;
}
const manager = params.getManager();
if (!manager) {
await interaction.reply({
content: "Voice manager is not available yet.",
ephemeral: true
});
return null;
}
return {
guildId,
manager
};
}
async function ensureVoiceCommandAccess(params) {
const access = await authorizeVoiceCommand(params.interaction, params.context, { channelOverride: params.channelOverride });
if (access.ok) return true;
await params.interaction.reply({
content: access.message ?? "Not authorized.",
ephemeral: true
});
return false;
}
function createDiscordVoiceCommand(params) {
const resolveSessionChannelId = (manager, guildId) => manager.status().find((entry) => entry.guildId === guildId)?.channelId;
class JoinCommand extends Command {
constructor(..._args) {
super(..._args);
this.name = "join";
this.description = "Join a voice channel";
this.defer = true;
this.ephemeral = params.ephemeralDefault;
this.options = [{
name: "channel",
description: "Voice channel to join",
type: ApplicationCommandOptionType.Channel,
required: true,
channel_types: VOICE_CHANNEL_TYPES
}];
}
async run(interaction) {
const channel = await interaction.options.getChannel("channel", true);
if (!channel || !("id" in channel)) {
await interaction.reply({
content: "Voice channel not found.",
ephemeral: true
});
return;
}
const access = await authorizeVoiceCommand(interaction, params, { channelOverride: {
id: channel.id,
name: resolveDiscordChannelNameSafe(channel)
} });
if (!access.ok) {
await interaction.reply({
content: access.message ?? "Not authorized.",
ephemeral: true
});
return;
}
if (!isVoiceChannelType(channel.type)) {
await interaction.reply({
content: "That is not a voice channel.",
ephemeral: true
});
return;
}
const guildId = access.guildId ?? ("guildId" in channel ? channel.guildId : void 0);
if (!guildId) {
await interaction.reply({
content: "Unable to resolve guild for this voice channel.",
ephemeral: true
});
return;
}
const manager = params.getManager();
if (!manager) {
await interaction.reply({
content: "Voice manager is not available yet.",
ephemeral: true
});
return;
}
const result = await manager.join({
guildId,
channelId: channel.id
});
await interaction.reply({
content: result.message,
ephemeral: true
});
}
}
class LeaveCommand extends Command {
constructor(..._args2) {
super(..._args2);
this.name = "leave";
this.description = "Leave the current voice channel";
this.defer = true;
this.ephemeral = params.ephemeralDefault;
}
async run(interaction) {
const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
if (!runtimeContext) return;
const sessionChannelId = resolveSessionChannelId(runtimeContext.manager, runtimeContext.guildId);
if (!await ensureVoiceCommandAccess({
interaction,
context: params,
channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
})) return;
const result = await runtimeContext.manager.leave({ guildId: runtimeContext.guildId });
await interaction.reply({
content: result.message,
ephemeral: true
});
}
}
class StatusCommand extends Command {
constructor(..._args3) {
super(..._args3);
this.name = "status";
this.description = "Show active voice sessions";
this.defer = true;
this.ephemeral = params.ephemeralDefault;
}
async run(interaction) {
const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
if (!runtimeContext) return;
const sessions = runtimeContext.manager.status().filter((entry) => entry.guildId === runtimeContext.guildId);
const sessionChannelId = sessions[0]?.channelId;
if (!await ensureVoiceCommandAccess({
interaction,
context: params,
channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
})) return;
if (sessions.length === 0) {
await interaction.reply({
content: "No active voice sessions.",
ephemeral: true
});
return;
}
const lines = sessions.map((entry) => `• ${formatMention({ channelId: entry.channelId })} (guild ${entry.guildId})`);
await interaction.reply({
content: lines.join("\n"),
ephemeral: true
});
}
}
return new class extends CommandWithSubcommands {
constructor(..._args4) {
super(..._args4);
this.name = "vc";
this.description = "Voice channel controls";
this.subcommands = [
new JoinCommand(),
new LeaveCommand(),
new StatusCommand()
];
}
}();
}
function isVoiceChannelType(type) {
return type === ChannelType.GuildVoice || type === ChannelType.GuildStageVoice;
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-context.ts
function formatUsername(user) {
if (user.discriminator && user.discriminator !== "0") return `${user.username}#${user.discriminator}`;
return user.username;
}
function isThreadChannelType(channelType) {
return channelType === ChannelType.PublicThread || channelType === ChannelType.PrivateThread || channelType === ChannelType.AnnouncementThread;
}
function resolveAgentComponentRoute(params) {
return resolveAgentRoute({
cfg: params.ctx.cfg,
channel: "discord",
accountId: params.ctx.accountId,
guildId: params.rawGuildId,
memberRoleIds: params.memberRoleIds,
peer: {
kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
id: params.isDirectMessage ? params.userId : params.channelId
},
parentPeer: params.parentId ? {
kind: "channel",
id: params.parentId
} : void 0
});
}
async function ackComponentInteraction(params) {
try {
await params.interaction.reply({
content: "✓",
...params.replyOpts
});
} catch (err) {
logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`);
}
}
function resolveDiscordChannelContext(interaction) {
const channel = interaction.channel;
const channelInfo = resolveDiscordChannelInfoSafe(channel);
const channelName = channelInfo.name;
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const displayChannelSlug = channelName ? normalizeDiscordDisplaySlug(channelName) : "";
const channelType = channelInfo.type;
const isThread = isThreadChannelType(channelType);
let parentId;
let parentName;
let parentSlug = "";
if (isThread) {
parentId = channelInfo.parentId;
parentName = channelInfo.parentName;
if (parentName) parentSlug = normalizeDiscordSlug(parentName);
}
return {
channelName,
channelSlug,
displayChannelSlug,
channelType,
isThread,
parentId,
parentName,
parentSlug
};
}
async function resolveComponentInteractionContext(params) {
const { interaction, label } = params;
const channelId = interaction.rawData.channel_id;
if (!channelId) {
logError(`${label}: missing channel_id in interaction`);
return null;
}
const user = interaction.user;
if (!user) {
logError(`${label}: missing user in interaction`);
return null;
}
const shouldDefer = params.defer !== false && "defer" in interaction;
let didDefer = false;
if (shouldDefer) try {
await interaction.defer({ ephemeral: true });
didDefer = true;
} catch (err) {
logError(`${label}: failed to defer interaction: ${String(err)}`);
}
const replyOpts = didDefer ? {} : { ephemeral: true };
const username = formatUsername(user);
const userId = user.id;
const rawGuildId = interaction.rawData.guild_id;
const channelType = resolveDiscordChannelContext(interaction).channelType;
const isGroupDm = channelType === ChannelType.GroupDM;
return {
channelId,
user,
username,
userId,
replyOpts,
rawGuildId,
isDirectMessage: channelType === ChannelType.DM || !rawGuildId && !isGroupDm && channelType == null,
isGroupDm,
memberRoleIds: Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : []
};
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-reply.ts
async function replySilently(interaction, params) {
try {
await interaction.reply(params);
} catch {}
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-dm-auth.ts
async function ensureDmComponentAuthorized(params) {
const { ctx, interaction, user, componentLabel, replyOpts } = params;
const dmPolicy = ctx.dmPolicy ?? "pairing";
if (dmPolicy === "disabled") {
logVerbose(`agent ${componentLabel}: blocked (DM policy disabled)`);
await replySilently(interaction, {
content: "DM interactions are disabled.",
...replyOpts
});
return false;
}
const access = await resolveDiscordDmCommandAccess({
accountId: ctx.accountId,
dmPolicy,
configuredAllowFrom: ctx.allowFrom ?? [],
sender: {
id: user.id,
name: user.username,
tag: formatDiscordUserTag(user)
},
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
cfg: ctx.cfg,
token: ctx.token,
readStoreAllowFrom: async ({ accountId, dmPolicy: dmPolicyLocal }) => await readChannelIngressStoreAllowFromForDmPolicy({
provider: "discord",
accountId,
dmPolicy: dmPolicyLocal
}),
eventKind: "button"
});
if (access.senderAccess.decision === "allow") return true;
if (access.senderAccess.decision !== "pairing") {
logVerbose(`agent ${componentLabel}: blocked DM user ${user.id} (not in allowFrom)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts
});
return false;
}
if (!(await createChannelPairingChallengeIssuer({
channel: "discord",
upsertPairingRequest: async ({ id, meta }) => {
return await upsertChannelPairingRequest({
channel: "discord",
id,
accountId: ctx.accountId,
meta
});
}
})({
senderId: user.id,
senderIdLine: `Your Discord user id: ${user.id}`,
meta: {
tag: formatDiscordUserTag(user),
name: user.username
},
sendPairingReply: async (text) => {
await interaction.reply({
content: text,
...replyOpts
});
}
})).created) await replySilently(interaction, {
content: "Pairing already requested. Ask the bot owner to approve your code.",
...replyOpts
});
return false;
}
async function ensureGroupDmComponentAuthorized(params) {
const { ctx, interaction, channelId, componentLabel, replyOpts } = params;
if (!(ctx.discordConfig?.dm?.groupEnabled ?? false)) {
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (group DMs disabled)`);
await replySilently(interaction, {
content: "Group DM interactions are disabled.",
...replyOpts
});
return false;
}
const channelCtx = resolveDiscordChannelContext(interaction);
if (resolveGroupDmAllow({
channels: ctx.discordConfig?.dm?.groupChannels,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug
})) return true;
logVerbose(`agent ${componentLabel}: blocked group dm ${channelId} (not allowlisted)`);
await replySilently(interaction, {
content: `You are not authorized to use this ${componentLabel}.`,
...replyOpts
});
return false;
}
async function resolveInteractionContextWithDmAuth(params) {
const interactionCtx = await resolveComponentInteractionContext({
interaction: params.interaction,
label: params.label,
defer: params.defer
});
if (!interactionCtx) return null;
if (interactionCtx.isDirectMessage) {
if (!await ensureDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
user: interactionCtx.user,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts
})) return null;
}
if (interactionCtx.isGroupDm) {
if (!await ensureGroupDmComponentAuthorized({
ctx: params.ctx,
interaction: params.interaction,
channelId: interactionCtx.channelId,
componentLabel: params.componentLabel,
replyOpts: interactionCtx.replyOpts
})) return null;
}
return interactionCtx;
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-guild-auth.ts
function resolveComponentRuntimeGroupPolicy(ctx) {
return resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: ctx.cfg.channels?.discord !== void 0,
groupPolicy: ctx.discordConfig?.groupPolicy,
defaultGroupPolicy: ctx.cfg.channels?.defaults?.groupPolicy
}).groupPolicy;
}
async function ensureGuildComponentMemberAllowed(params) {
const { interaction, guildInfo, channelId, rawGuildId, channelCtx, memberRoleIds, user, replyOpts, componentLabel, unauthorizedReply } = params;
if (!rawGuildId) return true;
const replyUnauthorized = async () => {
await replySilently(interaction, {
content: unauthorizedReply,
...replyOpts
});
};
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel"
});
if (channelConfig?.enabled === false) {
await replyUnauthorized();
return false;
}
const channelAllowlistConfigured = Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
if (!isDiscordGroupAllowedByPolicy({
groupPolicy: params.groupPolicy,
guildAllowlisted: Boolean(guildInfo),
channelAllowlistConfigured,
channelAllowed
})) {
await replyUnauthorized();
return false;
}
if (channelConfig?.allowed === false) {
await replyUnauthorized();
return false;
}
const { memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds,
sender: {
id: user.id,
name: user.username,
tag: user.discriminator ? `${user.username}#${user.discriminator}` : void 0
},
allowNameMatching: params.allowNameMatching
});
if (memberAllowed) return true;
logVerbose(`agent ${componentLabel}: blocked user ${user.id} (not in users/roles allowlist)`);
await replyUnauthorized();
return false;
}
async function ensureComponentUserAllowed(params) {
const allowList = normalizeDiscordAllowList(params.entry.allowedUsers, [
"discord:",
"user:",
"pk:"
]);
if (!allowList) return true;
if (resolveDiscordAllowListMatch({
allowList,
candidate: {
id: params.user.id,
name: params.user.username,
tag: formatDiscordUserTag(params.user)
},
allowNameMatching: params.allowNameMatching
}).allowed) return true;
logVerbose(`discord component ${params.componentLabel}: blocked user ${params.user.id} (not in allowedUsers)`);
await replySilently(params.interaction, {
content: params.unauthorizedReply,
...params.replyOpts
});
return false;
}
async function ensureAgentComponentInteractionAllowed(params) {
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? void 0,
guildId: params.rawGuildId,
guildEntries: params.ctx.guildEntries
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
if (!await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId: params.channelId,
rawGuildId: params.rawGuildId,
channelCtx,
memberRoleIds: params.memberRoleIds,
user: params.user,
replyOpts: params.replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching: isDangerousNameMatchingEnabled(params.ctx.discordConfig),
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx)
})) return null;
return { parentId: channelCtx.parentId };
}
async function resolveAuthorizedComponentInteraction(params) {
const interactionCtx = await resolveInteractionContextWithDmAuth({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: params.componentLabel,
defer: params.defer
});
if (!interactionCtx) return null;
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? void 0,
guildId: rawGuildId,
guildEntries: params.ctx.guildEntries
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
const allowNameMatching = isDangerousNameMatchingEnabled(params.ctx.discordConfig);
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel"
});
if (!await ensureGuildComponentMemberAllowed({
interaction: params.interaction,
guildInfo,
channelId,
rawGuildId,
channelCtx,
memberRoleIds,
user,
replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply: params.unauthorizedReply,
allowNameMatching,
groupPolicy: resolveComponentRuntimeGroupPolicy(params.ctx)
})) return null;
return {
interactionCtx,
channelCtx,
guildInfo,
channelConfig,
allowNameMatching,
commandAuthorized: await resolveComponentCommandAuthorized({
ctx: params.ctx,
interactionCtx,
channelConfig,
guildInfo,
allowNameMatching
}),
user,
replyOpts
};
}
async function resolveComponentCommandAuthorized(params) {
const { ctx, interactionCtx, channelConfig, guildInfo } = params;
if (interactionCtx.isDirectMessage) return true;
const { ownerAllowList, ownerAllowed: ownerOk } = resolveDiscordOwnerAccess({
allowFrom: ctx.allowFrom,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user)
},
allowNameMatching: params.allowNameMatching
});
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
channelConfig,
guildInfo,
memberRoleIds: interactionCtx.memberRoleIds,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: formatDiscordUserTag(interactionCtx.user)
},
allowNameMatching: params.allowNameMatching
});
const useAccessGroups = ctx.cfg.commands?.useAccessGroups !== false;
return resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: useAccessGroups ? [{
configured: ownerAllowList != null,
allowed: ownerOk
}, {
configured: hasAccessRestrictions,
allowed: memberAllowed
}] : [{
configured: hasAccessRestrictions,
allowed: memberAllowed
}],
modeWhenAccessGroupsOff: "configured"
});
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-data.ts
function readParsedComponentId(data) {
if (!data || typeof data !== "object") return;
return "cid" in data ? data.cid : data.componentId;
}
function normalizeComponentId(value) {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed ? trimmed : void 0;
}
if (typeof value === "number" && Number.isFinite(value)) return String(value);
}
function mapOptionLabels(options, values) {
if (!options || options.length === 0) return values;
const map = new Map(options.map((option) => [option.value, option.label]));
return values.map((value) => map.get(value) ?? value);
}
function parseAgentComponentData(data) {
const raw = readParsedComponentId(data);
const decodeSafe = (value) => {
if (!value.includes("%")) return value;
if (!/%[0-9A-Fa-f]{2}/.test(value)) return value;
try {
return decodeURIComponent(value);
} catch {
return value;
}
};
const componentId = typeof raw === "string" ? decodeSafe(raw) : typeof raw === "number" ? String(raw) : null;
if (!componentId) return null;
return { componentId };
}
function parseDiscordComponentData(data, customId) {
if (!data || typeof data !== "object") return null;
const rawComponentId = readParsedComponentId(data);
const rawModalId = "mid" in data ? data.mid : data.modalId;
let componentId = normalizeComponentId(rawComponentId);
let modalId = normalizeComponentId(rawModalId);
if (!componentId && customId) {
const parsed = parseDiscordComponentCustomId(customId);
if (parsed) {
componentId = parsed.componentId;
modalId = parsed.modalId;
}
}
if (!componentId) return null;
return {
componentId,
modalId
};
}
function parseDiscordModalId(data, customId) {
if (data && typeof data === "object") {
const modalId = normalizeComponentId("mid" in data ? data.mid : data.modalId);
if (modalId) return modalId;
}
if (customId) return parseDiscordModalCustomId(customId);
return null;
}
function resolveInteractionCustomId(interaction) {
if (!interaction?.rawData || typeof interaction.rawData !== "object") return;
if (!("data" in interaction.rawData)) return;
const customId = interaction.rawData.data?.custom_id;
if (typeof customId !== "string") return;
const trimmed = customId.trim();
return trimmed ? trimmed : void 0;
}
function mapSelectValues(entry, values) {
if (entry.selectType === "string") return mapOptionLabels(entry.options, values);
if (entry.selectType === "user") return values.map((value) => `user:${value}`);
if (entry.selectType === "role") return values.map((value) => `role:${value}`);
if (entry.selectType === "mentionable") return values.map((value) => `mentionable:${value}`);
if (entry.selectType === "channel") return values.map((value) => `channel:${value}`);
return values;
}
function resolveModalFieldValues(field, interaction) {
const fields = interaction.fields;
const optionLabels = field.options?.map((option) => ({
value: option.value,
label: option.label
}));
const required = field.required === true;
try {
switch (field.type) {
case "text": {
const value = required ? fields.getText(field.id, true) : fields.getText(field.id);
return value ? [value] : [];
}
case "select":
case "checkbox":
case "radio": return mapOptionLabels(optionLabels, required ? fields.getStringSelect(field.id, true) : fields.getStringSelect(field.id) ?? []);
case "role-select": try {
return (required ? fields.getRoleSelect(field.id, true) : fields.getRoleSelect(field.id) ?? []).map((role) => role.name ?? role.id);
} catch {
return required ? fields.getStringSelect(field.id, true) : fields.getStringSelect(field.id) ?? [];
}
case "user-select": return (required ? fields.getUserSelect(field.id, true) : fields.getUserSelect(field.id) ?? []).map((user) => formatDiscordUserTag(user));
default: return [];
}
} catch (err) {
logError(`agent modal: failed to read field ${field.id}: ${String(err)}`);
return [];
}
}
function formatModalSubmissionText(entry, interaction) {
const lines = [`Form "${entry.title}" submitted.`];
for (const field of entry.fields) {
const values = resolveModalFieldValues(field, interaction);
if (values.length === 0) continue;
lines.push(`- ${field.label}: ${values.join(", ")}`);
}
if (lines.length === 1) lines.push("- (no values)");
return lines.join("\n");
}
function resolveDiscordInteractionId(interaction) {
const rawId = interaction.rawData && typeof interaction.rawData === "object" && "id" in interaction.rawData ? interaction.rawData.id : void 0;
if (typeof rawId === "string" && rawId.trim()) return rawId.trim();
if (typeof rawId === "number" && Number.isFinite(rawId)) return String(rawId);
return `discord-interaction:${Date.now()}`;
}
//#endregion
//#region extensions/discord/src/monitor/agent-components-helpers.ts
const AGENT_BUTTON_KEY = "agent";
const AGENT_SELECT_KEY = "agentsel";
//#endregion
//#region extensions/discord/src/monitor/agent-components.dispatch.ts
let conversationRuntimePromise$1;
let typingRuntimePromise;
async function loadConversationRuntime$1() {
conversationRuntimePromise$1 ??= import("./agent-components.runtime.js");
return await conversationRuntimePromise$1;
}
async function loadTypingRuntime() {
typingRuntimePromise ??= import("./typing-k7wKaS2G.js");
return await typingRuntimePromise;
}
function buildDiscordComponentConversationLabel(params) {
if (params.interactionCtx.isDirectMessage) return buildDirectLabel(params.interactionCtx.user);
if (params.interactionCtx.isGroupDm) return `Group DM #${params.channelCtx.channelName ?? params.interactionCtx.channelId} channel id:${params.interactionCtx.channelId}`;
return buildGuildLabel({
guild: params.interaction.guild ?? void 0,
channelName: params.channelCtx.channelName ?? params.interactionCtx.channelId,
channelId: params.interactionCtx.channelId
});
}
function resolveDiscordComponentChatType(interactionCtx) {
if (interactionCtx.isDirectMessage) return "direct";
if (interactionCtx.isGroupDm) return "group";
return "channel";
}
function resolveDiscordComponentOriginatingTo(interactionCtx) {
return resolveDiscordConversationIdentity({
isDirectMessage: interactionCtx.isDirectMessage,
userId: interactionCtx.userId,
channelId: interactionCtx.channelId
});
}
async function dispatchDiscordComponentEvent(params) {
const { ctx, interaction, interactionCtx, channelCtx, guildInfo, eventText } = params;
const runtime = ctx.runtime ?? createNonExitingRuntime();
const route = resolveAgentComponentRoute({
ctx,
rawGuildId: interactionCtx.rawGuildId,
memberRoleIds: interactionCtx.memberRoleIds,
isDirectMessage: interactionCtx.isDirectMessage,
isGroupDm: interactionCtx.isGroupDm,
userId: interactionCtx.userId,
channelId: interactionCtx.channelId,
parentId: channelCtx.parentId
});
const sessionKey = params.routeOverrides?.sessionKey ?? route.sessionKey;
const agentId = params.routeOverrides?.agentId ?? route.agentId;
const accountId = params.routeOverrides?.accountId ?? route.accountId;
const inboundLastRouteSessionKey = sessionKey;
const fromLabel = buildDiscordComponentConversationLabel({
interactionCtx,
interaction,
channelCtx
});
const chatType = resolveDiscordComponentChatType(interactionCtx);
const senderName = interactionCtx.user.globalName ?? interactionCtx.user.username;
const senderUsername = interactionCtx.user.username;
const senderTag = formatDiscordUserTag(interactionCtx.user);
const groupChannel = !interactionCtx.isDirectMessage && channelCtx.displayChannelSlug ? `#${channelCtx.displayChannelSlug}` : void 0;
const groupSubject = interactionCtx.isDirectMessage ? void 0 : groupChannel;
const channelConfig = resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: interactionCtx.channelId,
channelName: channelCtx.channelName,
channelSlug: channelCtx.channelSlug,
parentId: channelCtx.parentId,
parentName: channelCtx.parentName,
parentSlug: channelCtx.parentSlug,
scope: channelCtx.isThread ? "thread" : "channel"
});
const allowNameMatching = isDangerousNameMatchingEnabled(ctx.discordConfig);
const { ownerAllowFrom } = buildDiscordInboundAccessContext({
channelConfig,
guildInfo,
sender: {
id: interactionCtx.user.id,
name: interactionCtx.user.username,
tag: senderTag
},
allowNameMatching,
isGuild: !interactionCtx.isDirectMessage
});
const groupSystemPrompt = buildDiscordGroupSystemPrompt(channelConfig);
const pinnedMainDmOwner = interactionCtx.isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({
dmScope: ctx.cfg.session?.dmScope,
allowFrom: channelConfig?.users ?? guildInfo?.users,
normalizeEntry: (entry) => {
const candidate = normalizeDiscordAllowList([entry], [
"discord:",
"user:",
"pk:"
])?.ids.values().next().value;
return typeof candidate === "string" && /^\d+$/.test(candidate) ? candidate : void 0;
}
}) : null;
const commandAuthorized = await resolveComponentCommandAuthorized({
ctx,
interactionCtx,
channelConfig,
guildInfo,
allowNameMatching
});
const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId });
const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
const previousTimestamp = readSessionUpdatedAt({
storePath,
sessionKey
});
const timestamp = Date.now();
const combinedBody = formatInboundEnvelope({
channel: "Discord",
from: fromLabel,
timestamp,
body: eventText,
chatType,
senderLabel: senderName,
previousTimestamp,
envelope: envelopeOptions
});
const { createReplyReferencePlanner, dispatchReplyWithBufferedBlockDispatcher, finalizeInboundContext, resolveChunkMode, resolveTextChunkLimit, recordInboundSession } = await (async () => {
return { ...await loadConversationRuntime$1() };
})();
const ctxPayload = finalizeInboundContext({
Body: combinedBody,
BodyForAgent: eventText,
RawBody: eventText,
CommandBody: eventText,
From: interactionCtx.isDirectMessage ? `discord:${interactionCtx.userId}` : interactionCtx.isGroupDm ? `discord:group:${interactionCtx.channelId}` : `discord:channel:${interactionCtx.channelId}`,
To: `channel:${interactionCtx.channelId}`,
SessionKey: sessionKey,
AccountId: accountId,
ChatType: chatType,
ConversationLabel: fromLabel,
SenderName: senderName,
SenderId: interactionCtx.userId,
SenderUsername: senderUsername,
SenderTag: senderTag,
GroupSubject: groupSubject,
GroupChannel: groupChannel,
MemberRoleIds: interactionCtx.memberRoleIds,
GroupSystemPrompt: interactionCtx.isDirectMessage ? void 0 : groupSystemPrompt,
GroupSpace: guildInfo?.id ?? guildInfo?.slug ?? interactionCtx.rawGuildId ?? void 0,
OwnerAllowFrom: ownerAllowFrom,
Provider: "discord",
Surface: "discord",
WasMentioned: true,
CommandAuthorized: commandAuthorized,
CommandTurn: {
kind: "text-slash",
source: "text",
authorized: commandAuthorized,
body: eventText
},
CommandSource: "text",
MessageSid: interaction.rawData.id,
Timestamp: timestamp,
OriginatingChannel: "discord",
OriginatingTo: resolveDiscordComponentOriginatingTo(interactionCtx) ?? `channel:${interactionCtx.channelId}`
});
const deliverTarget = `channel:${interactionCtx.channelId}`;
const typingChannelId = interactionCtx.channelId;
const tableMode = resolveMarkdownTableMode({
cfg: ctx.cfg,
channel: "discord",
accountId
});
const textLimit = resolveTextChunkLimit(ctx.cfg, "discord", accountId, { fallbackLimit: 2e3 });
const token = ctx.token ?? "";
const feedbackRest = createDiscordRestClient({
cfg: ctx.cfg,
token,
accountId
}).rest;
const mediaLocalRoots = getAgentScopedMediaLocalRoots(ctx.cfg, agentId);
const replyToMode = ctx.discordConfig?.replyToMode ?? ctx.cfg.channels?.discord?.replyToMode ?? "off";
const replyReference = createReplyReferencePlanner({
replyToMode,
startId: params.replyToId
});
await runChannelInboundEvent({
channel: "discord",
accountId,
raw: interaction,
adapter: {
ingest: () => ({
id: interaction.id,
rawText: ctxPayload.RawBody ?? "",
textForAgent: ctxPayload.BodyForAgent,
textForCommands: ctxPayload.CommandBody,
raw: interaction
}),
resolveTurn: () => ({
cfg: ctx.cfg,
channel: "discord",
accountId,
agentId,
routeSessionKey: sessionKey,
storePath,
ctxPayload,
recordInboundSession,
dispatchReplyWithBufferedBlockDispatcher,
record: {
updateLastRoute: interactionCtx.isDirectMessage ? {
sessionKey: inboundLastRouteSessionKey,
channel: "discord",
to: resolveDiscordComponentOriginatingTo(interactionCtx) ?? `user:${interactionCtx.userId}`,
accountId,
mainDmOwnerPin: inboundLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner ? {
ownerRecipient: pinnedMainDmOwner,
senderRecipient: interactionCtx.userId,
onSkip: ({ ownerRecipient, senderRecipient }) => {
logVerbose(`discord: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
}
} : void 0
} : void 0,
onRecordError: (err) => {
logVerbose(`discord: failed updating component session meta: ${String(err)}`);
}
},
delivery: {
deliver: async (payload, info) => {
const replyToId = replyReference.use();
await deliverDiscordReply({
cfg: ctx.cfg,
replies: [payload],
target: deliverTarget,
token,
accountId,
rest: interaction.client.rest,
runtime,
replyToId,
replyToMode,
textLimit,
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
cfg: ctx.cfg,
discordConfig: ctx.discordConfig,
accountId
}),
tableMode,
chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
mediaLocalRoots,
kind: info.kind
});
replyReference.markSent();
},
onError: (err) => {
logError(`discord component dispatch failed: ${String(err)}`);
}
},
replyPipeline: {},
dispatcherOptions: {
humanDelay: resolveHumanDelayConfig(ctx.cfg, agentId),
onReplyStart: async () => {
try {
const { sendTyping } = await loadTypingRuntime();
await sendTyping({
rest: feedbackRest,
channelId: typingChannelId
});
} catch (err) {
logVerbose(`discord: typing failed for component reply: ${String(err)}`);
}
}
}
})
}
});
}
//#endregion
//#region extensions/discord/src/interactive-dispatch.ts
async function dispatchDiscordPluginInteractiveHandler(params) {
return await dispatchPluginInteractiveHandler({
channel: "discord",
data: params.data,
dedupeId: params.interactionId,
onMatched: params.onMatched,
invoke: ({ registration, namespace, payload }) => registration.handler({
...params.ctx,
channel: "discord",
interaction: {
...params.ctx.interaction,
data: params.data,
namespace,
payload
},
respond: params.respond,
...createInteractiveConversationBindingHelpers({
registration,
senderId: params.ctx.senderId,
conversation: {
channel: "discord",
accountId: params.ctx.accountId,
conversationId: params.ctx.conversationId,
parentConversationId: params.ctx.parentConversationId
}
})
})
});
}
//#endregion
//#region extensions/discord/src/monitor/agent-components.plugin-interactive.ts
let conversationRuntimePromise;
async function loadConversationRuntime() {
conversationRuntimePromise ??= import("./agent-components.runtime.js");
return await conversationRuntimePromise;
}
async function dispatchPluginDiscordInteractiveEvent(params) {
const normalizedConversationId = params.interactionCtx.rawGuildId || params.channelCtx.channelType === ChannelType.GroupDM ? `channel:${params.interactionCtx.channelId}` : `user:${params.interactionCtx.userId}`;
let responded = false;
let acknowledged = false;
const updateOriginalMessage = async (input) => {
const payload = {
...input.text !== void 0 ? { content: input.text } : {},
...input.components !== void 0 ? { components: input.components } : {}
};
if (acknowledged) {
await params.interaction.reply(payload);
return;
}
if (!("update" in params.interaction) || typeof params.interaction.update !== "function") throw new Error("Discord interaction cannot update the source message");
await params.interaction.update(payload);
};
const respond = {
acknowledge: async () => {
if (responded) return;
await params.interaction.acknowledge();
acknowledged = true;
responded = true;
},
reply: async ({ text, ephemeral = true }) => {
responded = true;
await params.interaction.reply({
content: text,
ephemeral
});
},
followUp: async ({ text, ephemeral = true }) => {
responded = true;
await params.interaction.followUp({
content: text,
ephemeral
});
},
editMessage: async (input) => {
const { text, components } = input;
responded = true;
await updateOriginalMessage({
text,
components
});
},
clearComponents: async (input) => {
responded = true;
await updateOriginalMessage({
text: input?.text,
components: []
});
}
};
const conversationRuntime = await loadConversationRuntime();
const pluginBindingApproval = conversationRuntime.parsePluginBindingApprovalCustomId(params.data);
if (pluginBindingApproval) {
const { buildPluginBindingResolvedText, resolvePluginConversationBindingApproval } = conversationRuntime;
try {
await respond.acknowledge();
} catch {}
const resolved = await resolvePluginConversationBindingApproval({
approvalId: pluginBindingApproval.approvalId,
decision: pluginBindingApproval.decision,
senderId: params.interactionCtx.userId
});
const approvalMessageId = params.messageId?.trim() || params.interaction.message?.id?.trim();
if (approvalMessageId) try {
await editDiscordComponentMessage(normalizedConversationId, approvalMessageId, { text: buildPluginBindingResolvedText(resolved) }, {
cfg: params.ctx.cfg,
accountId: params.ctx.accountId
});
} catch (err) {
logError(`discord plugin binding approval: failed to clear prompt: ${String(err)}`);
}
if (resolved.status !== "approved") try {
await respond.followUp({
text: buildPluginBindingResolvedText(resolved),
ephemeral: true
});
} catch (err) {
logError(`discord plugin binding approval: failed to follow up: ${String(err)}`);
}
return "handled";
}
const dispatched = await dispatchDiscordPluginInteractiveHandler({
data: params.data,
interactionId: resolveDiscordInteractionId(params.interaction),
ctx: {
accountId: params.ctx.accountId,
interactionId: resolveDiscordInteractionId(params.interaction),
conversationId: normalizedConversationId,
parentConversationId: params.channelCtx.parentId,
guildId: params.interactionCtx.rawGuildId,
senderId: params.interactionCtx.userId,
senderUsername: params.interactionCtx.username,
auth: { isAuthorizedSender: params.isAuthorizedSender },
interaction: {
kind: params.kind,
messageId: params.messageId,
values: params.values,
fields: params.fields
}
},
respond,
onMatched: async () => {
try {
await respond.acknowledge();
} catch {}
}
});
if (!dispatched.matched) return "unmatched";
if (dispatched.handled) {
if (!responded) try {
await respond.acknowledge();
} catch {}
return "handled";
}
return "unmatched";
}
//#endregion
//#region extensions/discord/src/monitor/agent-components.handlers.ts
let componentsRuntimePromise;
async function loadComponentsRuntime() {
componentsRuntimePromise ??= import("./components-B6JeZNcv.js");
return await componentsRuntimePromise;
}
async function handleDiscordComponentEvent(params) {
const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
if (!parsed) {
logError(`${params.label}: failed to parse component data`);
try {
await params.interaction.reply({
content: "This component is no longer valid.",
ephemeral: true
});
} catch {}
return;
}
const entry = await resolveDiscordComponentEntryWithPersistence({
id: parsed.componentId,
consume: false
});
if (!entry) {
try {
await params.interaction.reply({
content: "This component has expired.",
ephemeral: true
});
} catch {}
return;
}
const unauthorizedReply = `You are not authorized to use this ${params.componentLabel}.`;
const authorized = await resolveAuthorizedComponentInteraction({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: params.componentLabel,
unauthorizedReply,
defer: false
});
if (!authorized) return;
const { interactionCtx, channelCtx, guildInfo, allowNameMatching, commandAuthorized, user, replyOpts } = authorized;
if (!await ensureComponentUserAllowed({
entry,
interaction: params.interaction,
user,
replyOpts,
componentLabel: params.componentLabel,
unauthorizedReply,
allowNameMatching
})) return;
const consumed = await resolveDiscordComponentEntryWithPersistence({
id: parsed.componentId,
consume: !entry.reusable
});
if (!consumed) {
try {
await params.interaction.reply({
content: "This component has expired.",
ephemeral: true
});
} catch {}
return;
}
if (consumed.kind === "modal-trigger") {
try {
await params.interaction.reply({
content: "This form is no longer available.",
ephemeral: true
});
} catch {}
return;
}
const values = params.values ? mapSelectValues(consumed, params.values) : void 0;
const selectedCallbackData = consumed.kind === "select" && consumed.callbackDataKind === "callback" && params.values?.length === 1 ? params.values[0]?.trim() : void 0;
const pluginCallbackData = consumed.callbackData ?? selectedCallbackData;
if (pluginCallbackData) {
if (await dispatchPluginDiscordInteractiveEvent({
ctx: params.ctx,
interaction: params.interaction,
interactionCtx,
channelCtx,
isAuthorizedSender: commandAuthorized,
data: pluginCallbackData,
kind: consumed.kind === "select" ? "select" : "button",
values,
messageId: consumed.messageId ?? params.interaction.message?.id
}) === "handled") return;
}
const buttonCallbackFallback = consumed.kind === "button" && consumed.callbackDataKind !== "callback" ? consumed.callbackData?.trim() : void 0;
const selectedCommandFallback = consumed.kind === "select" && consumed.callbackDataKind === "command" && params.values?.length === 1 ? params.values[0]?.trim() : void 0;
const eventText = buttonCallbackFallback || selectedCommandFallback || (await loadComponentsRuntime()).formatDiscordComponentEventText({
kind: consumed.kind === "select" ? "select" : "button",
label: consumed.label,
values
});
try {
await params.interaction.reply({
content: "✓",
...replyOpts
});
} catch (err) {
logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`);
}
await dispatchDiscordComponentEvent({
ctx: params.ctx,
interaction: params.interaction,
interactionCtx,
channelCtx,
guildInfo,
eventText,
replyToId: consumed.messageId ?? params.interaction.message?.id,
routeOverrides: {
sessionKey: consumed.sessionKey,
agentId: consumed.agentId,
accountId: consumed.accountId
}
});
}
async function handleDiscordModalTrigger(params) {
const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
if (!parsed) {
logError(`${params.label}: failed to parse modal trigger data`);
try {
await params.interaction.reply({
content: "This button is no longer valid.",
ephemeral: true
});
} catch {}
return;
}
const entry = await resolveDiscordComponentEntryWithPersistence({
id: parsed.componentId,
consume: false
});
if (!entry || entry.kind !== "modal-trigger") {
try {
await params.interaction.reply({
content: "This button has expired.",
ephemeral: true
});
} catch {}
return;
}
const modalId = entry.modalId ?? parsed.modalId;
if (!modalId) {
try {
await params.interaction.reply({
content: "This form is no longer available.",
ephemeral: true
});
} catch {}
return;
}
const unauthorizedReply = "You are not authorized to use this form.";
const authorized = await resolveAuthorizedComponentInteraction({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: "form",
unauthorizedReply,
defer: false
});
if (!authorized) return;
const { user, replyOpts, allowNameMatching } = authorized;
if (!await ensureComponentUserAllowed({
entry,
interaction: params.interaction,
user,
replyOpts,
componentLabel: "form",
unauthorizedReply,
allowNameMatching
})) return;
const consumed = await resolveDiscordComponentEntryWithPersistence({
id: parsed.componentId,
consume: !entry.reusable
});
if (!consumed) {
try {
await params.interaction.reply({
content: "This form has expired.",
ephemeral: true
});
} catch {}
return;
}
const modalEntry = await resolveDiscordModalEntryWithPersistence({
id: consumed.modalId ?? modalId,
consume: false
});
if (!modalEntry) {
try {
await params.interaction.reply({
content: "This form has expired.",
ephemeral: true
});
} catch {}
return;
}
try {
await params.interaction.showModal((await loadComponentsRuntime()).createDiscordFormModal(modalEntry));
} catch (err) {
logError(`${params.label}: failed to show modal: ${String(err)}`);
}
}
const discordComponentControlHandlers = {
handleComponentEvent: handleDiscordComponentEvent,
handleModalTrigger: handleDiscordModalTrigger
};
//#endregion
//#region extensions/discord/src/monitor/agent-components.modal.ts
var DiscordComponentModal = class extends Modal {
constructor(ctx) {
super();
this.title = "OpenClaw form";
this.customId = "__openclaw_discord_component_modal_wildcard__";
this.components = [];
this.customIdParser = parseDiscordModalCustomIdForInteraction;
this.ctx = ctx;
}
async run(interaction, data) {
const modalId = parseDiscordModalId(data, resolveInteractionCustomId(interaction));
if (!modalId) {
logError("discord component modal: missing modal id");
try {
await interaction.reply({
content: "This form is no longer valid.",
ephemeral: true
});
} catch {}
return;
}
const modalEntry = await resolveDiscordModalEntryWithPersistence({
id: modalId,
consume: false
});
if (!modalEntry) {
try {
await interaction.reply({
content: "This form has expired.",
ephemeral: true
});
} catch {}
return;
}
const unauthorizedReply = "You are not authorized to use this form.";
const authorized = await resolveAuthorizedComponentInteraction({
ctx: this.ctx,
interaction,
label: "discord component modal",
componentLabel: "form",
unauthorizedReply,
defer: false
});
if (!authorized) return;
const { interactionCtx, channelCtx, guildInfo, allowNameMatching, commandAuthorized, user, replyOpts } = authorized;
if (!await ensureComponentUserAllowed({
entry: {
id: modalEntry.id,
kind: "button",
label: modalEntry.title,
allowedUsers: modalEntry.allowedUsers
},
interaction,
user,
replyOpts,
componentLabel: "form",
unauthorizedReply,
allowNameMatching
})) return;
const consumed = await resolveDiscordModalEntryWithPersistence({
id: modalId,
consume: !modalEntry.reusable
});
if (!consumed) {
try {
await interaction.reply({
content: "This form has expired.",
ephemeral: true
});
} catch {}
return;
}
if (consumed.callbackData) {
const fields = consumed.fields.map((field) => ({
id: field.id,
name: field.name,
values: resolveModalFieldValues(field, interaction)
}));
if (await dispatchPluginDiscordInteractiveEvent({
ctx: this.ctx,
interaction,
interactionCtx,
channelCtx,
isAuthorizedSender: commandAuthorized,
data: consumed.callbackData,
kind: "modal",
fields,
messageId: consumed.messageId
}) === "handled") return;
}
try {
await interaction.acknowledge();
} catch (err) {
logError(`discord component modal: failed to acknowledge: ${String(err)}`);
}
const eventText = formatModalSubmissionText(consumed, interaction);
await dispatchDiscordComponentEvent({
ctx: this.ctx,
interaction,
interactionCtx,
channelCtx,
guildInfo,
eventText,
replyToId: consumed.messageId,
routeOverrides: {
sessionKey: consumed.sessionKey,
agentId: consumed.agentId,
accountId: consumed.accountId
}
});
}
};
//#endregion
//#region extensions/discord/src/monitor/agent-components.system-controls.ts
async function runAgentSystemControlInteraction(params) {
const parsed = parseAgentComponentData(params.data);
if (!parsed) {
logError(`${params.label}: failed to parse component data`);
try {
await params.interaction.reply({
content: params.invalidReply,
ephemeral: true
});
} catch {}
return;
}
const { componentId } = parsed;
const interactionCtx = await resolveInteractionContextWithDmAuth({
ctx: params.ctx,
interaction: params.interaction,
label: params.label,
componentLabel: params.interactionComponentLabel,
defer: false
});
if (!interactionCtx) return;
const { channelId, user, username, userId, replyOpts, rawGuildId, isDirectMessage, isGroupDm, memberRoleIds } = interactionCtx;
const allowed = await ensureAgentComponentInteractionAllowed({
ctx: params.ctx,
interaction: params.interaction,
channelId,
rawGuildId,
memberRoleIds,
user,
replyOpts,
componentLabel: params.authorizationComponentLabel,
unauthorizedReply: params.unauthorizedReply
});
if (!allowed) return;
const route = resolveAgentComponentRoute({
ctx: params.ctx,
rawGuildId,
memberRoleIds,
isDirectMessage,
isGroupDm,
userId,
channelId,
parentId: allowed.parentId
});
const eventText = params.formatEventText({
componentId,
username,
userId
});
logDebug(`${params.label}: enqueuing event for channel ${channelId}: ${eventText}`);
enqueueSystemEvent(eventText, {
sessionKey: route.sessionKey,
contextKey: `${params.contextKeyPrefix}:${channelId}:${componentId}:${userId}`
});
await ackComponentInteraction({
interaction: params.interaction,
replyOpts,
label: params.label
});
}
var AgentComponentButton = class extends Button {
constructor(ctx) {
super();
this.label = AGENT_BUTTON_KEY;
this.customId = `${AGENT_BUTTON_KEY}:seed=1`;
this.style = ButtonStyle.Primary;
this.ctx = ctx;
}
async run(interaction, data) {
await runAgentSystemControlInteraction({
ctx: this.ctx,
interaction,
data,
label: "agent button",
interactionComponentLabel: "button",
authorizationComponentLabel: "button",
invalidReply: "This button is no longer valid.",
unauthorizedReply: "You are not authorized to use this button.",
contextKeyPrefix: "discord:agent-button",
formatEventText: ({ componentId, username, userId }) => `[Discord component: ${componentId} clicked by ${username} (${userId})]`
});
}
};
var AgentSelectMenu = class extends StringSelectMenu {
constructor(ctx) {
super();
this.customId = `${AGENT_SELECT_KEY}:seed=1`;
this.options = [];
this.ctx = ctx;
}
async run(interaction, data) {
const values = interaction.values ?? [];
const valuesText = values.length > 0 ? ` (selected: ${values.join(", ")})` : "";
await runAgentSystemControlInteraction({
ctx: this.ctx,
interaction,
data,
label: "agent select",
interactionComponentLabel: "select menu",
authorizationComponentLabel: "select",
invalidReply: "This select menu is no longer valid.",
unauthorizedReply: "You are not authorized to use this select menu.",
contextKeyPrefix: "discord:agent-select",
formatEventText: ({ componentId, username, userId }) => `[Discord select menu: ${componentId} interacted by ${username} (${userId})${valuesText}]`
});
}
};
function createAgentComponentButton(ctx) {
return new AgentComponentButton(ctx);
}
function createAgentSelectMenu(ctx) {
return new AgentSelectMenu(ctx);
}
//#endregion
//#region extensions/discord/src/monitor/agent-components.wildcard-controls.ts
const SELECT_CONTROLS = {
string: {
type: ComponentType.StringSelect,
customId: "__openclaw_discord_component_string_select_wildcard__",
componentLabel: "select menu",
label: "discord component select"
},
user: {
type: ComponentType.UserSelect,
customId: "__openclaw_discord_component_user_select_wildcard__",
componentLabel: "user select",
label: "discord component user select"
},
role: {
type: ComponentType.RoleSelect,
customId: "__openclaw_discord_component_role_select_wildcard__",
componentLabel: "role select",
label: "discord component role select"
},
mentionable: {
type: ComponentType.MentionableSelect,
customId: "__openclaw_discord_component_mentionable_select_wildcard__",
componentLabel: "mentionable select",
label: "discord component mentionable select"
},
channel: {
type: ComponentType.ChannelSelect,
customId: "__openclaw_discord_component_channel_select_wildcard__",
componentLabel: "channel select",
label: "discord component channel select"
}
};
var DiscordComponentSelectControl = class extends BaseMessageInteractiveComponent {
constructor(spec, ctx, handlers) {
super();
this.spec = spec;
this.ctx = ctx;
this.handlers = handlers;
this.customIdParser = parseDiscordComponentCustomIdForInteraction;
this.type = spec.type;
this.customId = spec.customId;
}
serialize() {
return this.type === ComponentType.StringSelect ? {
type: this.type,
custom_id: this.customId,
options: []
} : {
type: this.type,
custom_id: this.customId
};
}
async run(interaction, data) {
await this.handlers.handleComponentEvent({
ctx: this.ctx,
interaction,
data,
componentLabel: this.spec.componentLabel,
label: this.spec.label,
values: interaction.values ?? []
});
}
};
var DiscordComponentButton = class extends Button {
constructor(ctx, handlers) {
super();
this.ctx = ctx;
this.handlers = handlers;
this.label = "component";
this.customId = "__openclaw_discord_component_button_wildcard__";
this.style = ButtonStyle.Primary;
this.customIdParser = parseDiscordComponentCustomIdForInteraction;
}
async run(interaction, data) {
if (parseDiscordComponentData(data, resolveInteractionCustomId(interaction))?.modalId) {
await this.handlers.handleModalTrigger({
ctx: this.ctx,
interaction,
data,
label: "discord component modal"
});
return;
}
await this.handlers.handleComponentEvent({
ctx: this.ctx,
interaction,
data,
componentLabel: "button",
label: "discord component button"
});
}
};
function createSelectControl(spec, ctx, handlers) {
return new DiscordComponentSelectControl(spec, ctx, handlers);
}
function bindSelectControl(spec) {
return (ctx, handlers) => createSelectControl(spec, ctx, handlers);
}
function createDiscordComponentButtonControl(ctx, handlers) {
return new DiscordComponentButton(ctx, handlers);
}
const createDiscordComponentStringSelectControl = bindSelectControl(SELECT_CONTROLS.string);
const createDiscordComponentUserSelectControl = bindSelectControl(SELECT_CONTROLS.user);
const createDiscordComponentRoleSelectControl = bindSelectControl(SELECT_CONTROLS.role);
const createDiscordComponentMentionableSelectControl = bindSelectControl(SELECT_CONTROLS.mentionable);
const createDiscordComponentChannelSelectControl = bindSelectControl(SELECT_CONTROLS.channel);
//#endregion
//#region extensions/discord/src/monitor/agent-components.ts
function bindDiscordComponentControl(createControl) {
return (ctx) => createControl(ctx, discordComponentControlHandlers);
}
const createDiscordComponentButton = bindDiscordComponentControl(createDiscordComponentButtonControl);
const createDiscordComponentStringSelect = bindDiscordComponentControl(createDiscordComponentStringSelectControl);
const createDiscordComponentUserSelect = bindDiscordComponentControl(createDiscordComponentUserSelectControl);
const createDiscordComponentRoleSelect = bindDiscordComponentControl(createDiscordComponentRoleSelectControl);
const createDiscordComponentMentionableSelect = bindDiscordComponentControl(createDiscordComponentMentionableSelectControl);
const createDiscordComponentChannelSelect = bindDiscordComponentControl(createDiscordComponentChannelSelectControl);
const createAgentComponentControls = [createAgentComponentButton, createAgentSelectMenu];
const createDiscordComponentControls = [
createDiscordComponentButton,
createDiscordComponentStringSelect,
createDiscordComponentUserSelect,
createDiscordComponentRoleSelect,
createDiscordComponentMentionableSelect,
createDiscordComponentChannelSelect
];
function createDiscordComponentModal(ctx) {
return new DiscordComponentModal(ctx);
}
//#endregion
//#region extensions/discord/src/monitor/exec-approvals.ts
function decodeCustomIdValue(value) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function parseExecApprovalData(data) {
if (!data || typeof data !== "object") return null;
const coerce = (value) => typeof value === "string" || typeof value === "number" ? String(value) : "";
const rawId = coerce(data.id);
const rawAction = coerce(data.action);
if (!rawId || !rawAction) return null;
const action = rawAction;
if (action !== "allow-once" && action !== "allow-always" && action !== "deny") return null;
return {
approvalId: decodeCustomIdValue(rawId),
action
};
}
function isStructuredApprovalNotFoundError(err) {
if (!err || typeof err !== "object") return false;
const record = err;
if (record.gatewayCode === "APPROVAL_NOT_FOUND") return true;
return record.gatewayCode === "INVALID_REQUEST" && record.details?.reason === "APPROVAL_NOT_FOUND";
}
var ExecApprovalButton = class extends Button {
constructor(ctx) {
super();
this.ctx = ctx;
this.label = "execapproval";
this.customId = "execapproval:seed=1";
this.style = ButtonStyle.Primary;
}
async run(interaction, data) {
const parsed = parseExecApprovalData(data);
if (!parsed) {
try {
await interaction.reply({
content: "This approval is no longer valid.",
ephemeral: true
});
} catch {}
return;
}
const approvers = this.ctx.getApprovers();
const userId = interaction.userId;
if (!approvers.some((id) => id === userId)) {
try {
await interaction.reply({
content: "⛔ You are not authorized to approve exec requests.",
ephemeral: true
});
} catch {}
return;
}
const decisionLabel = parsed.action === "allow-once" ? "Allowed (once)" : parsed.action === "allow-always" ? "Allowed (always)" : "Denied";
try {
await interaction.acknowledge();
} catch {}
const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.action);
if (!result.ok) try {
await interaction.followUp({
content: result.reason === "not-found" ? `That approval request is no longer pending. It may have expired or already been resolved.` : `Failed to submit approval decision for **${decisionLabel}**. The request may have expired or already been resolved.`,
ephemeral: true
});
} catch {}
}
};
function createExecApprovalButton(ctx) {
return new ExecApprovalButton(ctx);
}
function createDiscordExecApprovalButtonContext(params) {
return {
getApprovers: () => getDiscordExecApprovalApprovers({
cfg: params.cfg,
accountId: params.accountId,
configOverride: params.config
}),
resolveApproval: async (approvalId, decision) => {
try {
await resolveApprovalOverGateway({
cfg: params.cfg,
approvalId,
decision,
gatewayUrl: params.gatewayUrl,
clientDisplayName: `Discord approval (${params.accountId})`
});
return { ok: true };
} catch (err) {
return {
ok: false,
reason: isStructuredApprovalNotFoundError(err) ? "not-found" : "error"
};
}
}
};
}
//#endregion
//#region extensions/discord/src/monitor/provider.interactions.ts
function createDiscordProviderInteractionSurface(params) {
const createNativeCommand = params.createNativeCommand ?? createDiscordNativeCommand;
const commands = params.commandSpecs.map((spec) => createNativeCommand({
command: spec,
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
ephemeralDefault: params.ephemeralDefault,
threadBindings: params.threadBindings
}));
if (params.nativeEnabled && params.voiceEnabled) commands.push(createDiscordVoiceCommand({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
groupPolicy: params.groupPolicy,
useAccessGroups: params.useAccessGroups,
getManager: () => params.voiceManagerRef.current,
ephemeralDefault: params.ephemeralDefault
}));
const execApprovalsConfig = params.discordConfig.execApprovals ?? {};
const execApprovalsEnabled = isDiscordExecApprovalClientEnabled({
cfg: params.cfg,
accountId: params.accountId,
configOverride: execApprovalsConfig
});
if (execApprovalsEnabled) registerChannelRuntimeContext({
channelRuntime: params.channelRuntime,
channelId: "discord",
accountId: params.accountId,
capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
context: {
token: params.token,
config: execApprovalsConfig
},
abortSignal: params.abortSignal
});
const components = [
createDiscordCommandArgFallbackButton({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
threadBindings: params.threadBindings
}),
createDiscordModelPickerFallbackButton({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
threadBindings: params.threadBindings
}),
createDiscordModelPickerFallbackSelect({
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
threadBindings: params.threadBindings
})
];
const modals = [];
if (execApprovalsEnabled) components.push(createExecApprovalButton(createDiscordExecApprovalButtonContext({
cfg: params.cfg,
accountId: params.accountId,
config: execApprovalsConfig
})));
if ((params.discordConfig.agentComponents ?? {}).enabled ?? true) {
const componentContext = {
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
guildEntries: params.guildEntries,
allowFrom: params.allowFrom,
dmPolicy: params.dmPolicy,
runtime: params.runtime,
token: params.token
};
components.push(...createAgentComponentControls.map((create) => create(componentContext)));
components.push(...createDiscordComponentControls.map((create) => create(componentContext)));
modals.push(createDiscordComponentModal(componentContext));
}
return {
commands,
components,
modals
};
}
//#endregion
//#region extensions/discord/src/gateway-logging.ts
const INFO_DEBUG_MARKERS = [
"Gateway websocket closed",
"Gateway reconnect scheduled in",
"Gateway forcing fresh IDENTIFY after"
];
const shouldPromoteGatewayDebug = (message) => INFO_DEBUG_MARKERS.some((marker) => message.includes(marker));
const formatGatewayMetrics = (metrics) => {
if (metrics === null || metrics === void 0) return String(metrics);
if (typeof metrics === "string") return metrics;
if (typeof metrics === "number" || typeof metrics === "boolean" || typeof metrics === "bigint") return String(metrics);
try {
return JSON.stringify(metrics);
} catch {
return "[unserializable metrics]";
}
};
function attachDiscordGatewayLogging(params) {
const { emitter, runtime } = params;
if (!emitter) return () => {};
const onGatewayDebug = (msg) => {
const message = String(msg);
logVerbose(`discord gateway: ${message}`);
if (shouldPromoteGatewayDebug(message)) runtime.log?.(`discord gateway: ${message}`);
};
const onGatewayWarning = (warning) => {
logVerbose(`discord gateway warning: ${String(warning)}`);
};
const onGatewayMetrics = (metrics) => {
logVerbose(`discord gateway metrics: ${formatGatewayMetrics(metrics)}`);
};
emitter.on("debug", onGatewayDebug);
emitter.on("warning", onGatewayWarning);
emitter.on("metrics", onGatewayMetrics);
return () => {
emitter.removeListener("debug", onGatewayDebug);
emitter.removeListener("warning", onGatewayWarning);
emitter.removeListener("metrics", onGatewayMetrics);
};
}
//#endregion
//#region extensions/discord/src/monitor.gateway.ts
async function waitForDiscordGatewayStop(params) {
const { gateway, abortSignal } = params;
return await new Promise((resolve, reject) => {
let settled = false;
const cleanup = () => {
abortSignal?.removeEventListener("abort", onAbort);
params.gatewaySupervisor?.detachLifecycle();
};
const finishResolve = () => {
if (settled) return;
settled = true;
try {
gateway?.disconnect?.();
} finally {
cleanup();
resolve();
}
};
const finishReject = (err) => {
if (settled) return;
settled = true;
try {
gateway?.disconnect?.();
} finally {
cleanup();
reject(toLintErrorObject(err, "Non-Error rejection"));
}
};
const onAbort = () => {
finishResolve();
};
const onGatewayEvent = (event) => {
if ((params.onGatewayEvent?.(event) ?? "stop") === "stop") finishReject(new DiscordGatewayLifecycleError(event));
};
const onForceStop = (err) => {
finishReject(err);
};
if (abortSignal?.aborted) {
onAbort();
return;
}
abortSignal?.addEventListener("abort", onAbort, { once: true });
params.gatewaySupervisor?.attachLifecycle(onGatewayEvent);
params.registerForceStop?.(onForceStop);
});
}
function toLintErrorObject(value, fallbackMessage) {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
const error = new Error(fallbackMessage, { cause: value });
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
return error;
}
//#endregion
//#region extensions/discord/src/monitor/provider.lifecycle.ts
const DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS = 15e3;
const DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS = 3e4;
const MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS = 12e4;
const DISCORD_GATEWAY_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_READY_TIMEOUT_MS";
const DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS";
const DISCORD_GATEWAY_READY_POLL_MS = 250;
const DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS = 2e3;
const DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS = 5e3;
const DISCORD_GATEWAY_STARTUP_TERMINATE_CLOSE_TIMEOUT_MS = 1e3;
const DISCORD_GATEWAY_TRANSPORT_ACTIVITY_STATUS_MIN_INTERVAL_MS = 3e4;
function normalizeGatewayReadyTimeoutMs(value) {
const numeric = parseStrictPositiveInteger(value);
if (numeric === void 0) return;
return Math.min(numeric, MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS);
}
function resolveDiscordGatewayReadyTimeoutMs(params) {
return normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_READY_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS;
}
function resolveDiscordGatewayRuntimeReadyTimeoutMs(params) {
return normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ?? normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV]) ?? DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS;
}
async function restartGatewayAfterReadyTimeout(params) {
if (!params.gateway || params.abortSignal?.aborted) return;
const socket = params.gateway.ws;
if (!socket) {
params.gateway.disconnect();
if (!params.abortSignal?.aborted) params.gateway.connect(false);
return;
}
await new Promise((resolve, reject) => {
let settled = false;
let drainTimeout;
let terminateCloseTimeout;
const ignoreSocketError = () => {};
const clearTimers = () => {
if (drainTimeout) {
clearTimeout(drainTimeout);
drainTimeout = void 0;
}
if (terminateCloseTimeout) {
clearTimeout(terminateCloseTimeout);
terminateCloseTimeout = void 0;
}
};
const cleanup = () => {
clearTimers();
socket.removeListener("close", onClose);
socket.removeListener("error", ignoreSocketError);
};
const finishResolve = () => {
if (settled) return;
settled = true;
cleanup();
resolve();
};
const finishReject = (error) => {
if (params.abortSignal?.aborted) {
finishResolve();
return;
}
if (settled) return;
settled = true;
cleanup();
reject(error);
};
const onClose = () => {
finishResolve();
};
socket.on("error", ignoreSocketError);
socket.on("close", onClose);
params.gateway?.disconnect();
drainTimeout = setTimeout(() => {
if (settled) return;
if (typeof socket.terminate !== "function") {
finishReject(/* @__PURE__ */ new Error(`discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`));
return;
}
params.runtime.error?.(danger(`discord: startup restart waiting on a stale gateway socket for ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms; forcing terminate before reconnect`));
try {
socket.terminate();
} catch {
finishReject(/* @__PURE__ */ new Error(`discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`));
return;
}
terminateCloseTimeout = setTimeout(() => {
finishReject(/* @__PURE__ */ new Error(`discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`));
}, DISCORD_GATEWAY_STARTUP_TERMINATE_CLOSE_TIMEOUT_MS);
terminateCloseTimeout.unref?.();
}, DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS);
drainTimeout.unref?.();
});
if (!params.abortSignal?.aborted) params.gateway.connect(false);
}
function parseGatewayCloseCode(message) {
const match = /Gateway websocket closed:\s*(\d{3,5})/.exec(message);
if (!match?.[1]) return;
const code = Number.parseInt(match[1], 10);
return Number.isFinite(code) ? code : void 0;
}
function resolveTransportActivityAt(event) {
const at = event?.at;
const timestampMs = asDateTimestampMs(at);
return timestampMs !== void 0 && timestampMs >= 0 ? timestampMs : Date.now();
}
function createGatewayStatusObserver(params) {
let forceStopHandler;
let queuedForceStopError;
let readyPollId;
let readyTimeoutId;
const shouldStop = () => params.abortSignal?.aborted || params.isLifecycleStopping();
const clearReadyWatch = () => {
if (readyPollId) {
clearInterval(readyPollId);
readyPollId = void 0;
}
if (readyTimeoutId) {
clearTimeout(readyTimeoutId);
readyTimeoutId = void 0;
}
};
const triggerForceStop = (err) => {
if (forceStopHandler) {
forceStopHandler(err);
return;
}
queuedForceStopError = err;
};
const pushConnectedStatus = (at) => {
params.pushStatus({
...createConnectedChannelStatusPatch(at),
lastDisconnect: null,
lastError: null
});
};
const startReadyWatch = () => {
clearReadyWatch();
const pollConnected = () => {
if (shouldStop()) {
clearReadyWatch();
return;
}
if (!params.gateway?.isConnected) return;
clearReadyWatch();
pushConnectedStatus(Date.now());
};
pollConnected();
if (!readyTimeoutId) {
readyPollId = setInterval(pollConnected, DISCORD_GATEWAY_READY_POLL_MS);
readyPollId.unref?.();
readyTimeoutId = setTimeout(() => {
clearReadyWatch();
if (shouldStop() || params.gateway?.isConnected) return;
const at = Date.now();
const error = /* @__PURE__ */ new Error(`discord gateway opened but did not reach READY within ${params.runtimeReadyTimeoutMs}ms`);
params.pushStatus({
connected: false,
lastEventAt: at,
lastDisconnect: {
at,
error: "runtime-not-ready"
},
lastError: "runtime-not-ready"
});
params.runtime.error?.(danger(error.message));
triggerForceStop(error);
}, params.runtimeReadyTimeoutMs);
readyTimeoutId.unref?.();
}
};
const onGatewayDebug = (msg) => {
if (shouldStop()) return;
const at = Date.now();
const message = String(msg);
if (message.includes("Gateway websocket opened")) {
params.pushStatus({
connected: false,
lastEventAt: at
});
startReadyWatch();
return;
}
if (message.includes("Gateway websocket closed")) {
clearReadyWatch();
const code = parseGatewayCloseCode(message);
params.pushStatus({
connected: false,
lastEventAt: at,
lastDisconnect: {
at,
...code !== void 0 ? { status: code } : {}
}
});
return;
}
if (message.includes("Gateway reconnect scheduled in")) {
clearReadyWatch();
params.pushStatus({
connected: false,
lastEventAt: at,
lastError: message
});
}
};
return {
onGatewayDebug,
clearReadyWatch,
registerForceStop: (handler) => {
forceStopHandler = handler;
if (queuedForceStopError !== void 0) {
const err = queuedForceStopError;
queuedForceStopError = void 0;
handler(err);
}
},
dispose: () => {
clearReadyWatch();
forceStopHandler = void 0;
queuedForceStopError = void 0;
}
};
}
async function waitForGatewayReady(params) {
const waitUntilReady = async () => {
const deadlineAt = Date.now() + params.readyTimeoutMs;
while (!params.abortSignal?.aborted) {
if (await params.beforePoll?.() === "stop") return "stopped";
if (params.gateway?.isConnected === true) {
const at = Date.now();
params.pushStatus?.({
...createConnectedChannelStatusPatch(at),
lastDisconnect: null,
lastError: null
});
return "ready";
}
if (Date.now() >= deadlineAt) return "timeout";
await new Promise((resolve) => {
setTimeout(resolve, DISCORD_GATEWAY_READY_POLL_MS).unref?.();
});
}
return "stopped";
};
if (!params.gateway) {
if (await waitUntilReady() === "timeout") throw new Error(`discord gateway did not reach READY within ${params.readyTimeoutMs}ms`);
return;
}
let attempt = 0;
while (!params.abortSignal?.aborted) {
if (await waitUntilReady() !== "timeout") return;
attempt += 1;
const restartAt = Date.now();
params.runtime.error?.(danger(`discord: gateway READY wait timed out after ${params.readyTimeoutMs}ms; reconnecting with backoff (attempt ${attempt})`));
params.pushStatus?.({
connected: false,
lastEventAt: restartAt,
lastDisconnect: {
at: restartAt,
error: "startup-not-ready"
},
lastError: "startup-not-ready"
});
await params.beforeRestart?.();
await restartGatewayAfterReadyTimeout({
gateway: params.gateway,
abortSignal: params.abortSignal,
runtime: params.runtime
});
if (params.abortSignal?.aborted) return;
await new Promise((resolve) => {
setTimeout(resolve, DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS).unref?.();
});
}
}
async function runDiscordGatewayLifecycle(params) {
const gateway = params.gateway;
if (gateway) registerGateway(params.accountId, gateway);
const gatewayEmitter = params.gatewaySupervisor.emitter ?? getDiscordGatewayEmitter(gateway);
const stopGatewayLogging = attachDiscordGatewayLogging({
emitter: gatewayEmitter,
runtime: params.runtime
});
let lifecycleStopping = false;
const pushStatus = (patch) => {
params.statusSink?.(patch);
};
const gatewayReadyTimeoutMs = resolveDiscordGatewayReadyTimeoutMs({
configuredTimeoutMs: params.gatewayReadyTimeoutMs,
env: process.env
});
const gatewayRuntimeReadyTimeoutMs = resolveDiscordGatewayRuntimeReadyTimeoutMs({
configuredTimeoutMs: params.gatewayRuntimeReadyTimeoutMs,
env: process.env
});
const statusObserver = createGatewayStatusObserver({
gateway,
abortSignal: params.abortSignal,
runtime: params.runtime,
pushStatus,
isLifecycleStopping: () => lifecycleStopping,
runtimeReadyTimeoutMs: gatewayRuntimeReadyTimeoutMs
});
gatewayEmitter?.on("debug", statusObserver.onGatewayDebug);
let lastTransportActivityStatusAt;
const onGatewayTransportActivity = (event) => {
if (lifecycleStopping || params.abortSignal?.aborted) return;
const at = resolveTransportActivityAt(event);
if (lastTransportActivityStatusAt !== void 0 && at - lastTransportActivityStatusAt < DISCORD_GATEWAY_TRANSPORT_ACTIVITY_STATUS_MIN_INTERVAL_MS) return;
lastTransportActivityStatusAt = at;
pushStatus(createTransportActivityStatusPatch(at));
};
gatewayEmitter?.on(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, onGatewayTransportActivity);
let sawDisallowedIntents = false;
const handleGatewayEvent = (event) => {
if (params.abortSignal?.aborted && event.type === "reconnect-exhausted") {
lifecycleStopping = true;
params.runtime.log?.(`discord: treating reconnect-exhausted during expected shutdown as clean: ${event.message}`);
return "continue";
}
if (event.type === "disallowed-intents") {
lifecycleStopping = true;
sawDisallowedIntents = true;
params.runtime.error?.(danger("discord: gateway closed with code 4014 (missing privileged gateway intents). Enable the required intents in the Discord Developer Portal or disable them in config."));
return "stop";
}
if (event.shouldStopLifecycle) lifecycleStopping = true;
params.runtime.error?.(danger(event.shouldStopLifecycle ? `discord gateway ${event.type}: ${event.message}` : `discord gateway error: ${event.message}`));
return event.shouldStopLifecycle ? "stop" : "continue";
};
const drainPendingGatewayErrors = () => params.gatewaySupervisor.drainPending((event) => {
if (handleGatewayEvent(event) !== "stop") return "continue";
if (event.type === "disallowed-intents") return "stop";
throw new DiscordGatewayLifecycleError(event);
});
try {
if (drainPendingGatewayErrors() === "stop") return;
await waitForGatewayReady({
gateway,
abortSignal: params.abortSignal,
beforePoll: drainPendingGatewayErrors,
pushStatus,
runtime: params.runtime,
beforeRestart: statusObserver.clearReadyWatch,
readyTimeoutMs: gatewayReadyTimeoutMs
});
if (drainPendingGatewayErrors() === "stop") return;
await waitForDiscordGatewayStop({
gateway: gateway ? { disconnect: () => gateway.disconnect() } : void 0,
abortSignal: params.abortSignal,
gatewaySupervisor: params.gatewaySupervisor,
onGatewayEvent: handleGatewayEvent,
registerForceStop: statusObserver.registerForceStop
});
} catch (err) {
if (!sawDisallowedIntents && !params.isDisallowedIntentsError(err)) throw err;
} finally {
lifecycleStopping = true;
params.gatewaySupervisor.detachLifecycle();
unregisterGateway(params.accountId);
stopGatewayLogging();
statusObserver.dispose();
gatewayEmitter?.removeListener("debug", statusObserver.onGatewayDebug);
gatewayEmitter?.removeListener(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, onGatewayTransportActivity);
if (params.voiceManager) {
await params.voiceManager.destroy();
const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-CszH3stu.js");
setDiscordTranscriptsVoiceManager({
accountId: params.accountId,
manager: null
});
params.voiceManagerRef.current = null;
}
params.threadBindings.stop();
}
}
//#endregion
//#region extensions/discord/src/internal/voice.ts
var VoicePlugin = class extends Plugin {
constructor(..._args) {
super(..._args);
this.id = "voice";
this.adapters = /* @__PURE__ */ new Map();
}
registerClient(client) {
this.client = client;
this.gatewayPlugin = client.getPlugin("gateway");
if (!this.gatewayPlugin) throw new Error("Discord voice cannot be used without a gateway connection.");
}
getGateway(_guildId) {
return this.gatewayPlugin;
}
getGatewayAdapterCreator(guildId) {
const gateway = this.getGateway(guildId);
if (!gateway) throw new Error("Discord voice cannot be used without a gateway connection.");
return (methods) => {
this.adapters.set(guildId, methods);
return {
sendPayload(payload) {
try {
gateway.send(payload, true);
return true;
} catch {
return false;
}
},
destroy: () => {
this.adapters.delete(guildId);
}
};
};
}
};
//#endregion
//#region extensions/discord/src/monitor/provider.startup.ts
function registerLatePlugin(client, plugin) {
plugin.registerClient?.(client);
plugin.registerRoutes?.(client);
if (!client.plugins.some((entry) => entry.id === plugin.id)) client.plugins.push({
id: plugin.id,
plugin
});
}
function createDiscordStatusReadyListener(params) {
return new class DiscordStatusReadyListener extends ReadyListener {
async handle(_data, client) {
const autoPresenceController = params.getAutoPresenceController();
if (autoPresenceController?.enabled) {
autoPresenceController.refresh();
return;
}
const gateway = client.getPlugin("gateway");
if (!gateway) return;
const presence = resolveDiscordPresenceUpdate(params.discordConfig);
if (!presence) return;
gateway.updatePresence(presence);
}
}();
}
async function createDiscordMonitorClient(params) {
let autoPresenceController = null;
const clientPlugins = [params.createGatewayPlugin({
discordConfig: params.discordConfig,
runtime: params.runtime
})];
if (params.voiceEnabled) clientPlugins.push(new VoicePlugin());
const voicePlugin = clientPlugins.find((plugin) => plugin.id === "voice");
const constructorPlugins = voicePlugin ? clientPlugins.filter((plugin) => plugin !== voicePlugin) : clientPlugins;
const eventQueueOpts = {
listenerTimeout: 12e4,
slowListenerThreshold: 3e4,
...params.discordConfig.eventQueue
};
const readyListener = createDiscordStatusReadyListener({
discordConfig: params.discordConfig,
getAutoPresenceController: () => autoPresenceController
});
const client = params.createClient({
baseUrl: "http://localhost",
deploySecret: "a",
clientId: params.applicationId,
publicKey: "a",
token: params.token,
autoDeploy: false,
commandDeployHashStorePath: path.join(resolveStateDir(process.env), "discord", "command-deploy-cache.json"),
requestOptions: {
timeout: DISCORD_REST_TIMEOUT_MS,
runtimeProfile: "persistent",
maxQueueSize: 1e3,
...params.restFetch ? { fetch: params.restFetch } : {}
},
eventQueue: eventQueueOpts
}, {
commands: params.commands,
listeners: [readyListener],
components: params.components,
modals: params.modals
}, constructorPlugins);
if (voicePlugin) registerLatePlugin(client, voicePlugin);
const gateway = client.getPlugin("gateway");
await waitForDiscordGatewayPluginRegistration(gateway);
const gatewaySupervisor = params.createGatewaySupervisor({
gateway,
isDisallowedIntentsError: params.isDisallowedIntentsError,
runtime: params.runtime
});
if (gateway) {
autoPresenceController = params.createAutoPresenceController({
accountId: params.accountId,
discordConfig: params.discordConfig,
gateway,
log: (message) => params.runtime.log?.(message)
});
autoPresenceController.start();
}
return {
client,
gateway,
gatewaySupervisor,
autoPresenceController,
eventQueueOpts
};
}
async function fetchDiscordBotIdentity(params) {
params.logStartupPhase("fetch-bot-identity:start");
const parsedBotUserId = parseApplicationIdFromToken(params.token ?? "");
if (parsedBotUserId) {
params.logStartupPhase("fetch-bot-identity:done", `botUserId=${parsedBotUserId} botUserName=<missing> source=token`);
return {
botUserId: parsedBotUserId,
botUserName: void 0
};
}
let botUser;
try {
botUser = await params.client.fetchUser("@me");
} catch (err) {
params.runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
params.logStartupPhase("fetch-bot-identity:error", String(err));
throw new Error("Failed to resolve Discord bot identity", { cause: err });
}
const botUserRecord = botUser;
const botUserId = normalizeOptionalString(botUserRecord?.id);
const botUserName = normalizeOptionalString(botUserRecord?.username) ?? normalizeOptionalString(botUserRecord?.globalName);
if (!botUserId) {
const details = "fetchUser(\"@me\") returned no usable id";
params.runtime.error?.(danger(`discord: failed to fetch bot identity: ${details}`));
params.logStartupPhase("fetch-bot-identity:error", details);
throw new Error("Failed to resolve Discord bot identity");
}
params.logStartupPhase("fetch-bot-identity:done", `botUserId=${botUserId} botUserName=${botUserName ?? "<missing>"}`);
return {
botUserId,
botUserName
};
}
function registerDiscordMonitorListeners(params) {
registerDiscordListener(params.client.listeners, new DiscordInteractionListener(params.logger, params.trackInboundEvent));
registerDiscordListener(params.client.listeners, new DiscordMessageListener(params.messageHandler, params.logger, params.trackInboundEvent));
if (shouldRegisterDiscordReactionListeners(params)) {
const reactionListenerOptions = {
cfg: params.cfg,
accountId: params.accountId,
runtime: params.runtime,
botUserId: params.botUserId,
dmEnabled: params.dmEnabled,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels: params.groupDmChannels ?? [],
dmPolicy: params.dmPolicy,
allowFrom: params.allowFrom ?? [],
groupPolicy: params.groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
guildEntries: params.guildEntries,
logger: params.logger,
onEvent: params.trackInboundEvent
};
registerDiscordListener(params.client.listeners, new DiscordReactionListener(reactionListenerOptions));
registerDiscordListener(params.client.listeners, new DiscordReactionRemoveListener(reactionListenerOptions));
}
registerDiscordListener(params.client.listeners, new DiscordThreadUpdateListener(params.cfg, params.accountId, params.logger));
if (params.discordConfig.intents?.presence) {
registerDiscordListener(params.client.listeners, new DiscordPresenceListener({
logger: params.logger,
accountId: params.accountId
}));
params.runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
}
}
function shouldRegisterDiscordReactionListeners(params) {
if (params.dmEnabled || params.groupDmEnabled) return true;
if (params.groupPolicy === "disabled") return false;
const guildEntries = Object.values(params.guildEntries ?? {});
if (guildEntries.length === 0) return true;
return guildEntries.some((entry) => entry.reactionNotifications !== "off");
}
//#endregion
//#region extensions/discord/src/monitor/rest-fetch.ts
const discordDnsLookup = createDiscordDnsLookup();
function createDirectDiscordRestDispatcher() {
return new Agent$1({
allowH2: false,
connect: { lookup: discordDnsLookup }
});
}
function createEnvProxyDiscordRestDispatcher(runtime) {
const envProxyOptions = resolveEnvHttpProxyAgentOptions();
if (!envProxyOptions) return;
try {
return createHttp1EnvHttpProxyAgent({
...envProxyOptions,
connect: { lookup: discordDnsLookup }
});
} catch (err) {
runtime.error?.(danger(`discord: env proxy unavailable for REST fetch; using direct dispatcher: ${formatErrorMessage(err)}`));
return;
}
}
function createDiscordRestFetchWithDispatcher(dispatcher) {
return wrapFetchWithAbortSignal(((input, init) => fetch$1(input, {
...init,
dispatcher
}).then((response) => {
captureHttpExchange({
url: resolveRequestUrl(input),
method: init?.method ?? "GET",
requestHeaders: init?.headers,
requestBody: init?.body ?? null,
response,
flowId: randomUUID(),
meta: { subsystem: "discord-rest" }
});
return response;
})));
}
function resolveDiscordRestFetch(proxyUrl, runtime) {
const effectiveProxyUrl = resolveEffectiveDebugProxyUrl(proxyUrl);
if (effectiveProxyUrl) {
const fetcher = withValidatedDiscordProxy(effectiveProxyUrl, runtime, (proxy) => createDiscordRestFetchWithDispatcher(createHttp1ProxyAgent({ uri: proxy })));
if (!fetcher) return fetch;
runtime.log?.("discord: rest proxy enabled");
return fetcher;
}
return createDiscordRestFetchWithDispatcher(createEnvProxyDiscordRestDispatcher(runtime) ?? createDirectDiscordRestDispatcher());
}
//#endregion
//#region extensions/discord/src/monitor/startup-status.ts
function formatDiscordStartupStatusMessage(params) {
const identitySuffix = params.botIdentity ? ` as ${params.botIdentity}` : "";
if (params.gatewayReady) return `logged in to discord${identitySuffix}`;
return `discord client initialized${identitySuffix}; awaiting gateway readiness`;
}
//#endregion
//#region extensions/discord/src/monitor/provider.ts
const DEFAULT_DISCORD_MEDIA_MAX_MB = 100;
let discordVoiceRuntimePromise;
let discordProviderSessionRuntimePromise;
let getPluginCommandSpecsForTesting;
function logDiscordStartupPhase(params) {
logDiscordStartupPhase$1({
...params,
isVerbose
});
}
async function loadDiscordVoiceRuntime() {
const promise = discordVoiceRuntimePromise ?? import("./manager.runtime.js");
discordVoiceRuntimePromise = promise;
try {
return await promise;
} catch (error) {
if (discordVoiceRuntimePromise === promise) discordVoiceRuntimePromise = void 0;
throw error;
}
}
async function loadDiscordProviderSessionRuntime() {
const promise = discordProviderSessionRuntimePromise ?? import("./provider-session.runtime.js");
discordProviderSessionRuntimePromise = promise;
try {
return await promise;
} catch (error) {
if (discordProviderSessionRuntimePromise === promise) discordProviderSessionRuntimePromise = void 0;
throw error;
}
}
const DISCORD_DISALLOWED_INTENTS_CODE = GatewayCloseCodes.DisallowedIntents;
function isDiscordDisallowedIntentsError(err) {
if (!err) return false;
return formatErrorMessage(err).includes(String(DISCORD_DISALLOWED_INTENTS_CODE));
}
async function monitorDiscordProvider(opts = {}) {
const startupStartedAt = Date.now();
const cfg = opts.config ?? getRuntimeConfig();
const account = resolveDiscordAccount({
cfg,
accountId: opts.accountId
});
const token = normalizeDiscordToken(opts.token ?? void 0, "channels.discord.token") ?? account.token;
if (!token) throw new Error(`Discord bot token missing for account "${account.accountId}" (set discord.accounts.${account.accountId}.token or DISCORD_BOT_TOKEN for default).`);
const runtime = opts.runtime ?? createNonExitingRuntime();
const rawDiscordCfg = account.config;
const discordRootThreadBindings = cfg.channels?.discord?.threadBindings;
const discordAccountThreadBindings = cfg.channels?.discord?.accounts?.[account.accountId]?.threadBindings;
const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime);
const dmConfig = rawDiscordCfg.dm;
const configuredDmAllowFrom = resolveDiscordAccountAllowFrom({
cfg,
accountId: account.accountId
});
let guildEntries = rawDiscordCfg.guilds;
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: cfg.channels?.discord !== void 0,
groupPolicy: rawDiscordCfg.groupPolicy,
defaultGroupPolicy
});
const discordCfg = rawDiscordCfg.groupPolicy === groupPolicy ? rawDiscordCfg : {
...rawDiscordCfg,
groupPolicy
};
warnMissingProviderGroupPolicyFallbackOnce({
providerMissingFallbackApplied,
providerKey: "discord",
accountId: account.accountId,
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.guild,
log: (message) => runtime.log?.(warn(message))
});
let allowFrom = configuredDmAllowFrom ?? [];
const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? DEFAULT_DISCORD_MEDIA_MAX_MB) * 1024 * 1024;
const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, { fallbackLimit: 2e3 });
const historyLimit = Math.max(0, opts.historyLimit ?? discordCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 20);
const replyToMode = opts.replyToMode ?? discordCfg.replyToMode ?? "off";
const dmEnabled = dmConfig?.enabled ?? true;
const dmPolicy = resolveDiscordAccountDmPolicy({
cfg,
accountId: account.accountId
}) ?? "pairing";
const discordProviderSessionRuntime = await loadDiscordProviderSessionRuntime();
const threadBindingIdleTimeoutMs = discordProviderSessionRuntime.resolveThreadBindingIdleTimeoutMs({
channelIdleHoursRaw: discordAccountThreadBindings?.idleHours ?? discordRootThreadBindings?.idleHours,
sessionIdleHoursRaw: cfg.session?.threadBindings?.idleHours
});
const threadBindingMaxAgeMs = discordProviderSessionRuntime.resolveThreadBindingMaxAgeMs({
channelMaxAgeHoursRaw: discordAccountThreadBindings?.maxAgeHours ?? discordRootThreadBindings?.maxAgeHours,
sessionMaxAgeHoursRaw: cfg.session?.threadBindings?.maxAgeHours
});
const threadBindingsEnabled = discordProviderSessionRuntime.resolveThreadBindingsEnabled({
channelEnabledRaw: discordAccountThreadBindings?.enabled ?? discordRootThreadBindings?.enabled,
sessionEnabledRaw: cfg.session?.threadBindings?.enabled
});
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
const groupDmChannels = dmConfig?.groupChannels;
const nativeEnabled = resolveNativeCommandsEnabled({
providerId: "discord",
providerSetting: discordCfg.commands?.native,
globalSetting: cfg.commands?.native
});
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
providerId: "discord",
providerSetting: discordCfg.commands?.nativeSkills,
globalSetting: cfg.commands?.nativeSkills
});
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const slashCommand = resolveDiscordSlashCommandConfig(discordCfg.slashCommand);
const sessionPrefix = "discord:slash";
const ephemeralDefault = slashCommand.ephemeral;
const voiceEnabled = resolveDiscordVoiceEnabled(discordCfg.voice);
const allowlistResolved = await resolveDiscordAllowlistConfig({
token,
guildEntries,
allowFrom,
discordConfig: discordCfg,
fetcher: discordRestFetch,
runtime
});
guildEntries = allowlistResolved.guildEntries;
allowFrom = allowlistResolved.allowFrom ?? [];
if (shouldLogVerbose()) logDiscordResolvedConfig({
dmEnabled,
dmPolicy,
allowFrom,
groupDmEnabled,
groupDmChannels,
groupPolicy,
guildEntries,
historyLimit,
mediaMaxBytes,
nativeEnabled,
nativeSkillsEnabled,
useAccessGroups,
threadBindingsEnabled,
threadBindingIdleTimeoutMs,
threadBindingMaxAgeMs
});
logDiscordStartupPhase({
runtime,
accountId: account.accountId,
phase: "fetch-application-id:start",
startAt: startupStartedAt
});
const applicationId = (typeof discordCfg.applicationId === "string" && discordCfg.applicationId.trim() ? discordCfg.applicationId.trim() : void 0) ?? parseApplicationIdFromToken(token) ?? await fetchDiscordApplicationId(token, 4e3, discordRestFetch);
if (!applicationId) throw new Error("Failed to resolve Discord application id");
logDiscordStartupPhase({
runtime,
accountId: account.accountId,
phase: "fetch-application-id:done",
startAt: startupStartedAt,
details: `applicationId=${applicationId}`
});
const { commandSpecs } = await resolveDiscordProviderCommandSpecs({
cfg,
runtime,
nativeEnabled,
nativeSkillsEnabled,
listSkillCommandsForAgents,
listNativeCommandSpecsForConfig,
getPluginCommandSpecs: getPluginCommandSpecsForTesting
});
const voiceManagerRef = { current: null };
const threadBindings = threadBindingsEnabled ? discordProviderSessionRuntime.createThreadBindingManager({
accountId: account.accountId,
token,
cfg,
idleTimeoutMs: threadBindingIdleTimeoutMs,
maxAgeMs: threadBindingMaxAgeMs
}) : discordProviderSessionRuntime.createNoopThreadBindingManager(account.accountId);
if (threadBindingsEnabled) {
const uncertainProbeKeys = /* @__PURE__ */ new Set();
const reconciliation = await discordProviderSessionRuntime.reconcileAcpThreadBindingsOnStartup({
cfg,
accountId: account.accountId,
sendFarewell: false,
healthProbe: async ({ sessionKey, session }) => {
const probe = await probeDiscordAcpBindingHealth({
cfg,
sessionKey,
storedState: session.acp?.state,
lastActivityAt: session.acp?.lastActivityAt,
providerSessionRuntime: discordProviderSessionRuntime
});
if (probe.status === "uncertain") uncertainProbeKeys.add(`${sessionKey}${probe.reason ? ` (${probe.reason})` : ""}`);
return probe;
}
});
if (reconciliation.removed > 0) logVerbose(`discord: removed ${reconciliation.removed}/${reconciliation.checked} stale ACP thread bindings on startup for account ${account.accountId}: ${reconciliation.staleSessionKeys.join(", ")}`);
if (uncertainProbeKeys.size > 0) logVerbose(`discord: ACP thread-binding health probe uncertain for account ${account.accountId}: ${[...uncertainProbeKeys].join(", ")}`);
}
let lifecycleStarted = false;
let gatewaySupervisor;
let deactivateMessageHandler;
let autoPresenceController = null;
let lifecycleGateway;
let earlyGatewayEmitter = gatewaySupervisor?.emitter;
let onEarlyGatewayDebug;
try {
const { commands, components, modals } = createDiscordProviderInteractionSurface({
cfg,
discordConfig: discordCfg,
accountId: account.accountId,
token,
commandSpecs,
nativeEnabled,
voiceEnabled,
groupPolicy,
useAccessGroups,
sessionPrefix,
ephemeralDefault,
threadBindings,
voiceManagerRef,
guildEntries,
allowFrom,
dmPolicy,
runtime,
channelRuntime: opts.channelRuntime,
abortSignal: opts.abortSignal,
createNativeCommand: createDiscordNativeCommand
});
const { client, gateway, gatewaySupervisor: createdGatewaySupervisor, autoPresenceController: createdAutoPresenceController } = await createDiscordMonitorClient({
accountId: account.accountId,
applicationId,
token,
restFetch: discordRestFetch,
commands,
components,
modals,
voiceEnabled,
discordConfig: discordCfg,
runtime,
createClient: ((...args) => new Client(...args)),
createGatewayPlugin: createDiscordGatewayPlugin,
createGatewaySupervisor: createDiscordGatewaySupervisor,
createAutoPresenceController: createDiscordAutoPresenceController,
isDisallowedIntentsError: isDiscordDisallowedIntentsError
});
lifecycleGateway = gateway;
gatewaySupervisor = createdGatewaySupervisor;
autoPresenceController = createdAutoPresenceController;
earlyGatewayEmitter = gatewaySupervisor.emitter;
onEarlyGatewayDebug = (msg) => {
if (!isVerbose()) return;
runtime.log?.(`discord startup [${account.accountId}] gateway-debug ${Math.max(0, Date.now() - startupStartedAt)}ms ${String(msg)}`);
};
earlyGatewayEmitter?.on("debug", onEarlyGatewayDebug);
logDiscordStartupPhase({
runtime,
accountId: account.accountId,
phase: "deploy-commands:schedule",
startAt: startupStartedAt,
gateway: lifecycleGateway,
details: `native=${nativeEnabled ? "on" : "off"} reconcile=on commandCount=${commands.length}`
});
runDiscordCommandDeployInBackground({
client,
runtime,
enabled: nativeEnabled,
accountId: account.accountId,
startupStartedAt,
shouldLogVerbose,
isVerbose
});
const logger = createSubsystemLogger("discord/monitor");
const guildHistories = /* @__PURE__ */ new Map();
const { botUserId, botUserName } = await fetchDiscordBotIdentity({
client,
token,
runtime,
logStartupPhase: (phase, details) => logDiscordStartupPhase({
runtime,
accountId: account.accountId,
phase,
startAt: startupStartedAt,
gateway: lifecycleGateway,
details
})
});
let voiceManager = null;
if (voiceEnabled) {
const { DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener } = await loadDiscordVoiceRuntime();
voiceManager = new DiscordVoiceManager({
client,
cfg,
discordConfig: discordCfg,
accountId: account.accountId,
runtime,
botUserId
});
const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-CszH3stu.js");
setDiscordTranscriptsVoiceManager({
accountId: account.accountId,
manager: voiceManager
});
voiceManagerRef.current = voiceManager;
registerDiscordListener(client.listeners, new DiscordVoiceReadyListener(voiceManager));
registerDiscordListener(client.listeners, new DiscordVoiceResumedListener(voiceManager));
registerDiscordListener(client.listeners, new DiscordVoiceStateUpdateListener(voiceManager));
}
const messageHandler = discordProviderSessionRuntime.createDiscordMessageHandler({
cfg,
discordConfig: discordCfg,
accountId: account.accountId,
token,
runtime,
setStatus: opts.setStatus,
abortSignal: opts.abortSignal,
botUserId,
guildHistories,
historyLimit,
mediaMaxBytes,
textLimit,
replyToMode,
dmEnabled,
dmPolicy,
groupDmEnabled,
groupDmChannels,
allowFrom,
guildEntries,
threadBindings,
discordRestFetch
});
deactivateMessageHandler = messageHandler.deactivate;
const trackInboundEvent = opts.setStatus ? () => {
const at = Date.now();
opts.setStatus?.({
lastEventAt: at,
lastInboundAt: at
});
} : void 0;
registerDiscordMonitorListeners({
cfg,
client,
accountId: account.accountId,
discordConfig: discordCfg,
runtime,
botUserId,
dmEnabled,
groupDmEnabled,
groupDmChannels,
dmPolicy,
allowFrom,
groupPolicy,
guildEntries,
logger,
messageHandler,
trackInboundEvent
});
logDiscordStartupPhase({
runtime,
accountId: account.accountId,
phase: "client-start",
startAt: startupStartedAt,
gateway: lifecycleGateway
});
const botIdentity = botUserId && botUserName ? `${botUserId} (${botUserName})` : botUserId ?? botUserName ?? "";
runtime.log?.(formatDiscordStartupStatusMessage({
gatewayReady: lifecycleGateway?.isConnected === true,
botIdentity: botIdentity || void 0
}));
if (lifecycleGateway?.isConnected) opts.setStatus?.(createConnectedChannelStatusPatch());
lifecycleStarted = true;
earlyGatewayEmitter?.removeListener("debug", onEarlyGatewayDebug);
onEarlyGatewayDebug = void 0;
await runDiscordGatewayLifecycle({
accountId: account.accountId,
gateway: lifecycleGateway,
runtime,
abortSignal: opts.abortSignal,
statusSink: opts.setStatus,
isDisallowedIntentsError: isDiscordDisallowedIntentsError,
voiceManager,
voiceManagerRef,
threadBindings,
gatewaySupervisor,
gatewayReadyTimeoutMs: account.config.gatewayReadyTimeoutMs,
gatewayRuntimeReadyTimeoutMs: account.config.gatewayRuntimeReadyTimeoutMs
});
} finally {
cleanupDiscordProviderStartup({
deactivateMessageHandler,
autoPresenceController,
setStatus: opts.setStatus,
onEarlyGatewayDebug,
earlyGatewayEmitter,
lifecycleStarted,
lifecycleGateway,
gatewaySupervisor,
threadBindings,
runtime
});
}
}
//#endregion
export { createDiscordNativeCommand as a, waitForDiscordGatewayPluginRegistration as i, createDiscordGatewayPlugin as n, registerDiscordListener as o, resolveDiscordGatewayIntents as r, monitorDiscordProvider as t };