@openclaw/discord 2026.7.1-beta.6 → 2026.7.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/action-runtime-api.js +1 -1
  2. package/dist/{allow-list-C-MqM-B_.js → allow-list-B7qOH5xu.js} +22 -1
  3. package/dist/{api-DPRqYi4Z.js → api--f62GFaS.js} +38 -23
  4. package/dist/api.js +15 -15
  5. package/dist/approval-custom-id-NYPYj9Po.js +58 -0
  6. package/dist/{approval-handler.runtime-Bw2pZNk1.js → approval-handler.runtime-DeyIiw5I.js} +28 -23
  7. package/dist/approval-message-safety-C-8BL6Kv.js +47 -0
  8. package/dist/{audit-MLpDpN3Z.js → audit-Df2w046a.js} +3 -3
  9. package/dist/{channel-actions-D_fjVUZ_.js → channel-actions-ChpBSRcu.js} +6 -4
  10. package/dist/{channel-actions.runtime-CVHlFqQg.js → channel-actions.runtime-D8V8tyCq.js} +41 -12
  11. package/dist/channel-config-api.js +1 -1
  12. package/dist/{channel-DGWPH5u3.js → channel-nYmJ7FKQ.js} +275 -62
  13. package/dist/channel-plugin-api.js +1 -1
  14. package/dist/{channel.setup-tZcpIn4e.js → channel.setup-DyihrqHH.js} +3 -3
  15. package/dist/{components-BIBizbtZ.js → components-BVrfRS-F.js} +78 -52
  16. package/dist/config-api-B18fR8M5.js +3 -0
  17. package/dist/{config-schema-BgIhO3ko.js → config-schema-_bUKcWRm.js} +42 -82
  18. package/dist/conversation-identity-BpkAzklg.js +94 -0
  19. package/dist/{directory-config-ByaUKL1f.js → directory-config-BUe-qq_C.js} +1 -1
  20. package/dist/directory-contract-api.js +1 -1
  21. package/dist/{directory-live-BX79ImQA.js → directory-live-DZByMdPO.js} +6 -6
  22. package/dist/{doctor-Bgoxm5AN.js → doctor-bMaWXFKu.js} +5 -4
  23. package/dist/doctor-contract-api.js +1 -1
  24. package/dist/{doctor-contract-CU7lnZTF.js → doctor-contract-wId6pV41.js} +41 -37
  25. package/dist/{handle-action.guild-admin-CgJSNVui.js → handle-action.guild-admin-C_PQcBEV.js} +12 -12
  26. package/dist/index.js +1 -1
  27. package/dist/legacy-state-migrations-api.js +24 -2
  28. package/dist/{manager.runtime-DaB-CTrt.js → manager.runtime-3RVMVkyx.js} +546 -154
  29. package/dist/{message-handler-DcgvugZt.js → message-handler-CIxWq5Yh.js} +8 -9
  30. package/dist/{message-handler.preflight-DvLXiYVH.js → message-handler.preflight-BI86IbHz.js} +46 -22
  31. package/dist/{message-handler.process-Cjf30P_B.js → message-handler.process-bPS_9_tb.js} +241 -107
  32. package/dist/{message-utils-B_3yA5bN.js → message-utils-D4j07taS.js} +2 -3
  33. package/dist/{outbound-adapter-82RWi_SR.js → outbound-adapter-Czny0Fae.js} +16 -68
  34. package/dist/outbound-components-oJKLj62G.js +99 -0
  35. package/dist/pluralkit-UXq1OyUI.js +40 -0
  36. package/dist/{probe-D1ImJEo2.js → probe-DxjMBeVG.js} +1 -1
  37. package/dist/{probe.runtime-B1dsEeBD.js → probe.runtime-Bc8Oo6e8.js} +1 -1
  38. package/dist/{provider-BkfbC9FQ.js → provider-DWvDNb4J.js} +702 -236
  39. package/dist/{provider-session.runtime-BDjyFw6Y.js → provider-session.runtime-D5pCGibh.js} +3 -3
  40. package/dist/provider.runtime-C2iIfbTs.js +2 -0
  41. package/dist/{resolve-allowlist-common-CWbE8XlH.js → resolve-allowlist-common-B9S8LLBd.js} +5 -5
  42. package/dist/{resolve-channels-CcpvWoRa.js → resolve-channels-BrkWOKdG.js} +7 -7
  43. package/dist/{resolve-users-DJamun9s.js → resolve-users-OjrXXvJP.js} +5 -5
  44. package/dist/{runtime-C6rRmQ6l.js → runtime-C618lis9.js} +223 -33
  45. package/dist/runtime-api.actions.js +2 -2
  46. package/dist/runtime-api.js +23 -23
  47. package/dist/runtime-api.lookup.js +5 -5
  48. package/dist/runtime-api.monitor-DSbfGnzS.js +6 -0
  49. package/dist/runtime-api.monitor.js +7 -7
  50. package/dist/runtime-api.send.js +5 -5
  51. package/dist/runtime-api.threads.js +5 -5
  52. package/dist/{send-iJ0B4bAX.js → send-BFpa_DHn.js} +21 -12
  53. package/dist/{send.components-D0I39utR.js → send.components-BaXQJKWT.js} +15 -10
  54. package/dist/{send.outbound-Bj61Q3a0.js → send.outbound-BlA6VzSE.js} +22 -12
  55. package/dist/{send.receipt-BpBQED_5.js → send.receipt-aegk69an.js} +59 -75
  56. package/dist/{send.shared-wAwMhHDN.js → send.shared-Xz7IMVpu.js} +44 -20
  57. package/dist/{sender-identity-CEsfDF0L.js → sender-identity-C62lBe18.js} +1 -1
  58. package/dist/session-binding-contract-api.js +1 -1
  59. package/dist/setup-plugin-api.js +1 -1
  60. package/dist/{shared-BQVKfnev.js → shared-BLYBt5SA.js} +3 -3
  61. package/dist/{subagent-hooks-B-bJxxGg.js → subagent-hooks-DKJ4p-Oy.js} +2 -2
  62. package/dist/subagent-hooks-api.js +1 -1
  63. package/dist/{system-events-DVnqSyTt.js → system-events-DKwfE-2z.js} +2 -2
  64. package/dist/{target-resolver-CwHaSLt3.js → target-resolver-BQyEHyeS.js} +2 -2
  65. package/dist/targets-DvItNIvP.js +3 -0
  66. package/dist/thread-binding-api-BDZJD4na.js +4 -0
  67. package/dist/thread-binding-api.js +1 -3
  68. package/dist/{thread-bindings-DsTxqu3E.js → thread-bindings-BbzjUuNB.js} +26 -43
  69. package/dist/{thread-bindings.discord-api-BjIf9lGs.js → thread-bindings.discord-api-DJD3qwIN.js} +8 -10
  70. package/dist/{thread-bindings.manager-Bwmtqc3E.js → thread-bindings.manager-BIBm6bQd.js} +4 -4
  71. package/dist/{thread-bindings.session-updates-Chs3HgGa.js → thread-bindings.session-updates-eRcJlzIU.js} +1 -1
  72. package/dist/{thread-bindings.state-iSWjCYjB.js → thread-bindings.state-9VAxhVf6.js} +3 -14
  73. package/dist/{timeouts-C5TBc_9x.js → timeouts-ZRc9Lw4y.js} +3 -15
  74. package/dist/timeouts.js +1 -1
  75. package/dist/{transcripts-source-BvvM5Ns9.js → transcripts-source-CLZ5X8fh.js} +1 -1
  76. package/dist/transcripts-source-api.js +1 -1
  77. package/dist/{typing-BVcJgILg.js → typing-BnYT7Szq.js} +2 -2
  78. package/node_modules/p-map/index.d.ts +155 -0
  79. package/node_modules/p-map/index.js +284 -0
  80. package/node_modules/p-map/license +9 -0
  81. package/node_modules/p-map/package.json +57 -0
  82. package/node_modules/p-map/readme.md +190 -0
  83. package/npm-shrinkwrap.json +16 -3
  84. package/openclaw.plugin.json +70 -2
  85. package/package.json +6 -4
  86. package/dist/config-api-JiPdJeb0.js +0 -2
  87. package/dist/conversation-identity-DWC5d8Sy.js +0 -272
  88. package/dist/pluralkit-DcD-3EcL.js +0 -24
  89. package/dist/preview-streaming-DXT8oJdo.js +0 -8
  90. package/dist/provider.runtime-WrJXikSv.js +0 -2
  91. package/dist/runtime-api.monitor-C2_WVtJf.js +0 -6
  92. package/dist/targets-69w7c3vU.js +0 -3
@@ -1,23 +1,24 @@
1
- import { $ as StringSelectMenu, A as Command, H as Button, Jt as __exportAll, M as Guild, N as Message, P as User, Q as Separator, S as Plugin, T as RateLimitError, W as Container, X as Row, _ as ReadyListener, d as discord_exports, et as TextDisplay, f as InteractionCreateListener, ft as createThread, g as PresenceUpdateListener, h as MessageReactionRemoveListener, ht as editChannel, j as CommandWithSubcommands, m as MessageReactionAddListener, p as MessageCreateListener, rt as BaseMessageInteractiveComponent, u as chunkDiscordTextWithMode, vt as getChannelMessage, x as Client, y as ThreadUpdateListener, z as Modal } from "./send.receipt-BpBQED_5.js";
1
+ import { C as Client, D as RateLimitError, F as Message, I as User, K as Container, M as Command, N as CommandWithSubcommands, P as Guild, Q as Row, V as Modal, W as Button, Xt as __exportAll, _ as MessageReactionRemoveListener, _t as editChannel, at as BaseMessageInteractiveComponent, bt as getChannelMessage, d as discord_exports, et as Separator, f as GuildCreateListener, g as MessageReactionAddListener, h as MessageCreateListener, m as InteractionCreateListener, mt as createThread, nt as TextDisplay, p as GuildDeleteListener, tt as StringSelectMenu, u as chunkDiscordTextWithMode, v as PresenceUpdateListener, w as Plugin, x as ThreadUpdateListener, y as ReadyListener } from "./send.receipt-aegk69an.js";
2
2
  import { c as resolveDiscordAccountAllowFrom, d as resolveDiscordAccountDmPolicy, f as resolveDiscordMaxLinesPerMessage, p as normalizeDiscordToken, s as resolveDiscordAccount } from "./accounts-P19jm1lf.js";
3
- import { _ as parseDiscordComponentCustomId, b as parseDiscordModalCustomIdForInteraction, v as parseDiscordComponentCustomIdForInteraction, y as parseDiscordModalCustomId } from "./components-BIBizbtZ.js";
4
- import { a as resolveDiscordComponentEntryWithPersistence, o as resolveDiscordModalEntryWithPersistence, t as editDiscordComponentMessage } from "./send.components-D0I39utR.js";
5
- import { i as setPresence } from "./presence-cache-z1N8MbJ3.js";
6
- import { B as DISCORD_REST_TIMEOUT_MS, D as canViewDiscordGuildChannel, G as registerGateway, H as withValidatedDiscordProxy, I as createDiscordRestClient, K as unregisterGateway, V as validateDiscordProxyUrl } from "./send.shared-wAwMhHDN.js";
7
- import { a as summarizeDiscordResponseBody, i as isDiscordRateLimitResponseBody } from "./api-DPRqYi4Z.js";
8
- import { S as resolveTimestampMs, _ as resolveGroupDmAllow, a as normalizeDiscordSlug, b as formatDiscordUserTag, c as resolveDiscordChannelConfigWithFallback, d as resolveDiscordGuildEntry, f as resolveDiscordMemberAccessState, h as resolveDiscordOwnerAllowFrom, i as normalizeDiscordDisplaySlug, l as resolveDiscordChannelPolicyCommandAuthorizer, m as resolveDiscordOwnerAccess, n as isDiscordGroupAllowedByPolicy, o as resolveDiscordAllowListMatch, p as resolveDiscordMemberAllowed, r as normalizeDiscordAllowList, v as shouldEmitDiscordReactionNotification, y as formatDiscordReactionEmoji } from "./allow-list-C-MqM-B_.js";
9
- import { s as sendVoiceMessageDiscord } from "./send-iJ0B4bAX.js";
10
- import { a as formatMention, t as sendMessageDiscord } from "./send.outbound-Bj61Q3a0.js";
3
+ import { S as encodeCustomIdComponent, _ as parseDiscordComponentCustomId, b as parseDiscordModalCustomIdForInteraction, v as parseDiscordComponentCustomIdForInteraction, x as decodeCustomIdComponent, y as parseDiscordModalCustomId } from "./components-BVrfRS-F.js";
4
+ import { a as resolveDiscordComponentEntryWithPersistence, o as resolveDiscordModalEntryWithPersistence, t as editDiscordComponentMessage } from "./send.components-BaXQJKWT.js";
5
+ import { i as setPresence, t as clearPresences } from "./presence-cache-z1N8MbJ3.js";
6
+ import { B as DISCORD_REST_TIMEOUT_MS, D as canViewDiscordGuildChannel, G as registerGateway, H as withValidatedDiscordProxy, I as createDiscordRestClient, K as unregisterGateway, V as validateDiscordProxyUrl } from "./send.shared-Xz7IMVpu.js";
7
+ import { a as isDiscordRateLimitResponseBody, o as summarizeDiscordResponseBody } from "./api--f62GFaS.js";
8
+ import { C as resolveTimestampMs, a as normalizeDiscordSlug, b as formatDiscordReactionEmoji, c as resolveDiscordChannelConfigWithFallback, d as resolveDiscordCommandOwnerAllowFrom, f as resolveDiscordGuildEntry, g as resolveDiscordOwnerAllowFrom, h as resolveDiscordOwnerAccess, i as normalizeDiscordDisplaySlug, l as resolveDiscordChannelPolicyCommandAuthorizer, m as resolveDiscordMemberAllowed, n as isDiscordGroupAllowedByPolicy, o as resolveDiscordAllowListMatch, p as resolveDiscordMemberAccessState, r as normalizeDiscordAllowList, v as resolveGroupDmAllow, x as formatDiscordUserTag, y as shouldEmitDiscordReactionNotification } from "./allow-list-B7qOH5xu.js";
9
+ import { s as sendVoiceMessageDiscord } from "./send-BFpa_DHn.js";
10
+ import { a as formatMention, t as sendMessageDiscord } from "./send.outbound-BlA6VzSE.js";
11
+ import { d as withAbortTimeout, l as raceWithTimeout } from "./timeouts-ZRc9Lw4y.js";
12
+ import { r as parseExecApprovalData } from "./approval-custom-id-NYPYj9Po.js";
11
13
  import { t as getDiscordRuntime } from "./runtime-DgnVQ7zW.js";
12
- import { a as getDiscordExecApprovalApprovers, s as isDiscordExecApprovalClientEnabled, t as resolveDiscordConversationIdentity } from "./conversation-identity-DWC5d8Sy.js";
13
- import { d as withAbortTimeout, l as raceWithTimeout } from "./timeouts-C5TBc_9x.js";
14
- import { t as resolveDiscordChannelAllowlist } from "./resolve-channels-CcpvWoRa.js";
15
- import { t as resolveDiscordUserAllowlist } from "./resolve-users-DJamun9s.js";
16
- import { _ as formatThreadBindingDurationLabel, a as isThreadArchived, d as resolveDiscordChannelNameSafe, f as resolveDiscordChannelParentIdSafe, l as resolveDiscordChannelIdSafe, m as resolveDiscordChannelTopicSafe, p as resolveDiscordChannelParentSafe, u as resolveDiscordChannelInfoSafe } from "./thread-bindings.discord-api-BjIf9lGs.js";
17
- import { r as parseApplicationIdFromToken, t as fetchDiscordApplicationId } from "./probe-D1ImJEo2.js";
18
- import { d as resolveDiscordMessageChannelId, n as resolveDiscordForwardedMessagesTextFromSnapshots, t as resolveDiscordEmbedText, u as resolveDiscordChannelInfo } from "./message-utils-B_3yA5bN.js";
19
- import { t as resolveDiscordSenderIdentity } from "./sender-identity-CEsfDF0L.js";
20
- import "./approval-handler.runtime-Bw2pZNk1.js";
14
+ import { a as isDiscordExecApprovalClientEnabled, r as getDiscordExecApprovalApprovers, t as resolveDiscordConversationIdentity } from "./conversation-identity-BpkAzklg.js";
15
+ import { t as resolveDiscordChannelAllowlist } from "./resolve-channels-BrkWOKdG.js";
16
+ import { t as resolveDiscordUserAllowlist } from "./resolve-users-OjrXXvJP.js";
17
+ import { _ as formatThreadBindingDurationLabel, a as isThreadArchived, d as resolveDiscordChannelNameSafe, f as resolveDiscordChannelParentIdSafe, l as resolveDiscordChannelIdSafe, m as resolveDiscordChannelTopicSafe, p as resolveDiscordChannelParentSafe, u as resolveDiscordChannelInfoSafe } from "./thread-bindings.discord-api-DJD3qwIN.js";
18
+ import { r as parseApplicationIdFromToken, t as fetchDiscordApplicationId } from "./probe-DxjMBeVG.js";
19
+ import { d as resolveDiscordMessageChannelId, n as resolveDiscordForwardedMessagesTextFromSnapshots, t as resolveDiscordEmbedText, u as resolveDiscordChannelInfo } from "./message-utils-D4j07taS.js";
20
+ import { t as resolveDiscordSenderIdentity } from "./sender-identity-C62lBe18.js";
21
+ import { n as formatDiscordApprovalDisplayValue, t as DISCORD_APPROVAL_ALLOWED_MENTIONS } from "./approval-message-safety-C-8BL6Kv.js";
21
22
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
22
23
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
23
24
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeOptionalStringifiedId, normalizeStringEntries, normalizeStringEntriesLower, summarizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -28,6 +29,7 @@ import { MAX_DATE_TIMESTAMP_MS, asDateTimestampMs, parseFiniteNumber, parseStric
28
29
  import { createHash, randomUUID } from "node:crypto";
29
30
  import path from "node:path";
30
31
  import { resolvePinnedMainDmOwnerFromAllowlist as resolvePinnedMainDmOwnerFromAllowlist$1 } from "openclaw/plugin-sdk/security-runtime";
32
+ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
31
33
  import { Type } from "typebox";
32
34
  import { Check, Errors } from "typebox/value";
33
35
  import { inspect } from "node:util";
@@ -54,9 +56,10 @@ import { clearExpiredCooldowns, ensureAuthProfileStore, formatReasoningMessage,
54
56
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
55
57
  import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
56
58
  import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
59
+ import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
57
60
  import { enqueueSystemEvent, enqueueSystemEvent as enqueueSystemEvent$1 } from "openclaw/plugin-sdk/system-event-runtime";
58
61
  import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
59
- import { applyModelOverrideToSessionEntry, resolveChannelModelOverride } from "openclaw/plugin-sdk/model-session-runtime";
62
+ import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolveChannelModelOverride } from "openclaw/plugin-sdk/model-session-runtime";
60
63
  import { completeWithPreparedSimpleCompletionModel, extractAssistantText, prepareSimpleCompletionModelForAgent } from "openclaw/plugin-sdk/simple-completion-runtime";
61
64
  import { getSessionEntry, listSessionEntries, patchSessionEntry, readSessionUpdatedAt as readSessionUpdatedAt$1, resolveStorePath, resolveStorePath as resolveStorePath$1 } from "openclaw/plugin-sdk/session-store-runtime";
62
65
  import { formatInboundEnvelope, resolveEnvelopeFormatOptions, runChannelInboundEvent } from "openclaw/plugin-sdk/channel-inbound";
@@ -79,7 +82,8 @@ import { stripPlainTextToolCallBlocks } from "openclaw/plugin-sdk/tool-payload";
79
82
  import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
80
83
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
81
84
  import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
82
- import { Agent as Agent$1, fetch as fetch$1 } from "undici";
85
+ import { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/runtime-fetch";
86
+ import { Agent as Agent$1 } from "undici";
83
87
  //#region extensions/discord/src/monitor/listeners.queue.ts
84
88
  const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 3e4;
85
89
  const discordEventQueueLog = createSubsystemLogger("discord/event-queue");
@@ -1216,6 +1220,91 @@ async function handleDiscordReactionEvent(params) {
1216
1220
  }
1217
1221
  }
1218
1222
  //#endregion
1223
+ //#region extensions/discord/src/monitor/presence-events.ts
1224
+ const DISCORD_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
1225
+ function isDiscordOnlineStatus(status) {
1226
+ return status === "online" || status === "idle" || status === "dnd";
1227
+ }
1228
+ function isDiscordOfflineStatus(status) {
1229
+ return status === "offline";
1230
+ }
1231
+ function resolveDiscordOnlinePresenceEvent(params) {
1232
+ const config = params.config;
1233
+ const userId = params.data.user?.id?.trim();
1234
+ if (!config || config.enabled === false || !userId || userId === params.botUserId || params.data.user.bot === true || !isDiscordOnlineStatus(params.data.status) || !params.availabilityKind) return null;
1235
+ if (config.users !== void 0 && !config.users.includes(userId)) return null;
1236
+ if (params.lastEmittedAtMs !== void 0 && params.nowMs - params.lastEmittedAtMs < 288e5) return null;
1237
+ const lines = [
1238
+ "Discord online-presence event:",
1239
+ params.availabilityKind === "observed-offline" ? `A human member became available online after being observed offline in guild_id=${JSON.stringify(params.data.guild_id)} user_id=${JSON.stringify(userId)} status=${JSON.stringify(params.data.status)}.` : `A human member became newly visible online since the current guild snapshot in guild_id=${JSON.stringify(params.data.guild_id)} user_id=${JSON.stringify(userId)} status=${JSON.stringify(params.data.status)}. They may have come online or joined after the snapshot; do not claim an exact prior status.`,
1240
+ `The authorized greeting target is channel_id=${JSON.stringify(config.channelId)}.`,
1241
+ "Before greeting, retrieve relevant memory and wiki context for this immutable user_id, including a known timezone when available. Use their local time for the greeting; if their timezone is unknown, do not guess.",
1242
+ "Send at most one short, natural greeting to the target channel. Do not reveal private memory. If no greeting is appropriate, stay silent."
1243
+ ];
1244
+ return {
1245
+ channelId: config.channelId,
1246
+ userId,
1247
+ text: lines.join("\n")
1248
+ };
1249
+ }
1250
+ //#endregion
1251
+ //#region extensions/discord/src/monitor/presence-cooldown-store.ts
1252
+ const DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
1253
+ function openDiscordPresenceCooldownStore() {
1254
+ return getDiscordRuntime().state.openSyncKeyedStore({
1255
+ namespace: "presence-greeting-cooldowns",
1256
+ maxEntries: DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES,
1257
+ overflowPolicy: "reject-new",
1258
+ defaultTtlMs: DISCORD_PRESENCE_GREETING_COOLDOWN_MS
1259
+ });
1260
+ }
1261
+ //#endregion
1262
+ //#region extensions/discord/src/monitor/presence-transition-cache.ts
1263
+ const DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES = 75e3;
1264
+ var DiscordPresenceBaselineCache = class {
1265
+ constructor(maxEntries = DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES) {
1266
+ this.maxEntries = maxEntries;
1267
+ this.offlineByKey = /* @__PURE__ */ new Map();
1268
+ this.onlineByKey = /* @__PURE__ */ new Map();
1269
+ }
1270
+ clear() {
1271
+ this.offlineByKey.clear();
1272
+ this.onlineByKey.clear();
1273
+ }
1274
+ clearScope(scope) {
1275
+ for (const markers of [this.offlineByKey, this.onlineByKey]) for (const [key, markerScope] of markers) if (markerScope === scope) markers.delete(key);
1276
+ }
1277
+ isOffline(scope, key) {
1278
+ return this.offlineByKey.get(key) === scope;
1279
+ }
1280
+ isOnline(scope, key) {
1281
+ return this.onlineByKey.get(key) === scope;
1282
+ }
1283
+ observeOffline(scope, key) {
1284
+ this.deleteMarker(this.onlineByKey, scope, key);
1285
+ return this.observe(this.offlineByKey, scope, key);
1286
+ }
1287
+ observeOnline(scope, key) {
1288
+ this.deleteMarker(this.offlineByKey, scope, key);
1289
+ return this.observe(this.onlineByKey, scope, key);
1290
+ }
1291
+ deleteMarker(markers, scope, key) {
1292
+ if (markers.get(key) === scope) markers.delete(key);
1293
+ }
1294
+ observe(markers, scope, key) {
1295
+ markers.delete(key);
1296
+ markers.set(key, scope);
1297
+ let evictedScope;
1298
+ while (markers.size > this.maxEntries) {
1299
+ const oldestKey = markers.keys().next().value;
1300
+ if (oldestKey === void 0) break;
1301
+ evictedScope = markers.get(oldestKey);
1302
+ markers.delete(oldestKey);
1303
+ }
1304
+ return evictedScope;
1305
+ }
1306
+ };
1307
+ //#endregion
1219
1308
  //#region extensions/discord/src/monitor/thread-session-close.ts
1220
1309
  /**
1221
1310
  * Marks every session entry in the store whose key contains {@link threadId}
@@ -1294,18 +1383,194 @@ var DiscordInteractionListener = class extends InteractionCreateListener {
1294
1383
  var DiscordPresenceListener = class extends PresenceUpdateListener {
1295
1384
  constructor(params) {
1296
1385
  super();
1297
- this.logger = params.logger;
1298
- this.accountId = params.accountId;
1386
+ this.params = params;
1387
+ this.pendingByGuildUser = /* @__PURE__ */ new Map();
1388
+ this.guildPresenceState = /* @__PURE__ */ new Map();
1389
+ this.gatewayGeneration = 0;
1390
+ this.cooldownStore = params.cooldownStore ?? openDiscordPresenceCooldownStore();
1391
+ this.presenceBaseline = params.presenceBaseline ?? new DiscordPresenceBaselineCache();
1392
+ }
1393
+ seedGuildSnapshot(data) {
1394
+ const config = resolveDiscordGuildEntry({
1395
+ guildId: data.id,
1396
+ guildEntries: this.params.guildEntries
1397
+ })?.presenceEvents;
1398
+ if (!config || config.enabled === false) return;
1399
+ const keyPrefix = `${this.params.accountId}:${data.id}:`;
1400
+ const generation = (this.guildPresenceState.get(data.id)?.generation ?? 0) + 1;
1401
+ this.guildPresenceState.set(data.id, {
1402
+ generation,
1403
+ inferUnknownAsNewlyAvailable: false
1404
+ });
1405
+ this.detachPendingPrefix(keyPrefix);
1406
+ this.presenceBaseline.clearScope(data.id);
1407
+ if (data.unavailable === true || !("presences" in data) || !Array.isArray(data.presences)) return;
1408
+ this.guildPresenceState.set(data.id, {
1409
+ generation,
1410
+ inferUnknownAsNewlyAvailable: data.member_count <= 75e3
1411
+ });
1412
+ for (const presence of data.presences) {
1413
+ const userId = presence.user?.id;
1414
+ if (!userId || config.users !== void 0 && !config.users.includes(userId)) continue;
1415
+ const key = `${keyPrefix}${userId}`;
1416
+ if (isDiscordOfflineStatus(presence.status)) this.recordPresenceBaseline(data.id, key, "offline");
1417
+ else if (isDiscordOnlineStatus(presence.status)) this.recordPresenceBaseline(data.id, key, "online");
1418
+ }
1299
1419
  }
1300
- async handle(data) {
1420
+ async handle(data, client) {
1421
+ const userId = data.user?.id;
1422
+ if (!userId) return;
1423
+ setPresence(this.params.accountId, userId, data);
1424
+ const presenceKey = `${this.params.accountId}:${data.guild_id}:${userId}`;
1425
+ const gatewayGeneration = this.gatewayGeneration;
1426
+ const guildGeneration = this.guildPresenceState.get(data.guild_id)?.generation ?? 0;
1427
+ const run = (this.pendingByGuildUser.get(presenceKey) ?? Promise.resolve()).then(() => this.handleSerial(data, client, userId, presenceKey, gatewayGeneration, guildGeneration), () => this.handleSerial(data, client, userId, presenceKey, gatewayGeneration, guildGeneration));
1428
+ this.pendingByGuildUser.set(presenceKey, run);
1301
1429
  try {
1302
- const userId = "user" in data && data.user && typeof data.user === "object" && "id" in data.user ? data.user.id : void 0;
1303
- if (!userId) return;
1304
- setPresence(this.accountId, userId, data);
1430
+ await run;
1305
1431
  } catch (err) {
1306
- (this.logger ?? discordEventQueueLog).error(danger(`discord presence handler failed: ${String(err)}`));
1432
+ (this.params.logger ?? discordEventQueueLog).error(danger(`discord presence handler failed: ${String(err)}`));
1433
+ } finally {
1434
+ if (this.pendingByGuildUser.get(presenceKey) === run) this.pendingByGuildUser.delete(presenceKey);
1307
1435
  }
1308
1436
  }
1437
+ resetGatewaySession() {
1438
+ this.gatewayGeneration += 1;
1439
+ this.presenceBaseline.clear();
1440
+ this.guildPresenceState.clear();
1441
+ this.pendingByGuildUser.clear();
1442
+ clearPresences(this.params.accountId);
1443
+ }
1444
+ invalidateGuild(guildId) {
1445
+ const keyPrefix = `${this.params.accountId}:${guildId}:`;
1446
+ const generation = (this.guildPresenceState.get(guildId)?.generation ?? 0) + 1;
1447
+ this.guildPresenceState.set(guildId, {
1448
+ generation,
1449
+ inferUnknownAsNewlyAvailable: false
1450
+ });
1451
+ this.presenceBaseline.clearScope(guildId);
1452
+ this.detachPendingPrefix(keyPrefix);
1453
+ }
1454
+ detachPendingPrefix(prefix) {
1455
+ for (const key of this.pendingByGuildUser.keys()) if (key.startsWith(prefix)) this.pendingByGuildUser.delete(key);
1456
+ }
1457
+ async handleSerial(data, client, userId, presenceKey, gatewayGeneration, guildGeneration) {
1458
+ if (!this.isCurrentGeneration(data.guild_id, gatewayGeneration, guildGeneration)) return;
1459
+ const config = resolveDiscordGuildEntry({
1460
+ guildId: data.guild_id,
1461
+ guildEntries: this.params.guildEntries
1462
+ })?.presenceEvents;
1463
+ if (!config || config.enabled === false) return;
1464
+ if (config.users !== void 0 && !config.users.includes(userId)) return;
1465
+ const nowMs = this.params.nowMs?.() ?? Date.now();
1466
+ const presenceScope = data.guild_id;
1467
+ const presenceEvent = resolveDiscordOnlinePresenceEvent({
1468
+ config,
1469
+ data,
1470
+ availabilityKind: this.presenceBaseline.isOffline(presenceScope, presenceKey) ? "observed-offline" : this.guildPresenceState.get(data.guild_id)?.inferUnknownAsNewlyAvailable === true && !this.presenceBaseline.isOnline(presenceScope, presenceKey) ? "first-seen-after-snapshot" : null,
1471
+ botUserId: this.params.botUserId,
1472
+ nowMs,
1473
+ lastEmittedAtMs: this.cooldownStore.lookup(presenceKey)
1474
+ });
1475
+ if (!presenceEvent) {
1476
+ if (isDiscordOfflineStatus(data.status)) this.recordPresenceBaseline(data.guild_id, presenceKey, "offline");
1477
+ else if (isDiscordOnlineStatus(data.status)) this.recordPresenceBaseline(data.guild_id, presenceKey, "online");
1478
+ return;
1479
+ }
1480
+ const fetchedUserIsBot = data.user.bot === void 0 && (await client.fetchUser(userId)).bot === true;
1481
+ if (!this.isCurrentGeneration(data.guild_id, gatewayGeneration, guildGeneration)) return;
1482
+ if (fetchedUserIsBot) {
1483
+ this.recordPresenceBaseline(data.guild_id, presenceKey, "online");
1484
+ return;
1485
+ }
1486
+ const route = resolveAgentRoute({
1487
+ cfg: this.params.cfg,
1488
+ channel: "discord",
1489
+ accountId: this.params.accountId,
1490
+ guildId: data.guild_id,
1491
+ peer: {
1492
+ kind: "channel",
1493
+ id: presenceEvent.channelId
1494
+ }
1495
+ });
1496
+ try {
1497
+ if (!this.cooldownStore.registerIfAbsent(presenceKey, nowMs, { ttlMs: 288e5 })) {
1498
+ this.recordPresenceBaseline(data.guild_id, presenceKey, "online");
1499
+ return;
1500
+ }
1501
+ } catch (err) {
1502
+ (this.params.logger ?? discordEventQueueLog).warn(danger(`discord presence cooldown persistence failed: ${String(err)}`));
1503
+ return;
1504
+ }
1505
+ let queued;
1506
+ try {
1507
+ queued = enqueueSystemEvent(presenceEvent.text, {
1508
+ sessionKey: route.sessionKey,
1509
+ contextKey: `discord:presence-online:${this.params.accountId}:${data.guild_id}:${userId}`,
1510
+ deliveryContext: {
1511
+ channel: "discord",
1512
+ to: `channel:${presenceEvent.channelId}`,
1513
+ accountId: this.params.accountId
1514
+ }
1515
+ });
1516
+ } catch (err) {
1517
+ if (this.cooldownStore.lookup(presenceKey) === nowMs) this.cooldownStore.delete(presenceKey);
1518
+ throw err;
1519
+ }
1520
+ if (!queued) {
1521
+ if (this.cooldownStore.lookup(presenceKey) === nowMs) this.cooldownStore.delete(presenceKey);
1522
+ return;
1523
+ }
1524
+ this.recordPresenceBaseline(data.guild_id, presenceKey, "online");
1525
+ requestHeartbeat({
1526
+ source: "notifications-event",
1527
+ intent: "immediate",
1528
+ reason: "wake",
1529
+ agentId: route.agentId,
1530
+ sessionKey: route.sessionKey,
1531
+ heartbeat: {
1532
+ target: "discord",
1533
+ to: `channel:${presenceEvent.channelId}`,
1534
+ accountId: this.params.accountId
1535
+ }
1536
+ });
1537
+ }
1538
+ isCurrentGeneration(guildId, gatewayGeneration, guildGeneration) {
1539
+ return gatewayGeneration === this.gatewayGeneration && guildGeneration === (this.guildPresenceState.get(guildId)?.generation ?? 0);
1540
+ }
1541
+ recordPresenceBaseline(guildId, key, status) {
1542
+ const evictedGuildId = status === "offline" ? this.presenceBaseline.observeOffline(guildId, key) : this.presenceBaseline.observeOnline(guildId, key);
1543
+ if (!evictedGuildId) return;
1544
+ const state = this.guildPresenceState.get(evictedGuildId);
1545
+ if (state) state.inferUnknownAsNewlyAvailable = false;
1546
+ }
1547
+ };
1548
+ var DiscordPresenceGuildCreateListener = class extends GuildCreateListener {
1549
+ constructor(presenceListener) {
1550
+ super();
1551
+ this.presenceListener = presenceListener;
1552
+ }
1553
+ handle(data) {
1554
+ this.presenceListener.seedGuildSnapshot(data);
1555
+ }
1556
+ };
1557
+ var DiscordPresenceGuildDeleteListener = class extends GuildDeleteListener {
1558
+ constructor(presenceListener) {
1559
+ super();
1560
+ this.presenceListener = presenceListener;
1561
+ }
1562
+ handle(data) {
1563
+ this.presenceListener.invalidateGuild(data.id);
1564
+ }
1565
+ };
1566
+ var DiscordPresenceReadyListener = class extends ReadyListener {
1567
+ constructor(presenceListener) {
1568
+ super();
1569
+ this.presenceListener = presenceListener;
1570
+ }
1571
+ handle() {
1572
+ this.presenceListener.resetGatewaySession();
1573
+ }
1309
1574
  };
1310
1575
  var DiscordThreadUpdateListener = class extends ThreadUpdateListener {
1311
1576
  constructor(cfg, accountId, logger) {
@@ -1754,27 +2019,6 @@ function resolveDiscordNativeCommandChannelAccessContext(params) {
1754
2019
  }) : null
1755
2020
  };
1756
2021
  }
1757
- function resolveDiscordCommandOwnerAllowFrom(cfg) {
1758
- const raw = cfg.commands?.ownerAllowFrom;
1759
- if (!Array.isArray(raw) || raw.length === 0) return;
1760
- const entries = [];
1761
- for (const entry of raw) {
1762
- const trimmed = normalizeOptionalString(String(entry ?? "")) ?? "";
1763
- if (!trimmed) continue;
1764
- const separatorIndex = trimmed.indexOf(":");
1765
- if (separatorIndex > 0) {
1766
- const prefix = trimmed.slice(0, separatorIndex).toLowerCase();
1767
- if (prefix === "discord") {
1768
- const remainder = normalizeOptionalString(trimmed.slice(separatorIndex + 1)) ?? "";
1769
- if (remainder) entries.push(remainder);
1770
- continue;
1771
- }
1772
- if (prefix !== "user" && prefix !== "pk") continue;
1773
- }
1774
- entries.push(trimmed);
1775
- }
1776
- return entries.length > 0 ? entries : void 0;
1777
- }
1778
2022
  async function resolveDiscordGuildNativeCommandAuthorized(params) {
1779
2023
  const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
1780
2024
  providerConfigPresent: params.cfg.channels?.discord !== void 0,
@@ -2121,22 +2365,12 @@ const DISCORD_COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg";
2121
2365
  function createCommandArgsWithValue(params) {
2122
2366
  return { values: { [params.argName]: params.value } };
2123
2367
  }
2124
- function encodeDiscordCommandArgValue(value) {
2125
- return encodeURIComponent(value);
2126
- }
2127
- function decodeDiscordCommandArgValue(value) {
2128
- try {
2129
- return decodeURIComponent(value);
2130
- } catch {
2131
- return value;
2132
- }
2133
- }
2134
2368
  function buildDiscordCommandArgCustomId(params) {
2135
2369
  return [
2136
- `${DISCORD_COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeDiscordCommandArgValue(params.command)}`,
2137
- `arg=${encodeDiscordCommandArgValue(params.arg)}`,
2138
- `value=${encodeDiscordCommandArgValue(params.value)}`,
2139
- `user=${encodeDiscordCommandArgValue(params.userId)}`
2370
+ `${DISCORD_COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeCustomIdComponent(params.command)}`,
2371
+ `arg=${encodeCustomIdComponent(params.arg)}`,
2372
+ `value=${encodeCustomIdComponent(params.value)}`,
2373
+ `user=${encodeCustomIdComponent(params.userId)}`
2140
2374
  ].join(";");
2141
2375
  }
2142
2376
  function parseDiscordCommandArgData(data) {
@@ -2148,10 +2382,10 @@ function parseDiscordCommandArgData(data) {
2148
2382
  const rawUser = coerce(data.user);
2149
2383
  if (!rawCommand || !rawArg || !rawValue || !rawUser) return null;
2150
2384
  return {
2151
- command: decodeDiscordCommandArgValue(rawCommand),
2152
- arg: decodeDiscordCommandArgValue(rawArg),
2153
- value: decodeDiscordCommandArgValue(rawValue),
2154
- userId: decodeDiscordCommandArgValue(rawUser)
2385
+ command: decodeCustomIdComponent(rawCommand),
2386
+ arg: decodeCustomIdComponent(rawArg),
2387
+ value: decodeCustomIdComponent(rawValue),
2388
+ userId: decodeCustomIdComponent(rawUser)
2155
2389
  };
2156
2390
  }
2157
2391
  async function handleDiscordCommandArgInteraction(params) {
@@ -2384,6 +2618,10 @@ async function recordDiscordModelPickerRecentModel(params) {
2384
2618
  //#endregion
2385
2619
  //#region extensions/discord/src/monitor/model-picker.state.ts
2386
2620
  const DISCORD_MODEL_PICKER_CUSTOM_ID_KEY = "mdlpk";
2621
+ const DISCORD_CUSTOM_ID_MAX_CHARS = 100;
2622
+ const DISCORD_COMPONENT_MAX_SELECT_OPTIONS = 25;
2623
+ const DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2624
+ const DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2387
2625
  function compareBucketItems(left, right) {
2388
2626
  const normalized = left.toLowerCase().localeCompare(right.toLowerCase());
2389
2627
  return normalized === 0 ? left.localeCompare(right) : normalized;
@@ -2408,17 +2646,19 @@ const PICKER_VIEWS = [
2408
2646
  "models",
2409
2647
  "recents"
2410
2648
  ];
2411
- const loadModelsProviderRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/models-provider-runtime"));
2412
- function encodeCustomIdValue(value) {
2413
- return encodeURIComponent(value);
2414
- }
2415
- function decodeCustomIdValue$1(value) {
2416
- try {
2417
- return decodeURIComponent(value);
2418
- } catch {
2419
- return value;
2420
- }
2649
+ /**
2650
+ * Alpha buckets engage only when the sorted item list exceeds the single-page
2651
+ * select cap. Below this threshold the user gets the existing flat list +
2652
+ * prev/next behavior unchanged.
2653
+ */
2654
+ const DISCORD_MODEL_PICKER_BUCKET_THRESHOLD = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2655
+ /** Target items per alpha bucket. Discord caps selects at 25 options. */
2656
+ const DISCORD_MODEL_PICKER_BUCKET_TARGET_SIZE = 20;
2657
+ const DISCORD_MODEL_PICKER_MODEL_TOKEN_PATTERN = /^[A-Za-z0-9_-]{8}$/u;
2658
+ function createDiscordModelPickerModelToken(provider, model) {
2659
+ return createHash("sha256").update(JSON.stringify([normalizeProviderId(provider), model]), "utf8").digest("base64url").slice(0, 8);
2421
2660
  }
2661
+ const loadModelsProviderRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/models-provider-runtime"));
2422
2662
  function isValidCommandContext(value) {
2423
2663
  return COMMAND_CONTEXTS.includes(value);
2424
2664
  }
@@ -2479,44 +2719,51 @@ function buildDiscordModelPickerCustomId(params) {
2479
2719
  const normalizedProvider = params.provider ? normalizeProviderId(params.provider) : void 0;
2480
2720
  const modelIndex = typeof params.modelIndex === "number" && Number.isFinite(params.modelIndex) ? Math.max(1, Math.floor(params.modelIndex)) : void 0;
2481
2721
  const recentSlot = typeof params.recentSlot === "number" && Number.isFinite(params.recentSlot) ? Math.max(1, Math.floor(params.recentSlot)) : void 0;
2722
+ const modelToken = params.modelToken?.trim();
2723
+ if (modelToken && !DISCORD_MODEL_PICKER_MODEL_TOKEN_PATTERN.test(modelToken)) throw new Error("Discord model picker model token is invalid");
2482
2724
  const parts = [
2483
- `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:c=${encodeCustomIdValue(params.command)}`,
2484
- `a=${encodeCustomIdValue(params.action)}`,
2485
- `v=${encodeCustomIdValue(params.view)}`,
2486
- `u=${encodeCustomIdValue(userId)}`,
2725
+ `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:c=${encodeCustomIdComponent(params.command)}`,
2726
+ `a=${encodeCustomIdComponent(params.action)}`,
2727
+ `v=${encodeCustomIdComponent(params.view)}`,
2728
+ `u=${encodeCustomIdComponent(userId)}`,
2487
2729
  `g=${String(page)}`
2488
2730
  ];
2489
- if (normalizedProvider) parts.push(`p=${encodeCustomIdValue(normalizedProvider)}`);
2731
+ if (normalizedProvider) parts.push(`p=${encodeCustomIdComponent(normalizedProvider)}`);
2490
2732
  const runtime = params.runtime?.trim();
2491
- if (runtime) parts.push(`r=${encodeCustomIdValue(runtime)}`);
2733
+ if (runtime) parts.push(`r=${encodeCustomIdComponent(runtime)}`);
2492
2734
  const runtimeIndex = typeof params.runtimeIndex === "number" && Number.isFinite(params.runtimeIndex) ? Math.max(1, Math.floor(params.runtimeIndex)) : void 0;
2493
2735
  if (runtimeIndex) parts.push(`ri=${String(runtimeIndex)}`);
2494
2736
  if (providerPage) parts.push(`pp=${String(providerPage)}`);
2495
- if (modelIndex) parts.push(`mi=${String(modelIndex)}`);
2496
- if (recentSlot) parts.push(`rs=${String(recentSlot)}`);
2737
+ if (modelToken) parts.push(`m=${modelToken}`);
2738
+ else {
2739
+ if (modelIndex) parts.push(`mi=${String(modelIndex)}`);
2740
+ if (recentSlot) parts.push(`rs=${String(recentSlot)}`);
2741
+ }
2497
2742
  const providerBucket = params.providerBucket?.trim().toLowerCase();
2498
- if (providerBucket) parts.push(`pb=${encodeCustomIdValue(providerBucket)}`);
2743
+ if (providerBucket) parts.push(`pb=${encodeCustomIdComponent(providerBucket)}`);
2499
2744
  const modelBucket = params.modelBucket?.trim().toLowerCase();
2500
- if (modelBucket) parts.push(`mb=${encodeCustomIdValue(modelBucket)}`);
2745
+ if (modelBucket) parts.push(`mb=${encodeCustomIdComponent(modelBucket)}`);
2501
2746
  const customId = parts.join(";");
2502
- if (customId.length > 100) throw new Error(`Discord model picker custom_id exceeds 100 chars (${customId.length})`);
2747
+ if (customId.length > DISCORD_CUSTOM_ID_MAX_CHARS) throw new Error(`Discord model picker custom_id exceeds ${DISCORD_CUSTOM_ID_MAX_CHARS} chars (${customId.length})`);
2503
2748
  return customId;
2504
2749
  }
2505
2750
  function parseDiscordModelPickerData(data) {
2506
2751
  if (!data || typeof data !== "object") return null;
2507
- const command = decodeCustomIdValue$1(coerceString(data.c ?? data.cmd));
2508
- const action = decodeCustomIdValue$1(coerceString(data.a ?? data.act));
2509
- const view = decodeCustomIdValue$1(coerceString(data.v ?? data.view));
2510
- const userId = decodeCustomIdValue$1(coerceString(data.u));
2511
- const providerRaw = decodeCustomIdValue$1(coerceString(data.p));
2512
- const runtimeRaw = decodeCustomIdValue$1(coerceString(data.r));
2752
+ const command = decodeCustomIdComponent(coerceString(data.c ?? data.cmd));
2753
+ const action = decodeCustomIdComponent(coerceString(data.a ?? data.act));
2754
+ const view = decodeCustomIdComponent(coerceString(data.v ?? data.view));
2755
+ const userId = decodeCustomIdComponent(coerceString(data.u));
2756
+ const providerRaw = decodeCustomIdComponent(coerceString(data.p));
2757
+ const runtimeRaw = decodeCustomIdComponent(coerceString(data.r));
2513
2758
  const runtimeIndex = parseRawPositiveInt(data.ri);
2514
2759
  const page = parseRawPage(data.g ?? data.pg);
2515
2760
  const providerPage = parseRawPositiveInt(data.pp);
2516
2761
  const modelIndex = parseRawPositiveInt(data.mi);
2762
+ const modelTokenRaw = coerceString(data.m).trim();
2763
+ const modelToken = DISCORD_MODEL_PICKER_MODEL_TOKEN_PATTERN.test(modelTokenRaw) ? modelTokenRaw : void 0;
2517
2764
  const recentSlot = parseRawPositiveInt(data.rs);
2518
- const providerBucketRaw = decodeCustomIdValue$1(coerceString(data.pb)).trim().toLowerCase();
2519
- const modelBucketRaw = decodeCustomIdValue$1(coerceString(data.mb)).trim().toLowerCase();
2765
+ const providerBucketRaw = decodeCustomIdComponent(coerceString(data.pb)).trim().toLowerCase();
2766
+ const modelBucketRaw = decodeCustomIdComponent(coerceString(data.mb)).trim().toLowerCase();
2520
2767
  if (!isValidCommandContext(command) || !isValidPickerAction(action) || !isValidPickerView(view)) return null;
2521
2768
  const trimmedUserId = userId.trim();
2522
2769
  if (!trimmedUserId) return null;
@@ -2531,6 +2778,7 @@ function parseDiscordModelPickerData(data) {
2531
2778
  page,
2532
2779
  ...typeof providerPage === "number" ? { providerPage } : {},
2533
2780
  ...typeof modelIndex === "number" ? { modelIndex } : {},
2781
+ ...modelToken ? { modelToken } : {},
2534
2782
  ...typeof recentSlot === "number" ? { recentSlot } : {},
2535
2783
  ...providerBucketRaw ? { providerBucket: providerBucketRaw } : {},
2536
2784
  ...modelBucketRaw ? { modelBucket: modelBucketRaw } : {}
@@ -2549,25 +2797,26 @@ function parseDiscordModelPickerData(data) {
2549
2797
  */
2550
2798
  function computeAlphaBuckets(sortedItems) {
2551
2799
  if (sortedItems.length === 0) return [];
2552
- if (sortedItems.length <= 25) return [{
2800
+ if (sortedItems.length <= DISCORD_MODEL_PICKER_BUCKET_THRESHOLD) return [{
2553
2801
  id: "all",
2554
2802
  label: `All (${sortedItems.length})`,
2555
2803
  start: 0,
2556
2804
  end: sortedItems.length
2557
2805
  }];
2558
2806
  const firstLetter = (value) => value.charAt(0).toLowerCase();
2559
- if (sortedItems.every((item) => firstLetter(item) === firstLetter(sortedItems[0]))) return chunkBucketsByCount(sortedItems);
2807
+ const firstItem = expectDefined(sortedItems.at(0), "non-empty sorted model picker items");
2808
+ if (sortedItems.every((item) => firstLetter(item) === firstLetter(firstItem))) return chunkBucketsByCount(sortedItems);
2560
2809
  const buckets = [];
2561
2810
  const target = computeBucketTargetSize(sortedItems.length);
2562
2811
  let start = 0;
2563
2812
  while (start < sortedItems.length) {
2564
2813
  let end = Math.min(sortedItems.length, start + target);
2565
2814
  if (end < sortedItems.length) {
2566
- const last = firstLetter(sortedItems[end - 1]);
2567
- while (end < sortedItems.length && firstLetter(sortedItems[end]) === last) end += 1;
2815
+ const last = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
2816
+ while (end < sortedItems.length && firstLetter(expectDefined(sortedItems[end], "bucket extension index")) === last) end += 1;
2568
2817
  }
2569
- const startLetter = firstLetter(sortedItems[start]);
2570
- const endLetter = firstLetter(sortedItems[end - 1]);
2818
+ const startLetter = firstLetter(expectDefined(sortedItems[start], "bucket start index"));
2819
+ const endLetter = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor"));
2571
2820
  const id = startLetter === endLetter ? startLetter : `${startLetter}-${endLetter}`;
2572
2821
  const label = startLetter === endLetter ? `${startLetter.toUpperCase()} (${end - start})` : `${startLetter.toUpperCase()}–${endLetter.toUpperCase()} (${end - start})`;
2573
2822
  buckets.push({
@@ -2587,8 +2836,8 @@ function computeAlphaBuckets(sortedItems) {
2587
2836
  * for typical inputs and grows linearly for very large lists.
2588
2837
  */
2589
2838
  function computeBucketTargetSize(totalItems) {
2590
- const minTarget = 20;
2591
- const capByBucketCount = Math.ceil(totalItems / 25);
2839
+ const minTarget = DISCORD_MODEL_PICKER_BUCKET_TARGET_SIZE;
2840
+ const capByBucketCount = Math.ceil(totalItems / DISCORD_COMPONENT_MAX_SELECT_OPTIONS);
2592
2841
  return Math.max(minTarget, capByBucketCount);
2593
2842
  }
2594
2843
  function chunkBucketsByCount(sortedItems) {
@@ -2613,8 +2862,8 @@ function chunkBucketsByCount(sortedItems) {
2613
2862
  */
2614
2863
  function resolveBucket(buckets, id) {
2615
2864
  if (buckets.length === 0) return null;
2616
- if (!id) return buckets[0];
2617
- return buckets.find((bucket) => bucket.id === id) ?? buckets[0];
2865
+ if (!id) return expectDefined(buckets.at(0), "non-empty model picker buckets");
2866
+ return buckets.find((bucket) => bucket.id === id) ?? expectDefined(buckets.at(0), "non-empty model picker buckets");
2618
2867
  }
2619
2868
  /**
2620
2869
  * Derive the alpha-bucket id that contains a given provider id. Returns
@@ -2634,7 +2883,7 @@ function findProviderBucketLocation(data, provider) {
2634
2883
  if (idx < 0) return;
2635
2884
  const containing = computeAlphaBuckets(sorted).find((bucket) => idx >= bucket.start && idx < bucket.end);
2636
2885
  if (!containing) return;
2637
- const page = Math.floor((idx - containing.start) / 25) + 1;
2886
+ const page = Math.floor((idx - containing.start) / DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE) + 1;
2638
2887
  return {
2639
2888
  ...containing.id !== "all" ? { bucket: containing.id } : {},
2640
2889
  page
@@ -2666,7 +2915,7 @@ function getDiscordModelPickerProviderPage(params) {
2666
2915
  const buckets = computeAlphaBuckets(allItems.map((item) => item.id));
2667
2916
  const bucket = resolveBucket(buckets, params.bucket);
2668
2917
  const bucketItems = bucket ? allItems.slice(bucket.start, bucket.end) : allItems;
2669
- const pageSize = clampPageSize(params.pageSize, 25, 25);
2918
+ const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE, DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE);
2670
2919
  return {
2671
2920
  ...paginateItems({
2672
2921
  items: bucketItems,
@@ -2685,7 +2934,7 @@ function getDiscordModelPickerModelPage(params) {
2685
2934
  const buckets = computeAlphaBuckets(allModels);
2686
2935
  const bucket = resolveBucket(buckets, params.bucket);
2687
2936
  const bucketItems = bucket ? allModels.slice(bucket.start, bucket.end) : allModels;
2688
- const pageSize = clampPageSize(params.pageSize, 25, 25);
2937
+ const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
2689
2938
  return {
2690
2939
  ...paginateItems({
2691
2940
  items: bucketItems,
@@ -2704,7 +2953,7 @@ function resolveDiscordModelPickerPageForModel(params) {
2704
2953
  const sorted = [...modelSet].toSorted(compareBucketItems);
2705
2954
  const index = sorted.indexOf(params.model);
2706
2955
  if (index < 0) return { page: 1 };
2707
- const pageSize = clampPageSize(params.pageSize, 25, 25);
2956
+ const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
2708
2957
  const containingBucket = computeAlphaBuckets(sorted).find((bucket) => index >= bucket.start && index < bucket.end);
2709
2958
  if (!containingBucket) return { page: Math.floor(index / pageSize) + 1 };
2710
2959
  const offsetInBucket = index - containingBucket.start;
@@ -2719,8 +2968,10 @@ const DISCORD_MODEL_PICKER_PAGE_INDICATOR_CUSTOM_ID = "mdlpk:nav-indicator";
2719
2968
  function parseCurrentModelRef(raw) {
2720
2969
  const match = (raw?.trim())?.match(/^([^/]+)\/(.+)$/u);
2721
2970
  if (!match) return null;
2722
- const provider = normalizeProviderId(match[1]);
2971
+ const providerText = match[1];
2723
2972
  const model = match[2];
2973
+ if (providerText === void 0 || model === void 0) return null;
2974
+ const provider = normalizeProviderId(providerText);
2724
2975
  if (!provider || !model) return null;
2725
2976
  return {
2726
2977
  provider,
@@ -2904,6 +3155,7 @@ function buildPaginationRow(params) {
2904
3155
  page: Math.max(1, params.page - 1),
2905
3156
  providerPage: params.providerPage,
2906
3157
  modelIndex: params.modelIndex,
3158
+ modelToken: params.modelToken,
2907
3159
  providerBucket: params.providerBucket,
2908
3160
  modelBucket: params.modelBucket,
2909
3161
  userId: params.userId
@@ -2929,6 +3181,7 @@ function buildPaginationRow(params) {
2929
3181
  page: Math.min(params.totalPages, params.page + 1),
2930
3182
  providerPage: params.providerPage,
2931
3183
  modelIndex: params.modelIndex,
3184
+ modelToken: params.modelToken,
2932
3185
  providerBucket: params.providerBucket,
2933
3186
  modelBucket: params.modelBucket,
2934
3187
  userId: params.userId
@@ -2939,6 +3192,7 @@ function buildPaginationRow(params) {
2939
3192
  function buildModelRows(params) {
2940
3193
  const parsedCurrentModel = parseCurrentModelRef(params.currentModel);
2941
3194
  const parsedPendingModel = parseCurrentModelRef(params.pendingModel);
3195
+ const pendingModelToken = parsedPendingModel ? createDiscordModelPickerModelToken(parsedPendingModel.provider, parsedPendingModel.model) : void 0;
2942
3196
  const rows = [];
2943
3197
  const hasQuickModels = (params.quickModels ?? []).length > 0;
2944
3198
  const providerPage = getDiscordModelPickerProviderPage({
@@ -2991,6 +3245,7 @@ function buildModelRows(params) {
2991
3245
  page: params.modelPage.page,
2992
3246
  providerPage: providerPage.page,
2993
3247
  modelIndex: params.pendingModelIndex,
3248
+ modelToken: pendingModelToken,
2994
3249
  ...params.pendingModelIndex === void 0 && activeModelBucket ? { modelBucket: activeModelBucket } : {},
2995
3250
  userId: params.userId
2996
3251
  }),
@@ -3037,6 +3292,7 @@ function buildModelRows(params) {
3037
3292
  ...compactRuntime,
3038
3293
  providerPage: providerPage.page,
3039
3294
  modelIndex: params.pendingModelIndex,
3295
+ modelToken: pendingModelToken,
3040
3296
  modelBucket: params.modelPage.bucket && params.modelPage.bucket.id !== "all" ? params.modelPage.bucket.id : void 0
3041
3297
  });
3042
3298
  if (modelNavRow) rows.push(modelNavRow);
@@ -3114,6 +3370,7 @@ function buildModelRows(params) {
3114
3370
  page: params.modelPage.page,
3115
3371
  providerPage: providerPage.page,
3116
3372
  modelIndex: params.pendingModelIndex,
3373
+ modelToken: pendingModelToken,
3117
3374
  userId: params.userId
3118
3375
  })
3119
3376
  }));
@@ -3254,6 +3511,10 @@ function formatRecentsButtonLabel(modelRef, suffix) {
3254
3511
  if (label.length <= maxLen) return label;
3255
3512
  return suffix ? `${sliceUtf16Safe(modelRef, 0, maxLen - suffix.length - 2)}… ${suffix}` : `${sliceUtf16Safe(modelRef, 0, maxLen - 1)}…`;
3256
3513
  }
3514
+ function createModelRefToken(modelRef) {
3515
+ const parsed = parseCurrentModelRef(modelRef);
3516
+ return parsed ? createDiscordModelPickerModelToken(parsed.provider, parsed.model) : void 0;
3517
+ }
3257
3518
  function renderDiscordModelPickerRecentsView(params) {
3258
3519
  const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
3259
3520
  const rows = [];
@@ -3266,6 +3527,24 @@ function renderDiscordModelPickerRecentsView(params) {
3266
3527
  action: "submit",
3267
3528
  view: "recents",
3268
3529
  recentSlot: 1,
3530
+ modelToken: createModelRefToken(defaultModelRef),
3531
+ provider: params.provider,
3532
+ runtime: params.runtime,
3533
+ runtimeIndex: params.runtimeIndex,
3534
+ page: params.page,
3535
+ providerPage: params.providerPage,
3536
+ userId: params.userId
3537
+ })
3538
+ })]));
3539
+ for (const [i, modelRef] of dedupedQuickModels.entries()) rows.push(new Row([createModelPickerButton({
3540
+ label: formatRecentsButtonLabel(modelRef),
3541
+ style: ButtonStyle.Secondary,
3542
+ customId: buildDiscordModelPickerCustomId({
3543
+ command: params.command,
3544
+ action: "submit",
3545
+ view: "recents",
3546
+ recentSlot: i + 2,
3547
+ modelToken: createModelRefToken(modelRef),
3269
3548
  provider: params.provider,
3270
3549
  runtime: params.runtime,
3271
3550
  runtimeIndex: params.runtimeIndex,
@@ -3274,25 +3553,6 @@ function renderDiscordModelPickerRecentsView(params) {
3274
3553
  userId: params.userId
3275
3554
  })
3276
3555
  })]));
3277
- for (let i = 0; i < dedupedQuickModels.length; i++) {
3278
- const modelRef = dedupedQuickModels[i];
3279
- rows.push(new Row([createModelPickerButton({
3280
- label: formatRecentsButtonLabel(modelRef),
3281
- style: ButtonStyle.Secondary,
3282
- customId: buildDiscordModelPickerCustomId({
3283
- command: params.command,
3284
- action: "submit",
3285
- view: "recents",
3286
- recentSlot: i + 2,
3287
- provider: params.provider,
3288
- runtime: params.runtime,
3289
- runtimeIndex: params.runtimeIndex,
3290
- page: params.page,
3291
- providerPage: params.providerPage,
3292
- userId: params.userId
3293
- })
3294
- })]));
3295
- }
3296
3556
  const backRow = new Row([createModelPickerButton({
3297
3557
  label: "Back",
3298
3558
  style: ButtonStyle.Secondary,
@@ -3424,6 +3684,10 @@ async function applyDiscordModelPickerSelection(params) {
3424
3684
  if (!persisted) logVerbose(`discord: direct session override persist failed — expected ${params.resolvedModelRef} but read ${effectiveModelRef} from session key ${fallbackRoute.sessionKey}`);
3425
3685
  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}`);
3426
3686
  } catch (error) {
3687
+ if (error instanceof ModelSelectionLockedError) return {
3688
+ status: "rejected",
3689
+ noticeMessage: `❌ ${error.message}`
3690
+ };
3427
3691
  const message = error instanceof Error ? error.message : String(error);
3428
3692
  logVerbose(`discord: direct session override persist threw for session key ${fallbackRoute.sessionKey}: ${message}`);
3429
3693
  }
@@ -3443,6 +3707,10 @@ async function applyDiscordModelPickerSelection(params) {
3443
3707
  noticeMessage: `⚠️ Tried to set ${params.resolvedModelRef}, but current model is ${effectiveModelRef}.`
3444
3708
  };
3445
3709
  } catch (error) {
3710
+ if (error instanceof ModelSelectionLockedError) return {
3711
+ status: "rejected",
3712
+ noticeMessage: `❌ ${error.message}`
3713
+ };
3446
3714
  if (error instanceof Error && error.message === "timeout") return {
3447
3715
  status: "timeout",
3448
3716
  noticeMessage: `⏳ Model change to ${params.resolvedModelRef} is still processing. Check /status in a few seconds.`
@@ -3715,11 +3983,14 @@ function resolveParsedRuntimeForSubmission(params) {
3715
3983
  }
3716
3984
  function resolveSubmittedModelRef(params) {
3717
3985
  if (params.parsed.action === "reset") return `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
3986
+ if (params.parsed.modelToken) return resolveDiscordModelPickerModelRefByToken(params.data, params.parsed.modelToken);
3718
3987
  if (params.parsed.action === "quick") {
3988
+ if (params.requireModelToken) return null;
3719
3989
  const slot = params.parsed.recentSlot ?? 0;
3720
3990
  return slot >= 1 ? params.quickModels[slot - 1] ?? null : null;
3721
3991
  }
3722
3992
  if (params.parsed.view === "recents") {
3993
+ if (params.requireModelToken) return null;
3723
3994
  const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
3724
3995
  const dedupedRecents = params.quickModels.filter((ref) => ref !== defaultModelRef);
3725
3996
  const slot = params.parsed.recentSlot ?? 0;
@@ -3727,10 +3998,12 @@ function resolveSubmittedModelRef(params) {
3727
3998
  return slot >= 2 ? dedupedRecents[slot - 2] ?? null : null;
3728
3999
  }
3729
4000
  const provider = params.parsed.provider;
3730
- const selectedModel = resolveDiscordModelPickerModelByIndex({
4001
+ const selectedModel = resolveDiscordModelPickerModelSelection({
3731
4002
  data: params.data,
3732
4003
  provider: provider ?? "",
3733
- modelIndex: params.parsed.modelIndex
4004
+ modelIndex: params.parsed.modelIndex,
4005
+ modelToken: params.parsed.modelToken,
4006
+ requireModelToken: params.requireModelToken
3734
4007
  });
3735
4008
  return provider && selectedModel ? `${provider}/${selectedModel}` : null;
3736
4009
  }
@@ -3752,6 +4025,11 @@ function listDiscordModelPickerProviderModels(data, provider) {
3752
4025
  if (!modelSet) return [];
3753
4026
  return [...modelSet].toSorted();
3754
4027
  }
4028
+ function resolveDiscordModelPickerModelRefByToken(data, modelToken) {
4029
+ const matchingRefs = [];
4030
+ for (const [provider, models] of data.byProvider) for (const model of models) if (createDiscordModelPickerModelToken(provider, model) === modelToken) matchingRefs.push(`${provider}/${model}`);
4031
+ return matchingRefs.length === 1 ? matchingRefs[0] ?? null : null;
4032
+ }
3755
4033
  function resolveDiscordModelPickerModelIndex(params) {
3756
4034
  const models = listDiscordModelPickerProviderModels(params.data, params.provider);
3757
4035
  if (!models.length) return null;
@@ -3759,10 +4037,14 @@ function resolveDiscordModelPickerModelIndex(params) {
3759
4037
  if (index < 0) return null;
3760
4038
  return index + 1;
3761
4039
  }
3762
- function resolveDiscordModelPickerModelByIndex(params) {
3763
- if (!params.modelIndex || params.modelIndex < 1) return null;
4040
+ function resolveDiscordModelPickerModelSelection(params) {
3764
4041
  const models = listDiscordModelPickerProviderModels(params.data, params.provider);
3765
4042
  if (!models.length) return null;
4043
+ if (params.modelToken) {
4044
+ const matchingModels = models.filter((model) => createDiscordModelPickerModelToken(params.provider, model) === params.modelToken);
4045
+ return matchingModels.length === 1 ? matchingModels[0] ?? null : null;
4046
+ }
4047
+ if (params.requireModelToken || !params.modelIndex || params.modelIndex < 1) return null;
3766
4048
  return models[params.modelIndex - 1] ?? null;
3767
4049
  }
3768
4050
  function resolveDiscordModelPickerRuntimeForProvider(params) {
@@ -3801,20 +4083,22 @@ async function handleDiscordModelPickerInteraction(params) {
3801
4083
  if (await params.safeInteractionCall("model picker defer", () => interaction.acknowledge()) === null) return;
3802
4084
  deferredUpdate = true;
3803
4085
  }
4086
+ const cfg = getRuntimeConfigSnapshot() ?? ctx.cfg;
4087
+ const requireModelToken = cfg !== ctx.cfg;
3804
4088
  const route = await resolveDiscordModelPickerRoute({
3805
4089
  interaction,
3806
- cfg: ctx.cfg,
4090
+ cfg,
3807
4091
  accountId: ctx.accountId,
3808
4092
  threadBindings: ctx.threadBindings
3809
4093
  });
3810
- const pickerData = await loadDiscordModelPickerData(ctx.cfg, route.agentId);
4094
+ const pickerData = await loadDiscordModelPickerData(cfg, route.agentId);
3811
4095
  const currentModelRef = resolveDiscordModelPickerCurrentModel({
3812
- cfg: ctx.cfg,
4096
+ cfg,
3813
4097
  route,
3814
4098
  data: pickerData
3815
4099
  });
3816
4100
  const currentRuntime = resolveDiscordModelPickerCurrentRuntime({
3817
- cfg: ctx.cfg,
4101
+ cfg,
3818
4102
  route
3819
4103
  });
3820
4104
  const allowedModelRefs = buildDiscordModelPickerAllowedModelRefs(pickerData);
@@ -3911,11 +4195,22 @@ async function handleDiscordModelPickerInteraction(params) {
3911
4195
  currentModelRef,
3912
4196
  data: pickerData
3913
4197
  });
3914
- const pendingModel = resolveDiscordModelPickerModelByIndex({
4198
+ const pendingModel = resolveDiscordModelPickerModelSelection({
3915
4199
  data: pickerData,
3916
4200
  provider,
3917
- modelIndex: parsed.modelIndex
4201
+ modelIndex: parsed.modelIndex,
4202
+ modelToken: parsed.modelToken,
4203
+ requireModelToken
3918
4204
  });
4205
+ if ((parsed.modelIndex || parsed.modelToken) && !pendingModel) {
4206
+ await showNotice("That selection expired. Please choose a model again.");
4207
+ return;
4208
+ }
4209
+ const pendingModelIndex = pendingModel ? resolveDiscordModelPickerModelIndex({
4210
+ data: pickerData,
4211
+ provider,
4212
+ model: pendingModel
4213
+ }) : void 0;
3919
4214
  await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
3920
4215
  command: parsed.command,
3921
4216
  userId: parsed.userId,
@@ -3928,7 +4223,7 @@ async function handleDiscordModelPickerInteraction(params) {
3928
4223
  currentModel: currentModelRef,
3929
4224
  currentRuntime,
3930
4225
  ...pendingModel ? { pendingModel: `${provider}/${pendingModel}` } : {},
3931
- pendingModelIndex: parsed.modelIndex,
4226
+ pendingModelIndex: pendingModelIndex ?? void 0,
3932
4227
  pendingRuntime: resolvePendingRuntime({
3933
4228
  data: pickerData,
3934
4229
  provider,
@@ -4032,12 +4327,23 @@ async function handleDiscordModelPickerInteraction(params) {
4032
4327
  await showNotice("Sorry, that provider isn't available anymore.");
4033
4328
  return;
4034
4329
  }
4035
- const selectedModel = resolveDiscordModelPickerModelByIndex({
4330
+ const selectedModel = resolveDiscordModelPickerModelSelection({
4036
4331
  data: pickerData,
4037
4332
  provider,
4038
- modelIndex: parsed.modelIndex
4333
+ modelIndex: parsed.modelIndex,
4334
+ modelToken: parsed.modelToken,
4335
+ requireModelToken
4039
4336
  });
4337
+ if ((parsed.modelIndex || parsed.modelToken) && !selectedModel) {
4338
+ await showNotice("That selection expired. Please choose a model again.");
4339
+ return;
4340
+ }
4040
4341
  const pendingModel = selectedModel ? `${provider}/${selectedModel}` : void 0;
4342
+ const pendingModelIndex = selectedModel ? resolveDiscordModelPickerModelIndex({
4343
+ data: pickerData,
4344
+ provider,
4345
+ model: selectedModel
4346
+ }) : void 0;
4041
4347
  const derivedProviderBucket = parsed.providerBucket ?? findProviderBucketId(pickerData, provider);
4042
4348
  const currentModelOnly = splitDiscordModelRef(currentModelRef ?? "");
4043
4349
  const derivedModelBucket = parsed.modelBucket ?? (selectedModel ? findModelBucketId(pickerData, provider, selectedModel) : currentModelOnly && currentModelOnly.provider === provider ? findModelBucketId(pickerData, provider, currentModelOnly.model) : void 0);
@@ -4053,7 +4359,7 @@ async function handleDiscordModelPickerInteraction(params) {
4053
4359
  currentModel: currentModelRef,
4054
4360
  currentRuntime,
4055
4361
  ...pendingModel ? { pendingModel } : {},
4056
- pendingModelIndex: parsed.modelIndex,
4362
+ pendingModelIndex: pendingModelIndex ?? void 0,
4057
4363
  pendingRuntime: selectedRuntime,
4058
4364
  quickModels
4059
4365
  })));
@@ -4063,7 +4369,8 @@ async function handleDiscordModelPickerInteraction(params) {
4063
4369
  const modelRef = resolveSubmittedModelRef({
4064
4370
  data: pickerData,
4065
4371
  parsed,
4066
- quickModels
4372
+ quickModels,
4373
+ requireModelToken
4067
4374
  });
4068
4375
  const parsedModelRef = modelRef ? splitDiscordModelRef(modelRef) : null;
4069
4376
  if (!parsedModelRef || !pickerData.byProvider.get(parsedModelRef.provider)?.has(parsedModelRef.model)) {
@@ -4091,7 +4398,7 @@ async function handleDiscordModelPickerInteraction(params) {
4091
4398
  interaction,
4092
4399
  selectionCommand,
4093
4400
  dispatchCommandInteraction: params.dispatchCommandInteraction,
4094
- cfg: ctx.cfg,
4401
+ cfg,
4095
4402
  discordConfig: ctx.discordConfig,
4096
4403
  accountId: ctx.accountId,
4097
4404
  sessionPrefix: ctx.sessionPrefix,
@@ -4106,7 +4413,7 @@ async function handleDiscordModelPickerInteraction(params) {
4106
4413
  preferenceScope,
4107
4414
  settleMs: ctx.postApplySettleMs ?? 250,
4108
4415
  resolveCurrentModel: (currentRoute) => resolveDiscordModelPickerCurrentModel({
4109
- cfg: ctx.cfg,
4416
+ cfg,
4110
4417
  route: currentRoute,
4111
4418
  data: pickerData
4112
4419
  })
@@ -4960,6 +5267,22 @@ var GatewayReconnectTimer = class {
4960
5267
  }
4961
5268
  };
4962
5269
  //#endregion
5270
+ //#region extensions/discord/src/internal/gateway-payload.ts
5271
+ function ensureGatewayParams(url) {
5272
+ const parsed = new URL(url);
5273
+ parsed.searchParams.set("v", parsed.searchParams.get("v") ?? "10");
5274
+ parsed.searchParams.set("encoding", parsed.searchParams.get("encoding") ?? "json");
5275
+ return parsed.toString();
5276
+ }
5277
+ function decodeGatewayMessage(incoming) {
5278
+ 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);
5279
+ try {
5280
+ return JSON.parse(text);
5281
+ } catch {
5282
+ return null;
5283
+ }
5284
+ }
5285
+ //#endregion
4963
5286
  //#region extensions/discord/src/internal/gateway-rate-limit.ts
4964
5287
  const GATEWAY_SEND_LIMIT = 120;
4965
5288
  const GATEWAY_SEND_WINDOW_MS = 6e4;
@@ -5036,6 +5359,77 @@ var GatewaySendLimiter = class {
5036
5359
  }
5037
5360
  };
5038
5361
  //#endregion
5362
+ //#region extensions/discord/src/internal/gateway-voice-state-cache.ts
5363
+ var DiscordGatewayVoiceStateCache = class {
5364
+ constructor() {
5365
+ this.statesByGuild = /* @__PURE__ */ new Map();
5366
+ this.transitionsByState = /* @__PURE__ */ new WeakMap();
5367
+ }
5368
+ clear() {
5369
+ this.statesByGuild.clear();
5370
+ this.transitionsByState = /* @__PURE__ */ new WeakMap();
5371
+ }
5372
+ listVoiceChannelStates(guildId, channelId) {
5373
+ const states = this.statesByGuild.get(guildId);
5374
+ if (!states) return [];
5375
+ const result = [];
5376
+ for (const state of states.values()) if (state.channel_id === channelId) result.push({ ...state });
5377
+ return result;
5378
+ }
5379
+ takeTransition(state) {
5380
+ const transition = this.transitionsByState.get(state);
5381
+ if (!transition) return null;
5382
+ this.transitionsByState.delete(state);
5383
+ return {
5384
+ current: { ...transition.current },
5385
+ ...transition.previous ? { previous: { ...transition.previous } } : {}
5386
+ };
5387
+ }
5388
+ apply(payload) {
5389
+ if (payload.t === GatewayDispatchEvents.Ready) {
5390
+ this.clear();
5391
+ return;
5392
+ }
5393
+ if (payload.t === GatewayDispatchEvents.GuildCreate) {
5394
+ const guild = payload.d;
5395
+ if (guild.unavailable) {
5396
+ this.statesByGuild.delete(guild.id);
5397
+ return;
5398
+ }
5399
+ const states = /* @__PURE__ */ new Map();
5400
+ for (const state of guild.voice_states) if (state.channel_id) states.set(state.user_id, {
5401
+ ...state,
5402
+ guild_id: guild.id
5403
+ });
5404
+ this.statesByGuild.set(guild.id, states);
5405
+ return;
5406
+ }
5407
+ if (payload.t === GatewayDispatchEvents.VoiceStateUpdate) {
5408
+ const state = payload.d;
5409
+ const guildId = state.guild_id?.trim();
5410
+ if (!guildId) return;
5411
+ const states = this.statesByGuild.get(guildId) ?? /* @__PURE__ */ new Map();
5412
+ const previous = states.get(state.user_id);
5413
+ const current = {
5414
+ ...state,
5415
+ guild_id: guildId
5416
+ };
5417
+ this.transitionsByState.set(state, {
5418
+ current,
5419
+ ...previous ? { previous: { ...previous } } : {}
5420
+ });
5421
+ if (state.channel_id) states.set(state.user_id, current);
5422
+ else states.delete(state.user_id);
5423
+ this.statesByGuild.set(guildId, states);
5424
+ return;
5425
+ }
5426
+ if (payload.t === GatewayDispatchEvents.GuildDelete) {
5427
+ const guild = payload.d;
5428
+ this.statesByGuild.delete(guild.id);
5429
+ }
5430
+ }
5431
+ };
5432
+ //#endregion
5039
5433
  //#region extensions/discord/src/internal/gateway.ts
5040
5434
  const GatewayIntents = GatewayIntentBits;
5041
5435
  const READY_STATE_OPEN = 1;
@@ -5045,20 +5439,6 @@ const DISCORD_GATEWAY_WS_CLIENT_OPTIONS = Object.freeze({ maxPayload: 16 * 1024
5045
5439
  const INVALID_SESSION_MIN_DELAY_MS = 1e3;
5046
5440
  const INVALID_SESSION_JITTER_MS = 4e3;
5047
5441
  const RESUME_FAILURE_THRESHOLD = 3;
5048
- function ensureGatewayParams(url) {
5049
- const parsed = new URL(url);
5050
- parsed.searchParams.set("v", parsed.searchParams.get("v") ?? "10");
5051
- parsed.searchParams.set("encoding", parsed.searchParams.get("encoding") ?? "json");
5052
- return parsed.toString();
5053
- }
5054
- function decodeGatewayMessage(incoming) {
5055
- 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);
5056
- try {
5057
- return JSON.parse(text);
5058
- } catch {
5059
- return null;
5060
- }
5061
- }
5062
5442
  var GatewayPlugin = class extends Plugin {
5063
5443
  constructor(options, gatewayInfo) {
5064
5444
  super();
@@ -5076,6 +5456,7 @@ var GatewayPlugin = class extends Plugin {
5076
5456
  this.isConnecting = false;
5077
5457
  this.heartbeatTimers = new GatewayHeartbeatTimers();
5078
5458
  this.reconnectTimer = new GatewayReconnectTimer();
5459
+ this.voiceStateCache = new DiscordGatewayVoiceStateCache();
5079
5460
  this.outboundLimiter = new GatewaySendLimiter((payload) => this.sendSerializedGatewayEvent(payload), (error) => this.emitter.emit("error", error));
5080
5461
  this.options = {
5081
5462
  ...options,
@@ -5091,6 +5472,12 @@ var GatewayPlugin = class extends Plugin {
5091
5472
  get ping() {
5092
5473
  return null;
5093
5474
  }
5475
+ listVoiceChannelStates(guildId, channelId) {
5476
+ return this.voiceStateCache.listVoiceChannelStates(guildId, channelId);
5477
+ }
5478
+ takeVoiceStateTransition(state) {
5479
+ return this.voiceStateCache.takeTransition(state);
5480
+ }
5094
5481
  get heartbeatInterval() {
5095
5482
  return this.heartbeatTimers.heartbeatInterval;
5096
5483
  }
@@ -5138,6 +5525,7 @@ var GatewayPlugin = class extends Plugin {
5138
5525
  this.isConnected = false;
5139
5526
  this.reconnectAttempts = 0;
5140
5527
  this.consecutiveResumeFailures = 0;
5528
+ this.voiceStateCache.clear();
5141
5529
  }
5142
5530
  createWebSocket(url) {
5143
5531
  return new ws.WebSocket(url, DISCORD_GATEWAY_WS_CLIENT_OPTIONS);
@@ -5316,6 +5704,7 @@ var GatewayPlugin = class extends Plugin {
5316
5704
  this.consecutiveResumeFailures = 0;
5317
5705
  this.isConnected = true;
5318
5706
  }
5707
+ this.voiceStateCache.apply(payload);
5319
5708
  dispatchVoiceGatewayEvent(this.client, payload.t, payload.d);
5320
5709
  const data = mapGatewayDispatchData(this.client, payload.t, payload.d);
5321
5710
  await this.client.dispatchGatewayEvent(payload.t, data);
@@ -5326,6 +5715,7 @@ var GatewayPlugin = class extends Plugin {
5326
5715
  this.resumeGatewayUrl = null;
5327
5716
  this.sequence = null;
5328
5717
  this.consecutiveResumeFailures = 0;
5718
+ this.voiceStateCache.clear();
5329
5719
  }
5330
5720
  getResumeState() {
5331
5721
  return this.sessionId && this.sequence !== null ? {
@@ -5917,7 +6307,7 @@ function formatDiscordGatewayCloseReason(reason) {
5917
6307
  const text = reason.toString("utf8").replaceAll(/\s+/g, " ").trim();
5918
6308
  if (!text) return `<${reason.length} bytes>`;
5919
6309
  if (text.length <= DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS) return text;
5920
- return `${text.slice(0, DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS)}...`;
6310
+ return `${truncateUtf16Safe(text, DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS)}...`;
5921
6311
  }
5922
6312
  function formatDiscordGatewayTransportErrorLog(params) {
5923
6313
  return `discord: gateway websocket error ${[
@@ -6131,6 +6521,26 @@ var DiscordGatewayLifecycleError = class extends Error {
6131
6521
  function getDiscordGatewayEmitter(gateway) {
6132
6522
  return gateway?.emitter;
6133
6523
  }
6524
+ const discordGatewayLog = createSubsystemLogger("discord/gateway");
6525
+ const discordGatewayLateErrorGuards = /* @__PURE__ */ new WeakMap();
6526
+ function removeDiscordGatewayLateErrorGuard(emitter) {
6527
+ const guard = discordGatewayLateErrorGuards.get(emitter);
6528
+ if (!guard) return;
6529
+ emitter.off("error", guard);
6530
+ discordGatewayLateErrorGuards.delete(emitter);
6531
+ }
6532
+ function ensureDiscordGatewayLateErrorGuard(emitter) {
6533
+ if (emitter.listenerCount("error") > 0) return;
6534
+ const seenMessages = /* @__PURE__ */ new Set();
6535
+ const guard = (err) => {
6536
+ const message = formatDiscordGatewayErrorMessage(err);
6537
+ if (seenMessages.has(message)) return;
6538
+ seenMessages.add(message);
6539
+ discordGatewayLog.error(`suppressed late gateway error after dispose: ${message}`);
6540
+ };
6541
+ discordGatewayLateErrorGuards.set(emitter, guard);
6542
+ emitter.on("error", guard);
6543
+ }
6134
6544
  function readFirstStackFrame(err) {
6135
6545
  const stack = err.stack;
6136
6546
  if (!stack) return;
@@ -6212,6 +6622,7 @@ function createDiscordGatewaySupervisor(params) {
6212
6622
  case "buffering": pending.push(event);
6213
6623
  }
6214
6624
  };
6625
+ removeDiscordGatewayLateErrorGuard(emitter);
6215
6626
  emitter.on("error", onGatewayError);
6216
6627
  return {
6217
6628
  emitter,
@@ -6232,6 +6643,8 @@ function createDiscordGatewaySupervisor(params) {
6232
6643
  },
6233
6644
  dispose: () => {
6234
6645
  if (phase === "disposed") return;
6646
+ emitter.off("error", onGatewayError);
6647
+ ensureDiscordGatewayLateErrorGuard(emitter);
6235
6648
  lifecycleHandler = void 0;
6236
6649
  phase = "disposed";
6237
6650
  pending.length = 0;
@@ -6344,11 +6757,12 @@ function formatDiscordChannelUnresolved(entry) {
6344
6757
  function formatDiscordUserResolved(entry) {
6345
6758
  const displayName = entry.name?.trim();
6346
6759
  const target = displayName || entry.id;
6347
- return formatResolutionLogDetails(formatResolvedBase(entry.input, target), [
6348
- displayName && entry.id ? `id:${entry.id}` : void 0,
6760
+ const formatted = formatResolutionLogDetails(formatResolvedBase(entry.input, target), [
6761
+ displayName && entry.id && entry.id !== entry.input ? `id:${entry.id}` : void 0,
6349
6762
  entry.guildName ? `guild:${entry.guildName}` : void 0,
6350
6763
  entry.note
6351
6764
  ]);
6765
+ return formatted === entry.input ? null : formatted;
6352
6766
  }
6353
6767
  function formatDiscordUserUnresolved(entry) {
6354
6768
  return formatResolutionLogDetails(entry.input, [
@@ -6634,9 +7048,9 @@ async function resolveDiscordProviderCommandSpecs(params) {
6634
7048
  cfg: params.cfg,
6635
7049
  getPluginCommandSpecs: params.getPluginCommandSpecs
6636
7050
  });
6637
- params.runtime.log?.(warn(`discord: ${initialCommandCount} commands exceeds limit; removing per-skill commands and keeping /skill.`));
7051
+ params.runtime.log?.(warn(`${initialCommandCount} commands exceed the ${maxDiscordCommands}-command Discord limit; removing per-skill commands and keeping /skill.`));
6638
7052
  }
6639
- if (params.nativeEnabled && commandSpecs.length > maxDiscordCommands) params.runtime.log?.(warn(`discord: ${commandSpecs.length} commands exceeds limit; some commands may fail to deploy.`));
7053
+ if (params.nativeEnabled && commandSpecs.length > maxDiscordCommands) params.runtime.log?.(warn(`${commandSpecs.length} commands exceed the ${maxDiscordCommands}-command Discord limit; some commands may fail to deploy.`));
6640
7054
  return {
6641
7055
  skillCommands,
6642
7056
  commandSpecs
@@ -6714,9 +7128,6 @@ function readFiniteNumber(value) {
6714
7128
  function readNonNegativeInteger(value) {
6715
7129
  return parseStrictNonNegativeInteger(value);
6716
7130
  }
6717
- function formatDurationMs(ms) {
6718
- return formatDurationSeconds(ms, { decimals: ms >= 1e3 ? 1 : 0 });
6719
- }
6720
7131
  function isAbortLikeError(err) {
6721
7132
  if (!err || typeof err !== "object") return false;
6722
7133
  const name = "name" in err && typeof err.name === "string" ? err.name : void 0;
@@ -6739,8 +7150,8 @@ function formatDiscordDeployErrorMessage(err) {
6739
7150
  const operation = formatDiscordDeployRestOperation(deployErr);
6740
7151
  if (!(requestMs !== void 0 || timeoutMs !== void 0 || deployErr.deployRestMethod !== void 0 || deployErr.deployRestPath !== void 0)) return "Discord REST request was aborted";
6741
7152
  const timing = [];
6742
- if (timeoutMs !== void 0) timing.push(`timeout=${formatDurationMs(timeoutMs)}`);
6743
- if (requestMs !== void 0) timing.push(`observed=${formatDurationMs(requestMs)}`);
7153
+ if (timeoutMs !== void 0) timing.push(`timeout=${formatDurationSeconds(timeoutMs, { decimals: timeoutMs >= 1e3 ? 1 : 0 })}`);
7154
+ if (requestMs !== void 0) timing.push(`observed=${formatDurationSeconds(requestMs, { decimals: requestMs >= 1e3 ? 1 : 0 })}`);
6744
7155
  const timingText = timing.length > 0 ? ` (${timing.join(", ")})` : "";
6745
7156
  if (timeoutMs !== void 0 && requestMs !== void 0 && requestMs >= timeoutMs) return `Discord REST ${operation} timed out${timingText}`;
6746
7157
  return `Discord REST ${operation} was aborted${timingText}`;
@@ -6774,7 +7185,7 @@ function formatDiscordDeployRateLimitDetails(err) {
6774
7185
  function formatDiscordDeployRateLimitWarning(err, accountId) {
6775
7186
  const rateLimit = resolveDiscordDeployRateLimitDetails(err);
6776
7187
  if (!rateLimit) return;
6777
- const parts = [`discord: native slash command deploy rate limited for ${accountId}`];
7188
+ const parts = [`[${accountId}] slash command deploy rate limited`];
6778
7189
  if (typeof rateLimit.retryAfterMs === "number") parts.push(`retry after ${formatDurationSeconds(rateLimit.retryAfterMs, { decimals: 1 })}`);
6779
7190
  if (rateLimit.scope) parts.push(`scope=${rateLimit.scope}`);
6780
7191
  if (typeof rateLimit.discordCode === "number" || typeof rateLimit.discordCode === "string") parts.push(`code=${rateLimit.discordCode}`);
@@ -6797,6 +7208,11 @@ function formatDiscordRejectedDeployEntries(params) {
6797
7208
  return [parts.join(" ")];
6798
7209
  });
6799
7210
  }
7211
+ function isRedundantDiscordDeployBody(rawBody) {
7212
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return false;
7213
+ const keys = Object.keys(rawBody);
7214
+ return keys.length > 0 && keys.every((key) => key === "message" || key === "code");
7215
+ }
6800
7216
  function formatDiscordDeployErrorDetails(err) {
6801
7217
  if (!err || typeof err !== "object") return "";
6802
7218
  const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
@@ -6808,7 +7224,7 @@ function formatDiscordDeployErrorDetails(err) {
6808
7224
  const details = [];
6809
7225
  if (typeof status === "number") details.push(`status=${status}`);
6810
7226
  if (typeof discordCode === "number" || typeof discordCode === "string") details.push(`code=${discordCode}`);
6811
- if (rawBody !== void 0) {
7227
+ if (rawBody !== void 0 && !isRedundantDiscordDeployBody(rawBody)) {
6812
7228
  let bodyText;
6813
7229
  try {
6814
7230
  bodyText = JSON.stringify(rawBody);
@@ -6820,7 +7236,7 @@ function formatDiscordDeployErrorDetails(err) {
6820
7236
  }
6821
7237
  if (bodyText) {
6822
7238
  const maxLen = 800;
6823
- const trimmed = bodyText.length > maxLen ? `${bodyText.slice(0, maxLen)}...` : bodyText;
7239
+ const trimmed = bodyText.length > maxLen ? `${truncateUtf16Safe(bodyText, maxLen)}...` : bodyText;
6824
7240
  details.push(`body=${trimmed}`);
6825
7241
  }
6826
7242
  }
@@ -6856,8 +7272,12 @@ function logDiscordStartupPhase$1(params) {
6856
7272
  function readDeployRequestBody(data) {
6857
7273
  return data && typeof data === "object" && "body" in data ? data.body : void 0;
6858
7274
  }
7275
+ function isDeployCommandsPath(path) {
7276
+ return path.startsWith("/applications/") && path.includes("/commands");
7277
+ }
6859
7278
  function wrapDeployRestMethod(params) {
6860
7279
  return async (path, data, query) => {
7280
+ if (!isDeployCommandsPath(path)) return params.original[params.method](path, data, query);
6861
7281
  const startedAt = Date.now();
6862
7282
  const body = readDeployRequestBody(data);
6863
7283
  const commandCount = Array.isArray(body) ? body.length : void 0;
@@ -6879,7 +7299,7 @@ function wrapDeployRestMethod(params) {
6879
7299
  const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
6880
7300
  if (rateLimitDetails) {
6881
7301
  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}`));
6882
- } else {
7302
+ } else if (params.shouldLogVerbose()) {
6883
7303
  const details = formatDiscordDeployErrorDetails(err);
6884
7304
  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}`);
6885
7305
  }
@@ -6931,7 +7351,7 @@ async function deployDiscordCommands(params) {
6931
7351
  await params.client.deployCommands({ mode: "reconcile" });
6932
7352
  } catch (err) {
6933
7353
  if (isDiscordDeployDailyCreateLimit(err)) {
6934
- 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.`));
7354
+ params.runtime.log?.(warn(`[${accountId}] slash command deploy skipped: daily application command create limit reached. Existing slash commands stay active until Discord resets the quota; message send/receive is unaffected.`));
6935
7355
  return;
6936
7356
  }
6937
7357
  const rateLimitWarning = formatDiscordDeployRateLimitWarning(err, accountId);
@@ -6942,7 +7362,7 @@ async function deployDiscordCommands(params) {
6942
7362
  throw err;
6943
7363
  }
6944
7364
  } catch (err) {
6945
- params.runtime.log?.(warn(`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`));
7365
+ params.runtime.log?.(warn(`[${accountId}] slash command deploy failed (message send/receive unaffected): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`));
6946
7366
  } finally {
6947
7367
  restoreDeployRestLogging();
6948
7368
  }
@@ -6967,7 +7387,7 @@ function runDiscordCommandDeployInBackground(params) {
6967
7387
  isVerbose: params.isVerbose
6968
7388
  });
6969
7389
  }).catch((err) => {
6970
- params.runtime.log?.(warn(`discord: native slash command deploy background warning (not message send): ${formatErrorMessage$1(err)}`));
7390
+ params.runtime.log?.(warn(`[${params.accountId}] slash command deploy failed in background (message send/receive unaffected): ${formatErrorMessage$1(err)}`));
6971
7391
  });
6972
7392
  }
6973
7393
  //#endregion
@@ -7031,8 +7451,8 @@ async function authorizeDiscordVoiceIngress(params) {
7031
7451
  return resolveCommandAuthorizedFromAuthorizers({
7032
7452
  useAccessGroups,
7033
7453
  authorizers: useAccessGroups ? [{
7034
- configured: ownerAllowList != null,
7035
- allowed: ownerAllowed
7454
+ configured: params.ownerAllowAll === true || ownerAllowList != null,
7455
+ allowed: params.ownerAllowAll === true || ownerAllowed
7036
7456
  }, {
7037
7457
  configured: hasAccessRestrictions,
7038
7458
  allowed: memberAllowed
@@ -7050,6 +7470,22 @@ async function authorizeDiscordVoiceIngress(params) {
7050
7470
  };
7051
7471
  }
7052
7472
  //#endregion
7473
+ //#region extensions/discord/src/voice/owner-access.ts
7474
+ function resolveDiscordVoiceOwnerAccess(params) {
7475
+ const commandOwnerAllowFrom = resolveDiscordCommandOwnerAllowFrom(params.cfg);
7476
+ if (commandOwnerAllowFrom) return {
7477
+ ownerAllowFrom: commandOwnerAllowFrom,
7478
+ ownerAllowAll: commandOwnerAllowFrom.includes("*")
7479
+ };
7480
+ return {
7481
+ ownerAllowFrom: resolveDiscordAccountAllowFrom({
7482
+ cfg: params.cfg,
7483
+ accountId: params.accountId
7484
+ }) ?? params.discordConfig.allowFrom ?? params.discordConfig.dm?.allowFrom ?? [],
7485
+ ownerAllowAll: false
7486
+ };
7487
+ }
7488
+ //#endregion
7053
7489
  //#region extensions/discord/src/voice/command.ts
7054
7490
  const VOICE_CHANNEL_TYPES = [ChannelType.GuildVoice, ChannelType.GuildStageVoice];
7055
7491
  async function authorizeVoiceCommand(interaction, params, options) {
@@ -7076,6 +7512,7 @@ async function authorizeVoiceCommand(interaction, params, options) {
7076
7512
  author: user,
7077
7513
  member: interaction.rawData.member
7078
7514
  });
7515
+ const ownerAccess = resolveDiscordVoiceOwnerAccess(params);
7079
7516
  const access = await authorizeDiscordVoiceIngress({
7080
7517
  cfg: params.cfg,
7081
7518
  discordConfig: params.discordConfig,
@@ -7093,10 +7530,8 @@ async function authorizeVoiceCommand(interaction, params, options) {
7093
7530
  scope: channelContext.isThreadChannel ? "thread" : "channel",
7094
7531
  channelLabel: channelId ? formatMention({ channelId }) : "This channel",
7095
7532
  memberRoleIds,
7096
- ownerAllowFrom: resolveDiscordAccountAllowFrom({
7097
- cfg: params.cfg,
7098
- accountId: params.accountId
7099
- }),
7533
+ ownerAllowFrom: ownerAccess.ownerAllowFrom,
7534
+ ownerAllowAll: ownerAccess.ownerAllowAll,
7100
7535
  sender: {
7101
7536
  id: sender.id,
7102
7537
  name: sender.name,
@@ -7741,16 +8176,7 @@ function mapOptionLabels(options, values) {
7741
8176
  }
7742
8177
  function parseAgentComponentData(data) {
7743
8178
  const raw = readParsedComponentId(data);
7744
- const decodeSafe = (value) => {
7745
- if (!value.includes("%")) return value;
7746
- if (!/%[0-9A-Fa-f]{2}/.test(value)) return value;
7747
- try {
7748
- return decodeURIComponent(value);
7749
- } catch {
7750
- return value;
7751
- }
7752
- };
7753
- const componentId = typeof raw === "string" ? decodeSafe(raw) : typeof raw === "number" ? String(raw) : null;
8179
+ const componentId = typeof raw === "string" ? decodeCustomIdComponent(raw) : typeof raw === "number" ? String(raw) : null;
7754
8180
  if (!componentId) return null;
7755
8181
  return { componentId };
7756
8182
  }
@@ -7933,6 +8359,10 @@ function sanitizeDiscordFrontChannelReplyPayloads(payloads, options = {}) {
7933
8359
  }
7934
8360
  //#endregion
7935
8361
  //#region extensions/discord/src/monitor/reply-delivery.ts
8362
+ function formatDiscordReplyDeliveryFailure(params) {
8363
+ const context = [`target=${params.target}`, params.sessionKey ? `session=${params.sessionKey}` : void 0].filter(Boolean).join(" ");
8364
+ return `discord ${params.kind} reply failed (${context}): ${String(params.err)}`;
8365
+ }
7936
8366
  function resolveTargetChannelId(target) {
7937
8367
  if (!target.startsWith("channel:")) return;
7938
8368
  return target.slice(8).trim() || void 0;
@@ -8037,7 +8467,7 @@ async function deliverDiscordReply(params) {
8037
8467
  //#endregion
8038
8468
  //#region extensions/discord/src/monitor/agent-components.dispatch.ts
8039
8469
  const loadConversationRuntime$1 = createLazyRuntimeModule(() => import("./agent-components.runtime-CEPrf2SY.js"));
8040
- const loadTypingRuntime = createLazyRuntimeModule(() => import("./typing-BVcJgILg.js").then((n) => n.n));
8470
+ const loadTypingRuntime = createLazyRuntimeModule(() => import("./typing-BnYT7Szq.js").then((n) => n.n));
8041
8471
  function buildDiscordComponentConversationLabel(params) {
8042
8472
  if (params.interactionCtx.isDirectMessage) return buildDirectLabel(params.interactionCtx.user);
8043
8473
  if (params.interactionCtx.isGroupDm) return `Group DM #${params.channelCtx.channelName ?? params.interactionCtx.channelId} channel id:${params.interactionCtx.channelId}`;
@@ -8450,7 +8880,7 @@ async function dispatchPluginDiscordInteractiveEvent(params) {
8450
8880
  }
8451
8881
  //#endregion
8452
8882
  //#region extensions/discord/src/monitor/agent-components.handlers.ts
8453
- const loadComponentsRuntime = createLazyRuntimeModule(() => import("./components-BIBizbtZ.js").then((n) => n.t));
8883
+ const loadComponentsRuntime = createLazyRuntimeModule(() => import("./components-BVrfRS-F.js").then((n) => n.t));
8454
8884
  async function handleDiscordComponentEvent(params) {
8455
8885
  const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
8456
8886
  if (!parsed) {
@@ -9017,24 +9447,26 @@ function createDiscordComponentModal(ctx) {
9017
9447
  }
9018
9448
  //#endregion
9019
9449
  //#region extensions/discord/src/monitor/exec-approvals.ts
9020
- function decodeCustomIdValue(value) {
9021
- try {
9022
- return decodeURIComponent(value);
9023
- } catch {
9024
- return value;
9025
- }
9026
- }
9027
- function parseExecApprovalData(data) {
9028
- if (!data || typeof data !== "object") return null;
9029
- const coerce = (value) => typeof value === "string" || typeof value === "number" ? String(value) : "";
9030
- const rawId = coerce(data.id);
9031
- const rawAction = coerce(data.action);
9032
- if (!rawId || !rawAction) return null;
9033
- const action = rawAction;
9034
- if (action !== "allow-once" && action !== "allow-always" && action !== "deny") return null;
9450
+ function resolveTerminalLabel(approval) {
9451
+ if (approval.status === "allowed") return approval.decision === "allow-always" ? "Allowed always" : "Allowed once";
9452
+ if (approval.status === "denied") return "Denied";
9453
+ return approval.status === "expired" ? "Expired" : "Cancelled";
9454
+ }
9455
+ function buildTerminalPayload(params) {
9456
+ const { approval } = params;
9457
+ const label = resolveTerminalLabel(approval);
9458
+ const accentColor = approval.status === "denied" ? "#ED4245" : approval.status === "allowed" ? "#57F287" : "#99AAB5";
9035
9459
  return {
9036
- approvalId: decodeCustomIdValue(rawId),
9037
- action
9460
+ allowed_mentions: DISCORD_APPROVAL_ALLOWED_MENTIONS,
9461
+ components: [new Container([
9462
+ new TextDisplay(params.applied ? "## Approval resolved" : "## Approval already resolved"),
9463
+ new TextDisplay(`Canonical result: **${label}**`),
9464
+ new Separator({
9465
+ divider: false,
9466
+ spacing: "small"
9467
+ }),
9468
+ new TextDisplay(`-# ID: ${formatDiscordApprovalDisplayValue(approval.id)}`)
9469
+ ], { accentColor })]
9038
9470
  };
9039
9471
  }
9040
9472
  function isStructuredApprovalNotFoundError(err) {
@@ -9067,7 +9499,7 @@ var ExecApprovalButton = class extends Button {
9067
9499
  if (!approvers.some((id) => id === userId)) {
9068
9500
  try {
9069
9501
  await interaction.reply({
9070
- content: "⛔ You are not authorized to approve exec requests.",
9502
+ content: "⛔ You are not authorized to approve requests.",
9071
9503
  ephemeral: true
9072
9504
  });
9073
9505
  } catch {}
@@ -9077,10 +9509,28 @@ var ExecApprovalButton = class extends Button {
9077
9509
  try {
9078
9510
  await interaction.acknowledge();
9079
9511
  } catch {}
9080
- const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.action);
9081
- if (!result.ok) try {
9512
+ const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.approvalKind, parsed.action);
9513
+ if (!result.ok) {
9514
+ try {
9515
+ await interaction.followUp({
9516
+ 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.`,
9517
+ ephemeral: true
9518
+ });
9519
+ } catch {}
9520
+ return;
9521
+ }
9522
+ const terminalLabel = resolveTerminalLabel(result.resolution.approval);
9523
+ let terminalized = false;
9524
+ try {
9525
+ await interaction.editReply(buildTerminalPayload({
9526
+ approval: result.resolution.approval,
9527
+ applied: result.resolution.applied
9528
+ }));
9529
+ terminalized = true;
9530
+ } catch {}
9531
+ if (!terminalized || !result.resolution.applied) try {
9082
9532
  await interaction.followUp({
9083
- 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.`,
9533
+ content: result.resolution.applied ? `Approval resolved: ${terminalLabel}.` : `This approval was already resolved: ${terminalLabel}.`,
9084
9534
  ephemeral: true
9085
9535
  });
9086
9536
  } catch {}
@@ -9096,16 +9546,19 @@ function createDiscordExecApprovalButtonContext(params) {
9096
9546
  accountId: params.accountId,
9097
9547
  configOverride: params.config
9098
9548
  }),
9099
- resolveApproval: async (approvalId, decision) => {
9549
+ resolveApproval: async (approvalId, approvalKind, decision) => {
9100
9550
  try {
9101
- await resolveApprovalOverGateway({
9102
- cfg: params.cfg,
9103
- approvalId,
9104
- decision,
9105
- gatewayUrl: params.gatewayUrl,
9106
- clientDisplayName: `Discord approval (${params.accountId})`
9107
- });
9108
- return { ok: true };
9551
+ return {
9552
+ ok: true,
9553
+ resolution: await resolveApprovalOverGateway({
9554
+ cfg: params.cfg,
9555
+ approvalId,
9556
+ approvalKind,
9557
+ decision,
9558
+ gatewayUrl: params.gatewayUrl,
9559
+ clientDisplayName: `Discord approval (${params.accountId})`
9560
+ })
9561
+ };
9109
9562
  } catch (err) {
9110
9563
  return {
9111
9564
  ok: false,
@@ -9143,6 +9596,11 @@ function createDiscordProviderInteractionSurface(params) {
9143
9596
  accountId: params.accountId,
9144
9597
  configOverride: execApprovalsConfig
9145
9598
  });
9599
+ const approvalActionsEnabled = getDiscordExecApprovalApprovers({
9600
+ cfg: params.cfg,
9601
+ accountId: params.accountId,
9602
+ configOverride: execApprovalsConfig
9603
+ }).length > 0;
9146
9604
  if (execApprovalsEnabled) registerChannelRuntimeContext({
9147
9605
  channelRuntime: params.channelRuntime,
9148
9606
  channelId: "discord",
@@ -9178,7 +9636,7 @@ function createDiscordProviderInteractionSurface(params) {
9178
9636
  })
9179
9637
  ];
9180
9638
  const modals = [];
9181
- if (execApprovalsEnabled) components.push(createExecApprovalButton(createDiscordExecApprovalButtonContext({
9639
+ if (approvalActionsEnabled) components.push(createExecApprovalButton(createDiscordExecApprovalButtonContext({
9182
9640
  cfg: params.cfg,
9183
9641
  accountId: params.accountId,
9184
9642
  config: execApprovalsConfig
@@ -9668,7 +10126,7 @@ async function runDiscordGatewayLifecycle(params) {
9668
10126
  gatewayEmitter?.removeListener(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, onGatewayTransportActivity);
9669
10127
  if (params.voiceManager) {
9670
10128
  await params.voiceManager.destroy();
9671
- const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-BvvM5Ns9.js").then((n) => n.n);
10129
+ const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-CLZ5X8fh.js").then((n) => n.n);
9672
10130
  setDiscordTranscriptsVoiceManager({
9673
10131
  accountId: params.accountId,
9674
10132
  manager: null
@@ -9863,10 +10321,17 @@ function registerDiscordMonitorListeners(params) {
9863
10321
  }
9864
10322
  registerDiscordListener(params.client.listeners, new DiscordThreadUpdateListener(params.cfg, params.accountId, params.logger));
9865
10323
  if (params.discordConfig.intents?.presence) {
9866
- registerDiscordListener(params.client.listeners, new DiscordPresenceListener({
10324
+ const presenceListener = new DiscordPresenceListener({
10325
+ cfg: params.cfg,
9867
10326
  logger: params.logger,
9868
- accountId: params.accountId
9869
- }));
10327
+ accountId: params.accountId,
10328
+ botUserId: params.botUserId,
10329
+ guildEntries: params.guildEntries
10330
+ });
10331
+ registerDiscordListener(params.client.listeners, presenceListener);
10332
+ registerDiscordListener(params.client.listeners, new DiscordPresenceGuildCreateListener(presenceListener));
10333
+ registerDiscordListener(params.client.listeners, new DiscordPresenceGuildDeleteListener(presenceListener));
10334
+ registerDiscordListener(params.client.listeners, new DiscordPresenceReadyListener(presenceListener));
9870
10335
  params.runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
9871
10336
  }
9872
10337
  }
@@ -9900,7 +10365,7 @@ function createEnvProxyDiscordRestDispatcher(runtime) {
9900
10365
  }
9901
10366
  }
9902
10367
  function createDiscordRestFetchWithDispatcher(dispatcher) {
9903
- return wrapFetchWithAbortSignal(((input, init) => fetch$1(input, {
10368
+ return wrapFetchWithAbortSignal(((input, init) => fetchWithRuntimeDispatcher(input, {
9904
10369
  ...init,
9905
10370
  dispatcher
9906
10371
  }).then((response) => {
@@ -9946,7 +10411,7 @@ function logDiscordStartupPhase(params) {
9946
10411
  });
9947
10412
  }
9948
10413
  async function loadDiscordVoiceRuntime() {
9949
- const promise = discordVoiceRuntimePromise ?? import("./manager.runtime-DaB-CTrt.js");
10414
+ const promise = discordVoiceRuntimePromise ?? import("./manager.runtime-3RVMVkyx.js");
9950
10415
  discordVoiceRuntimePromise = promise;
9951
10416
  try {
9952
10417
  return await promise;
@@ -9956,7 +10421,7 @@ async function loadDiscordVoiceRuntime() {
9956
10421
  }
9957
10422
  }
9958
10423
  async function loadDiscordProviderSessionRuntime() {
9959
- const promise = discordProviderSessionRuntimePromise ?? import("./provider-session.runtime-BDjyFw6Y.js");
10424
+ const promise = discordProviderSessionRuntimePromise ?? import("./provider-session.runtime-D5pCGibh.js");
9960
10425
  discordProviderSessionRuntimePromise = promise;
9961
10426
  try {
9962
10427
  return await promise;
@@ -10218,7 +10683,7 @@ async function monitorDiscordProvider(opts = {}) {
10218
10683
  });
10219
10684
  let voiceManager = null;
10220
10685
  if (voiceEnabled) {
10221
- const { DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener } = await loadDiscordVoiceRuntime();
10686
+ const { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener } = await loadDiscordVoiceRuntime();
10222
10687
  voiceManager = new DiscordVoiceManager({
10223
10688
  client,
10224
10689
  cfg,
@@ -10227,12 +10692,13 @@ async function monitorDiscordProvider(opts = {}) {
10227
10692
  runtime,
10228
10693
  botUserId
10229
10694
  });
10230
- const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-BvvM5Ns9.js").then((n) => n.n);
10695
+ const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-CLZ5X8fh.js").then((n) => n.n);
10231
10696
  setDiscordTranscriptsVoiceManager({
10232
10697
  accountId: account.accountId,
10233
10698
  manager: voiceManager
10234
10699
  });
10235
10700
  voiceManagerRef.current = voiceManager;
10701
+ registerDiscordListener(client.listeners, new DiscordVoiceGuildCreateListener(voiceManager));
10236
10702
  registerDiscordListener(client.listeners, new DiscordVoiceReadyListener(voiceManager));
10237
10703
  registerDiscordListener(client.listeners, new DiscordVoiceResumedListener(voiceManager));
10238
10704
  registerDiscordListener(client.listeners, new DiscordVoiceStateUpdateListener(voiceManager));
@@ -10332,4 +10798,4 @@ async function monitorDiscordProvider(opts = {}) {
10332
10798
  }
10333
10799
  }
10334
10800
  //#endregion
10335
- export { resolveDiscordAutoThreadReplyPlan as C, resolveDiscordDmCommandAccess as D, sanitizeDiscordThreadName as E, resolveDiscordTextCommandAccess as O, threading_exports as S, resolveDiscordThreadStarter as T, resolveDiscordConversationRoute as _, buildGuildLabel as a, handleDiscordDmCommandDecision as b, createDiscordGatewayPlugin as c, resolveDiscordVoiceEnabled as d, createDiscordNativeCommand as f, buildDiscordRoutePeer as g, createDiscordSupplementalContextAccessChecker as h, buildDirectLabel as i, resolveDiscordGatewayIntents as l, buildDiscordInboundAccessContext as m, deliverDiscordReply as n, resolveReplyContext as o, buildDiscordGroupSystemPrompt as p, sanitizeDiscordFrontChannelReplyPayloads as r, authorizeDiscordVoiceIngress as s, monitorDiscordProvider as t, waitForDiscordGatewayPluginRegistration as u, resolveDiscordEffectiveRoute as v, resolveDiscordReplyTarget as w, registerDiscordListener as x, shouldIgnoreStaleDiscordRouteBinding as y };
10801
+ export { resolveDiscordTextCommandAccess as A, registerDiscordListener as C, resolveDiscordThreadStarter as D, resolveDiscordReplyTarget as E, sanitizeDiscordThreadName as O, handleDiscordDmCommandDecision as S, resolveDiscordAutoThreadReplyPlan as T, createDiscordSupplementalContextAccessChecker as _, buildDirectLabel as a, resolveDiscordEffectiveRoute as b, resolveDiscordVoiceOwnerAccess as c, resolveDiscordGatewayIntents as d, waitForDiscordGatewayPluginRegistration as f, buildDiscordInboundAccessContext as g, buildDiscordGroupSystemPrompt as h, sanitizeDiscordFrontChannelReplyPayloads as i, resolveDiscordDmCommandAccess as k, authorizeDiscordVoiceIngress as l, createDiscordNativeCommand as m, deliverDiscordReply as n, buildGuildLabel as o, resolveDiscordVoiceEnabled as p, formatDiscordReplyDeliveryFailure as r, resolveReplyContext as s, monitorDiscordProvider as t, createDiscordGatewayPlugin as u, buildDiscordRoutePeer as v, threading_exports as w, shouldIgnoreStaleDiscordRouteBinding as x, resolveDiscordConversationRoute as y };