@openclaw/discord 2026.7.2-beta.7 → 2026.8.1-beta.3

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 (114) hide show
  1. package/dist/{account-inspect-SXiCBCgP.js → account-inspect-D5xmUpnj.js} +49 -43
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/accounts-DNl-oCEw.js +371 -0
  4. package/dist/action-runtime-api.js +1 -1
  5. package/dist/{active-turn-thread-route-MlKWp8RD.js → active-turn-thread-route-fmHl1HNd.js} +9 -11
  6. package/dist/{activities-api-DTKsuDxf.js → activities-api-KCBAmWxD.js} +133 -175
  7. package/dist/activities-api.js +1 -1
  8. package/dist/{allow-list-DzKrtfah.js → allow-list-BDMimASD.js} +2 -3
  9. package/dist/{api-DnrK9gML.js → api-DNVbJyEy.js} +7 -2
  10. package/dist/api.js +18 -18
  11. package/dist/{approval-handler.runtime-HymvFTYn.js → approval-handler.runtime-B12RCYr8.js} +6 -6
  12. package/dist/{approval-message-safety-DEyKVsAw.js → approval-message-safety-XGFPbDoI.js} +89 -3
  13. package/dist/{audit-CjsrabP8.js → audit-Yc7eZDd6.js} +5 -5
  14. package/dist/{channel-actions-uXFTJX3o.js → channel-actions-Dr2QI8Bf.js} +11 -7
  15. package/dist/{channel-actions.runtime-CIYU90sl.js → channel-actions.runtime-CODTvo7c.js} +99 -97
  16. package/dist/{channel-vKa-76xY.js → channel-cXYzGc4L.js} +102 -104
  17. package/dist/channel-config-api.js +1 -1
  18. package/dist/channel-plugin-api.js +1 -1
  19. package/dist/{channel.setup-CmcwZ_qd.js → channel.setup-CSiwnMgs.js} +16 -54
  20. package/dist/{outbound-session-route-H-HoyH5U.js → components-ezGKo_Yh.js} +69 -142
  21. package/dist/{config-schema-WHRQcY19.js → config-schema-DC5kDtQ5.js} +42 -11
  22. package/dist/conversation-binding-route-D6rCQ5S0.js +43 -0
  23. package/dist/{directory-config-D_LojtCe.js → directory-config-XyCfoqLg.js} +2 -2
  24. package/dist/directory-contract-api.js +1 -1
  25. package/dist/{directory-live-Bw5Ephzv.js → directory-live-Df0NbFJB.js} +7 -4
  26. package/dist/{doctor-Bc4S1qyd.js → doctor-DYY31GrO.js} +34 -13
  27. package/dist/{doctor-contract-CCFMIDk9.js → doctor-contract-DsWvKIXF.js} +2 -3
  28. package/dist/doctor-contract-api.js +147 -2
  29. package/dist/{handle-action.guild-admin-D6VRRW-C.js → handle-action.guild-admin-6Tvvinsa.js} +4 -7
  30. package/dist/{inbound-event-delivery-CEPlt2uz.js → inbound-event-delivery-De2qxTrP.js} +4 -25
  31. package/dist/index.js +5 -4
  32. package/dist/{message-handler-CeiSulIw.js → message-handler-B61q5MOF.js} +118 -33
  33. package/dist/{message-handler.preflight-DibgQFf2.js → message-handler.preflight-Bzg_Hawz.js} +75 -61
  34. package/dist/{message-handler.process-Dnc_oz9y.js → message-handler.process--4_712Ec.js} +236 -296
  35. package/dist/{message-utils-BgTwT_q7.js → message-utils-BXLLnFl6.js} +34 -35
  36. package/dist/{model-picker-preference-primitives-D25pnCOu.js → model-picker-preference-primitives-BHT1LkR_.js} +3 -3
  37. package/dist/{outbound-adapter-DtE-i2OJ.js → outbound-adapter-DxHTOqiN.js} +157 -68
  38. package/dist/{outbound-components-C7GVyfPh.js → outbound-components-B2PL0ooH.js} +3 -3
  39. package/dist/owner-access-Bg41VzAf.js +107 -0
  40. package/dist/{pluralkit-CD4B11JB.js → pluralkit-dUl4v1X3.js} +1 -1
  41. package/dist/{preflight-audio-BjnQZ5yS.js → preflight-audio-DrnqFA8U.js} +22 -28
  42. package/dist/{probe-BeWKhjqX.js → probe-BTdtVRwU.js} +37 -13
  43. package/dist/probe.runtime-Dq5-PRKT.js +2 -0
  44. package/dist/{provider-DfQJX3z5.js → provider-DlK_ynqt.js} +1131 -2021
  45. package/dist/{provider-session.runtime-Bo88k0pV.js → provider-session.runtime-CvvphHvs.js} +3 -3
  46. package/dist/provider.runtime-CzKGRiAF.js +2 -0
  47. package/dist/realtime-session.runtime-ysbMG8v7.js +1351 -0
  48. package/dist/{resolve-allowlist-common-DHERApxi.js → resolve-allowlist-common-DIVXbD1P.js} +3 -3
  49. package/dist/{resolve-channels-BjepAj8b.js → resolve-channels-CGeUoIqb.js} +4 -4
  50. package/dist/{resolve-users-Bc28htgt.js → resolve-users-BtKxLUZv.js} +3 -3
  51. package/dist/{chunk-DX4jpFQP.js → retry-DNsVTeTo.js} +308 -131
  52. package/dist/{runtime-BmW_j_hl.js → runtime-CQovMsH3.js} +12 -15
  53. package/dist/{runtime-_aRC6V2s.js → runtime-D71_VFLr.js} +49 -58
  54. package/dist/runtime-api.actions.js +2 -2
  55. package/dist/runtime-api.js +23 -23
  56. package/dist/runtime-api.lookup.js +5 -5
  57. package/dist/runtime-api.monitor-CzeTYF32.js +6 -0
  58. package/dist/runtime-api.monitor.js +6 -5
  59. package/dist/runtime-api.send.js +6 -6
  60. package/dist/runtime-api.threads.js +5 -6
  61. package/dist/sdk-runtime-37OU6_Yh.js +369 -0
  62. package/dist/{secret-config-contract-B1akrlyY.js → secret-config-contract-B3i6WxV5.js} +53 -25
  63. package/dist/secret-contract-api.js +1 -1
  64. package/dist/security-audit-contract-api.js +1 -1
  65. package/dist/{security-audit.runtime-CPN0xFYz.js → security-audit.runtime-BVKF9VtY.js} +1 -1
  66. package/dist/{send-CdkZ6zeW.js → send-rQIffSYu.js} +180 -91
  67. package/dist/{send.components-5uqwkjgE.js → send.components-ChK1wS7w.js} +32 -46
  68. package/dist/{send.outbound-D-80Ej-t.js → send.outbound-HIkgji_7.js} +57 -34
  69. package/dist/{send.reactions-CtlmGd_o.js → send.reactions-gmJ6yFPo.js} +14 -21
  70. package/dist/{send.receipt-CuwRrpFz.js → send.receipt-Cnhoautb.js} +29 -1
  71. package/dist/{send.shared-jpvAG_u6.js → send.shared-B3fpaLWJ.js} +112 -181
  72. package/dist/{sender-identity-mG_zsdox.js → sender-identity-BJtfyise.js} +1 -1
  73. package/dist/setup-entry.js +0 -5
  74. package/dist/setup-plugin-api.js +1 -1
  75. package/dist/{shared-CXF1tup1.js → shared-CaAaoksy.js} +27 -23
  76. package/dist/subagent-hooks-CNT2ZJTJ.js +49 -0
  77. package/dist/subagent-hooks-api.js +2 -6
  78. package/dist/subagent-progress-FPLqBY1s.js +110 -0
  79. package/dist/{system-events-DdUqf91B.js → system-events-Nuwoq_au.js} +2 -2
  80. package/dist/{target-resolver-C3s_2m0t.js → target-resolver-DIXShHaZ.js} +3 -3
  81. package/dist/targets-B2pf49zc.js +3 -0
  82. package/dist/{thread-bindings-C-M-ly7P.js → thread-bindings-VlymUnbz.js} +29 -28
  83. package/dist/{thread-bindings.manager-DYvSlrva.js → thread-bindings.manager-CngsgErq.js} +258 -29
  84. package/dist/{thread-bindings.session-updates-DAIlp7Px.js → thread-bindings.session-updates-CJb-BpPL.js} +1 -1
  85. package/dist/{thread-bindings.state-Cgi5uRn-.js → thread-bindings.state-D8OMdk4O.js} +3 -16
  86. package/dist/threading-DU6pgeGm.js +456 -0
  87. package/dist/transcripts-source-DeIMNbRh.js +265 -0
  88. package/dist/transcripts-source-api.js +7 -2
  89. package/dist/{typing--Mzo2kM7.js → typing-oLiS5Z13.js} +1 -1
  90. package/dist/voice-runtime-CjNFaynA.js +2549 -0
  91. package/openclaw.plugin.json +215 -25
  92. package/package.json +15 -11
  93. package/skills/discord/SKILL.md +22 -115
  94. package/dist/accounts-DJTf1LtA.js +0 -219
  95. package/dist/channel-api-CAJ0wMoV.js +0 -21
  96. package/dist/legacy-state-migrations-api.js +0 -138
  97. package/dist/manager.runtime-Bjav3pfC.js +0 -3655
  98. package/dist/preflight-audio.runtime-CoCXMM8r.js +0 -7
  99. package/dist/probe.runtime-DqFGFMbZ.js +0 -2
  100. package/dist/provider.runtime-Cy7XZmyh.js +0 -2
  101. package/dist/runtime-api.monitor-CCRubd3-.js +0 -5
  102. package/dist/session-binding-contract-api.js +0 -2
  103. package/dist/subagent-hooks-Dxy7Fmnq.js +0 -120
  104. package/dist/subagent-progress-DI3sYLLo.js +0 -664
  105. package/dist/targets-Dv_RN7vT.js +0 -3
  106. package/dist/thread-bindings.discord-api-BoDNqamb.js +0 -246
  107. package/dist/transcripts-source-DIKGsTGT.js +0 -123
  108. package/node_modules/p-map/index.d.ts +0 -155
  109. package/node_modules/p-map/index.js +0 -285
  110. package/node_modules/p-map/license +0 -9
  111. package/node_modules/p-map/package.json +0 -57
  112. package/node_modules/p-map/readme.md +0 -190
  113. package/dist/{audit-core-xwjIczO0.js → audit-core-BCX8UeFH.js} +1 -1
  114. package/dist/{security-audit-jGnJF__m.js → security-audit-BfJhAXZp.js} +1 -1
@@ -1,84 +1,83 @@
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, Yt 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, t as chunkDiscordTextWithMode, tt as StringSelectMenu, v as PresenceUpdateListener, w as Plugin, x as ThreadUpdateListener, y as ReadyListener } from "./chunk-DX4jpFQP.js";
2
- import { c as resolveDiscordAccountAllowFrom, d as resolveDiscordAccountDmPolicy, f as resolveDiscordMaxLinesPerMessage, p as normalizeDiscordToken, s as resolveDiscordAccount } from "./accounts-DJTf1LtA.js";
3
- import { n as getDiscordActivitiesRuntime } from "./runtime-BmW_j_hl.js";
4
- import { C as parseDiscordActivityCustomIdForInteraction, D as parseDiscordModalCustomIdForInteraction, E as parseDiscordModalCustomId, O as decodeCustomIdComponent, T as parseDiscordComponentCustomIdForInteraction, k as encodeCustomIdComponent, l as parseExecApprovalData, s as parseDiscordQuestionData, w as parseDiscordComponentCustomId, y as buildDiscordActivityCustomId } from "./outbound-session-route-H-HoyH5U.js";
5
- import { D as canViewDiscordGuildChannel, F as createDiscordRestClient, H as registerGateway, K as validateDiscordProxyUrl, U as unregisterGateway, q as withValidatedDiscordProxy, z as DISCORD_REST_TIMEOUT_MS } from "./send.shared-jpvAG_u6.js";
6
- import { _ as resolveDiscordModalEntryWithPersistence, g as resolveDiscordComponentEntryWithPersistence, t as editDiscordComponentMessage } from "./send.components-5uqwkjgE.js";
1
+ import { A as RateLimitError, B as hasDiscordV2Components, C as ThreadDeleteListener, D as Plugin, E as Client, F as Command, G as Modal, I as CommandWithSubcommands, J as Button, L as Guild, R as Message, X as Container, _ as MessageCreateListener, a as chunkDiscordTextWithMode, at as StringSelectMenu, b as PresenceUpdateListener, g as InteractionCreateListener, h as GuildDeleteListener, it as Separator, lt as BaseMessageInteractiveComponent, m as GuildCreateListener, nt as Row, ot as TextDisplay, p as discord_exports, v as MessageReactionAddListener, w as ThreadUpdateListener, x as ReadyListener, y as MessageReactionRemoveListener, z as User } from "./retry-DNsVTeTo.js";
2
+ import { C as parseDiscordComponentCustomId, D as decodeCustomIdComponent, E as parseDiscordModalCustomIdForInteraction, O as encodeCustomIdComponent, S as parseDiscordActivityCustomIdForInteraction, T as parseDiscordModalCustomId, c as parseExecApprovalData, o as parseDiscordQuestionData, v as buildDiscordActivityCustomId, w as parseDiscordComponentCustomIdForInteraction } from "./components-ezGKo_Yh.js";
3
+ import { c as resolveDiscordAccount, f as resolveDiscordAccountDmPolicy, l as resolveDiscordAccountAllowFrom, m as normalizeDiscordToken, p as resolveDiscordMaxLinesPerMessage } from "./accounts-DNl-oCEw.js";
4
+ import { a as resolveDiscordComponentEntryWithPersistence, o as resolveDiscordModalEntryWithPersistence, t as editDiscordComponentMessage } from "./send.components-ChK1wS7w.js";
7
5
  import { t as getDiscordRuntime } from "./runtime-DgnVQ7zW.js";
8
- import { a as isDiscordRateLimitResponseBody, o as summarizeDiscordResponseBody } from "./api-DnrK9gML.js";
9
- 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, t as allowListMatches, v as resolveGroupDmAllow, x as formatDiscordUserTag, y as shouldEmitDiscordReactionNotification } from "./allow-list-DzKrtfah.js";
10
- import { a as formatMention, t as sendMessageDiscord } from "./send.outbound-D-80Ej-t.js";
6
+ import { a as isDiscordRateLimitResponseBody, o as summarizeDiscordResponseBody } from "./api-DNVbJyEy.js";
7
+ 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-BDMimASD.js";
8
+ import { F as createDiscordRestClient, G as registerGateway, H as withValidatedDiscordProxy, K as unregisterGateway, V as validateDiscordProxyUrl, b as canViewDiscordGuildChannel, z as DISCORD_REST_TIMEOUT_MS } from "./send.shared-B3fpaLWJ.js";
9
+ import { a as formatMention, t as sendMessageDiscord } from "./send.outbound-HIkgji_7.js";
10
+ import { n as getDiscordActivitiesRuntime } from "./runtime-CQovMsH3.js";
11
+ import { n as resolveDiscordVoiceEnabled, r as authorizeDiscordVoiceIngress, t as resolveDiscordVoiceAccess } from "./owner-access-Bg41VzAf.js";
11
12
  import { i as setPresence, t as clearPresences } from "./presence-cache-z1N8MbJ3.js";
12
- import { n as sendVoiceMessageDiscord } from "./send-CdkZ6zeW.js";
13
+ import { r as sendVoiceMessageDiscord } from "./send-rQIffSYu.js";
13
14
  import { c as raceWithTimeout, u as withAbortTimeout } from "./timeouts-B9cy6RvQ.js";
14
- import { a as getDiscordExecApprovalApprovers, n as formatDiscordApprovalDisplayValue, r as resolveDiscordConversationIdentity, s as isDiscordExecApprovalClientEnabled, t as DISCORD_APPROVAL_ALLOWED_MENTIONS } from "./approval-message-safety-DEyKVsAw.js";
15
- import { t as resolveDiscordChannelAllowlist } from "./resolve-channels-BjepAj8b.js";
16
- import { t as resolveDiscordUserAllowlist } from "./resolve-users-Bc28htgt.js";
17
- import { a as isThreadArchived, d as resolveDiscordChannelIdSafe, f as resolveDiscordChannelInfoSafe, g as resolveDiscordChannelTopicSafe, h as resolveDiscordChannelParentSafe, m as resolveDiscordChannelParentIdSafe, p as resolveDiscordChannelNameSafe, y as formatThreadBindingDurationLabel } from "./thread-bindings.discord-api-BoDNqamb.js";
18
- import { r as parseApplicationIdFromToken, t as fetchDiscordApplicationId } from "./probe-BeWKhjqX.js";
19
- import { i as timestampMs, n as normalizeModelRef, r as sanitizeRecentModels, t as buildPreferenceModelKey } from "./model-picker-preference-primitives-D25pnCOu.js";
20
- import { a as formatDiscordMediaText, d as resolveDiscordChannelInfo, f as resolveDiscordMessageChannelId, n as resolveDiscordForwardedMessagesTextFromSnapshots, t as resolveDiscordEmbedText } from "./message-utils-BgTwT_q7.js";
21
- import { t as resolveDiscordSenderIdentity } from "./sender-identity-mG_zsdox.js";
22
- import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
23
- import { buildAgentSessionKey, deriveLastRoutePolicy, isAcpSessionKey, isSubagentSessionKey, parseAgentSessionKey, resolveAgentIdFromSessionKey, resolveAgentRoute } from "openclaw/plugin-sdk/routing";
24
- import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeOptionalStringifiedId, normalizeStringEntries, normalizeStringEntriesLower, summarizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
25
- import { getRuntimeConfig, getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
15
+ import { a as resolveDiscordBoundConversationRoute, f as getDiscordExecApprovalApprovers, l as resolveDiscordConversationIdentity, m as isDiscordExecApprovalClientEnabled, n as formatDiscordApprovalDisplayValue, r as buildDiscordConversationRouteContext, s as resolveDiscordEffectiveRoute, t as DISCORD_APPROVAL_ALLOWED_MENTIONS } from "./approval-message-safety-XGFPbDoI.js";
16
+ import { t as resolveDiscordChannelAllowlist } from "./resolve-channels-CGeUoIqb.js";
17
+ import { t as resolveDiscordUserAllowlist } from "./resolve-users-BtKxLUZv.js";
18
+ import { a as isThreadArchived, c as resolveDiscordChannelInfoSafe, f as resolveDiscordChannelTopicSafe, h as formatThreadBindingDurationLabel, l as resolveDiscordChannelNameSafe, r as getThreadBindingManager, s as resolveDiscordChannelIdSafe, u as resolveDiscordChannelParentIdSafe } from "./thread-bindings.manager-CngsgErq.js";
19
+ import { a as probeDiscordApplicationId, r as parseApplicationIdFromToken } from "./probe-BTdtVRwU.js";
20
+ import { i as sanitizeRecentModels, n as normalizeModelRef, r as preferenceTimestampMs, t as buildPreferenceModelKey } from "./model-picker-preference-primitives-BHT1LkR_.js";
21
+ import { d as resolveDiscordMessageStickers, f as resolveDiscordChannelInfo } from "./message-utils-BXLLnFl6.js";
22
+ import { i as resolveDiscordThreadParentInfo } from "./threading-DU6pgeGm.js";
23
+ import { t as resolveDiscordSenderIdentity } from "./sender-identity-BJtfyise.js";
26
24
  import { logDebug, logError } from "openclaw/plugin-sdk/logging-core";
27
25
  import { ApplicationCommandOptionType, ButtonStyle, ChannelType, ComponentType, GatewayCloseCodes, GatewayCloseCodes as GatewayCloseCodes$1, GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes } from "discord-api-types/v10";
28
26
  import { createHash, randomUUID } from "node:crypto";
29
- import { MAX_DATE_TIMESTAMP_MS, asDateTimestampMs, parseFiniteNumber, parseStrictFiniteNumber, parseStrictInteger, parseStrictNonNegativeInteger, parseStrictPositiveInteger, resolveDateTimestampMs, resolveTimestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
27
+ import { MAX_DATE_TIMESTAMP_MS, MAX_TIMER_TIMEOUT_MS, asDateTimestampMs, asSafeIntegerInRange, parseFiniteNumber, parseStrictInteger, parseStrictPositiveInteger, resolveDateTimestampMs, resolveTimestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
30
28
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
31
29
  import { Type } from "typebox";
32
30
  import { Check, Errors } from "typebox/value";
33
31
  import { inspect } from "node:util";
34
32
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
35
- import { PlatformMessageNotDispatchedError, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
33
+ import { PlatformMessageNotDispatchedError, formatErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
36
34
  import { fetchWithSsrFGuard, formatErrorMessage as formatErrorMessage$1 } from "openclaw/plugin-sdk/ssrf-runtime";
35
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
36
+ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
37
+ import { asFiniteNumber, asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, parseFiniteNumber as parseFiniteNumber$1, parseStrictNonNegativeInteger as parseStrictNonNegativeInteger$1, summarizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
38
+ import { getRuntimeConfig, getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
37
39
  import { createHttp1EnvHttpProxyAgent, createHttp1ProxyAgent, createNodeProxyAgent, resolveEnvHttpProxyAgentOptions, wrapFetchWithAbortSignal } from "openclaw/plugin-sdk/fetch-runtime";
38
40
  import { createNonExitingRuntime, createSubsystemLogger, danger, formatDurationSeconds, isVerbose, logVerbose, shouldLogVerbose, sleepWithAbort, warn } from "openclaw/plugin-sdk/runtime-env";
39
41
  import { sliceUtf16Safe, truncateUtf16Safe, withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
40
42
  import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, canonicalizeAllowlistWithResolvedIds, patchAllowlistUsersInConfigEntries, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
41
43
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
42
44
  import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
43
- import { chunkItems, sanitizeAssistantVisibleText, sanitizeAssistantVisibleTextWithProfile } from "openclaw/plugin-sdk/text-chunking";
44
- import { createReplyReferencePlanner } from "openclaw/plugin-sdk/reply-reference";
45
- import { buildOutboundSessionContext, resolveChannelStreamingBlockEnabled, sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound";
45
+ import { chunkItems, findCodeRegions, sanitizeAssistantVisibleText, sanitizeAssistantVisibleTextWithProfile } from "openclaw/plugin-sdk/text-chunking";
46
+ import { buildOutboundSessionContext, listMessageReceiptPlatformIds, resolveChannelStreamingBlockEnabled, sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound";
46
47
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
47
48
  import { resolveSendableOutboundReplyParts, resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
48
49
  import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
49
- import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
50
- import { resolvePinnedMainDmOwnerFromAllowlist as resolvePinnedMainDmOwnerFromAllowlist$1 } from "openclaw/plugin-sdk/security-runtime";
50
+ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
51
+ import { buildCommandTextFromArgs, findCommandByNativeName, formatCommandArgMenuTitle, listChatCommands, listNativeCommandSpecsForConfig, listSkillCommandsForAgents, resolveCommandAuthorizedFromAuthorizers, resolveEffectiveAgentRuntime, resolveNativeCommandSessionTargets, resolveStoredModelOverride, serializeCommandArgs } from "openclaw/plugin-sdk/command-auth-native";
51
52
  import { GROUP_POLICY_BLOCKED_LABEL, resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
53
+ import { resolvePinnedMainDmOwnerFromAllowlist as resolvePinnedMainDmOwnerFromAllowlist$1 } from "openclaw/plugin-sdk/security-runtime";
52
54
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
53
55
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
54
- import { questionGatewayRuntime } from "openclaw/plugin-sdk/question-gateway-runtime";
55
56
  import { buildPairingReply, upsertChannelPairingRequest, upsertChannelPairingRequest as upsertChannelPairingRequest$1 } from "openclaw/plugin-sdk/conversation-runtime";
56
- import { clearExpiredCooldowns, ensureAuthProfileStore, formatReasoningMessage, getPreparedModelCatalogSnapshot, isProfileInCooldown, loadPreparedModelCatalog, resolveAgentAvatar, resolveAgentDir, resolveDefaultModelForAgent, resolveHumanDelayConfig, resolveProfilesUnavailableReason } from "openclaw/plugin-sdk/agent-runtime";
57
+ import * as conversationRuntime from "openclaw/plugin-sdk/conversation-binding-runtime";
58
+ import { questionGatewayRuntime } from "openclaw/plugin-sdk/question-gateway-runtime";
59
+ import { clearExpiredCooldowns, ensureAuthProfileStore, formatReasoningMessage, getPreparedModelCatalogSnapshot, isProfileInCooldown, listAgentIds, loadPreparedModelCatalog, resolveAgentAvatar, resolveAgentDir, resolveDefaultModelForAgent, resolveHumanDelayConfig, resolveProfilesUnavailableReason } from "openclaw/plugin-sdk/agent-runtime";
57
60
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
58
61
  import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
59
- import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
60
62
  import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
61
- import { enqueueSystemEvent, enqueueSystemEvent as enqueueSystemEvent$1 } from "openclaw/plugin-sdk/system-event-runtime";
63
+ import { enqueueRoutedSystemEvent, enqueueRoutedSystemEvent as enqueueRoutedSystemEvent$1 } from "openclaw/plugin-sdk/system-event-runtime";
64
+ import { deleteSessionEntry, getSessionEntry, listSessionEntries, readSessionUpdatedAt as readSessionUpdatedAt$1, resolveStorePath, resolveStorePath as resolveStorePath$1 } from "openclaw/plugin-sdk/session-store-runtime";
62
65
  import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
63
66
  import { createChannelPartialDeliveryError, dispatchChannelInboundTurn, formatInboundEnvelope, hasVisibleInboundReplyDispatch, isChannelPartialDeliveryError, resolveEnvelopeFormatOptions, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent } from "openclaw/plugin-sdk/channel-inbound";
64
- import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolveChannelModelOverride } from "openclaw/plugin-sdk/model-session-runtime";
65
- import { completeWithPreparedSimpleCompletionModel, extractAssistantText, prepareSimpleCompletionModelForAgent } from "openclaw/plugin-sdk/simple-completion-runtime";
66
- import { deleteSessionEntry, getSessionEntry, listSessionEntries, patchSessionEntry, readSessionUpdatedAt as readSessionUpdatedAt$1, resolveStorePath, resolveStorePath as resolveStorePath$1 } from "openclaw/plugin-sdk/session-store-runtime";
67
- import { buildCommandTextFromArgs, findCommandByNativeName, formatCommandArgMenuTitle, listChatCommands, listNativeCommandSpecsForConfig, listSkillCommandsForAgents, resolveCommandAuthorizedFromAuthorizers, resolveEffectiveAgentRuntime, resolveNativeCommandSessionTargets, resolveStoredModelOverride, serializeCommandArgs } from "openclaw/plugin-sdk/command-auth-native";
68
- import { buildCommandTextFromArgs as buildCommandTextFromArgs$1, findCommandByNativeName as findCommandByNativeName$1, parseCommandArgs, resolveCommandArgChoices, resolveCommandArgMenu, serializeCommandArgs as serializeCommandArgs$1 } from "openclaw/plugin-sdk/native-command-registry";
69
- import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
70
- import * as pluginRuntime from "openclaw/plugin-sdk/plugin-runtime";
71
- import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
72
- import * as conversationRuntime from "openclaw/plugin-sdk/conversation-binding-runtime";
73
67
  import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
74
- import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
68
+ import { buildCommandTextFromArgs as buildCommandTextFromArgs$1, findCommandByNativeName as findCommandByNativeName$1, mergeNativeCommandSpecs, parseCommandArgs, resolveCommandArgChoices, resolveCommandArgMenu, serializeCommandArgs as serializeCommandArgs$1 } from "openclaw/plugin-sdk/native-command-registry";
69
+ import { PLUGIN_COMMAND_DISPATCH } from "openclaw/plugin-sdk/plugin-command-runtime";
70
+ import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
71
+ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
75
72
  import { EventEmitter } from "node:events";
76
73
  import * as ws from "ws";
77
74
  import { Agent } from "node:https";
78
75
  import { captureHttpExchange, captureWsEvent, resolveDebugProxySettings, resolveEffectiveDebugProxyUrl } from "openclaw/plugin-sdk/proxy-capture";
79
76
  import * as dns from "node:dns";
77
+ import { channelReadyPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
80
78
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
81
79
  import { stripPlainTextToolCallBlocks } from "openclaw/plugin-sdk/tool-payload";
80
+ import { createChannelInteractiveDispatcher } from "openclaw/plugin-sdk/plugin-runtime";
82
81
  import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
83
82
  import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
84
83
  import { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/runtime-fetch";
@@ -141,6 +140,205 @@ async function runDiscordListenerWithSlowLog(params) {
141
140
  }
142
141
  }
143
142
  //#endregion
143
+ //#region extensions/discord/src/monitor/presence-events.ts
144
+ const DISCORD_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
145
+ function isDiscordOnlineStatus(status) {
146
+ return status === "online" || status === "idle" || status === "dnd";
147
+ }
148
+ function isDiscordOfflineStatus(status) {
149
+ return status === "offline";
150
+ }
151
+ function resolveDiscordOnlinePresenceEvent(params) {
152
+ const config = params.config;
153
+ const userId = params.data.user?.id?.trim();
154
+ if (!config || config.enabled === false || !userId || userId === params.botUserId || params.data.user.bot === true || !isDiscordOnlineStatus(params.data.status) || !params.availabilityKind) return null;
155
+ if (config.users !== void 0 && !config.users.includes(userId)) return null;
156
+ if (params.lastEmittedAtMs !== void 0 && params.nowMs - params.lastEmittedAtMs < 288e5) return null;
157
+ const lines = [
158
+ "Discord online-presence event:",
159
+ 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.`,
160
+ `The authorized greeting target is channel_id=${JSON.stringify(config.channelId)}.`,
161
+ "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.",
162
+ "Send at most one short, natural greeting to the target channel. Do not reveal private memory. If no greeting is appropriate, stay silent."
163
+ ];
164
+ return {
165
+ channelId: config.channelId,
166
+ userId,
167
+ text: lines.join("\n")
168
+ };
169
+ }
170
+ //#endregion
171
+ //#region extensions/discord/src/monitor/presence-cooldown-store.ts
172
+ const DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
173
+ function openDiscordPresenceCooldownStore() {
174
+ return getDiscordRuntime().state.openSyncKeyedStore({
175
+ namespace: "presence-greeting-cooldowns",
176
+ maxEntries: DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES,
177
+ overflowPolicy: "reject-new",
178
+ defaultTtlMs: DISCORD_PRESENCE_GREETING_COOLDOWN_MS
179
+ });
180
+ }
181
+ //#endregion
182
+ //#region extensions/discord/src/monitor/presence-emission-gate.ts
183
+ const DISCORD_PRESENCE_RECONNECT_SUPPRESS_MS = 300 * 1e3;
184
+ const DISCORD_PRESENCE_BURST_LIMIT = 8;
185
+ const DISCORD_PRESENCE_BURST_WINDOW_MS = 60 * 1e3;
186
+ function resolveDiscordPresenceGateOptions(config) {
187
+ return {
188
+ reconnectSuppressMs: config?.reconnectSuppressSeconds !== void 0 ? config.reconnectSuppressSeconds * 1e3 : DISCORD_PRESENCE_RECONNECT_SUPPRESS_MS,
189
+ burstLimit: config?.burstLimit ?? DISCORD_PRESENCE_BURST_LIMIT,
190
+ burstWindowMs: config?.burstWindowSeconds !== void 0 ? config.burstWindowSeconds * 1e3 : DISCORD_PRESENCE_BURST_WINDOW_MS
191
+ };
192
+ }
193
+ /**
194
+ * Per-account lifecycle gate for online-presence events. Reconnect state belongs to the Gateway
195
+ * session, while burst state follows the guild-owned configuration that supplies its limits.
196
+ */
197
+ var DiscordPresenceEmissionGate = class {
198
+ constructor() {
199
+ this.reconnectLogged = false;
200
+ this.burstByGuild = /* @__PURE__ */ new Map();
201
+ this.nextReservationId = 0;
202
+ }
203
+ noteGatewaySessionReset(nowMs) {
204
+ this.lastSessionResetAtMs = nowMs;
205
+ this.reconnectLogged = false;
206
+ }
207
+ evaluateReconnectWindow(nowMs, options) {
208
+ if (this.lastSessionResetAtMs !== void 0 && options.reconnectSuppressMs > 0 && nowMs - this.lastSessionResetAtMs < options.reconnectSuppressMs) {
209
+ const shouldLog = !this.reconnectLogged;
210
+ this.reconnectLogged = true;
211
+ return {
212
+ allowed: false,
213
+ reason: "reconnect-window",
214
+ shouldLog
215
+ };
216
+ }
217
+ return { allowed: true };
218
+ }
219
+ reserveBurst(guildId, nowMs, options) {
220
+ const state = this.burstByGuild.get(guildId) ?? {
221
+ reservations: [],
222
+ logged: false
223
+ };
224
+ state.reservations = state.reservations.filter((reservation) => !reservation.committed || nowMs - reservation.atMs < options.burstWindowMs);
225
+ this.burstByGuild.set(guildId, state);
226
+ if (state.reservations.length >= options.burstLimit) {
227
+ const shouldLog = !state.logged;
228
+ state.logged = true;
229
+ return {
230
+ allowed: false,
231
+ reason: state.reservations.some((reservation) => !reservation.committed) ? "burst-pending" : "burst",
232
+ shouldLog
233
+ };
234
+ }
235
+ state.logged = false;
236
+ const reservation = {
237
+ id: this.nextReservationId++,
238
+ atMs: nowMs,
239
+ committed: false
240
+ };
241
+ state.reservations.push(reservation);
242
+ return {
243
+ allowed: true,
244
+ reservation: reservation.id
245
+ };
246
+ }
247
+ commitBurst(guildId, reservation, nowMs) {
248
+ const entry = this.burstByGuild.get(guildId)?.reservations.find((candidate) => candidate.id === reservation);
249
+ if (!entry) return;
250
+ entry.atMs = nowMs;
251
+ entry.committed = true;
252
+ }
253
+ releaseBurst(guildId, reservation) {
254
+ const state = this.burstByGuild.get(guildId);
255
+ if (!state) return;
256
+ state.reservations = state.reservations.filter((candidate) => candidate.id !== reservation);
257
+ if (state.reservations.length === 0 && !state.logged) this.burstByGuild.delete(guildId);
258
+ }
259
+ };
260
+ //#endregion
261
+ //#region extensions/discord/src/monitor/presence-transition-cache.ts
262
+ const DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES = 75e3;
263
+ var DiscordPresenceBaselineCache = class {
264
+ constructor(maxEntries = DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES) {
265
+ this.maxEntries = maxEntries;
266
+ this.offlineByKey = /* @__PURE__ */ new Map();
267
+ this.onlineByKey = /* @__PURE__ */ new Map();
268
+ }
269
+ clear() {
270
+ this.offlineByKey.clear();
271
+ this.onlineByKey.clear();
272
+ }
273
+ clearScope(scope) {
274
+ for (const markers of [this.offlineByKey, this.onlineByKey]) for (const [key, markerScope] of markers) if (markerScope === scope) markers.delete(key);
275
+ }
276
+ isOffline(scope, key) {
277
+ return this.offlineByKey.get(key) === scope;
278
+ }
279
+ isOnline(scope, key) {
280
+ return this.onlineByKey.get(key) === scope;
281
+ }
282
+ observeOffline(scope, key) {
283
+ this.deleteMarker(this.onlineByKey, scope, key);
284
+ return this.observe(this.offlineByKey, scope, key);
285
+ }
286
+ observeOnline(scope, key) {
287
+ this.deleteMarker(this.offlineByKey, scope, key);
288
+ return this.observe(this.onlineByKey, scope, key);
289
+ }
290
+ deleteMarker(markers, scope, key) {
291
+ if (markers.get(key) === scope) markers.delete(key);
292
+ }
293
+ observe(markers, scope, key) {
294
+ markers.delete(key);
295
+ markers.set(key, scope);
296
+ let evictedScope;
297
+ while (markers.size > this.maxEntries) {
298
+ const oldestKey = markers.keys().next().value;
299
+ if (oldestKey === void 0) break;
300
+ evictedScope = markers.get(oldestKey);
301
+ markers.delete(oldestKey);
302
+ }
303
+ return evictedScope;
304
+ }
305
+ };
306
+ //#endregion
307
+ //#region extensions/discord/src/monitor/thread-session-close.ts
308
+ /**
309
+ * Closes every session entry in the store whose key contains {@link threadId}.
310
+ * The explicit lifecycle deletion archives the old transcript and guarantees
311
+ * that a later inbound message starts a fresh session in every reset mode.
312
+ */
313
+ async function closeDiscordThreadSessions(params) {
314
+ const { cfg, threadId } = params;
315
+ const normalizedThreadId = normalizeOptionalLowercaseString(threadId) ?? "";
316
+ if (!normalizedThreadId) return 0;
317
+ const segmentRe = new RegExp(`:${normalizedThreadId}(?::|$)`, "i");
318
+ function sessionKeyContainsThreadId(key) {
319
+ return segmentRe.test(key);
320
+ }
321
+ let resetCount = 0;
322
+ for (const agentId of listAgentIds(cfg)) {
323
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
324
+ for (const { sessionKey, entry } of listSessionEntries({
325
+ agentId,
326
+ storePath,
327
+ readOnly: true
328
+ })) {
329
+ if (!sessionKeyContainsThreadId(sessionKey)) continue;
330
+ if (await deleteSessionEntry({
331
+ archiveTranscript: true,
332
+ expectedSessionId: entry.sessionId ?? null,
333
+ expectedUpdatedAt: entry.updatedAt,
334
+ sessionKey,
335
+ storePath
336
+ })) resetCount += 1;
337
+ }
338
+ }
339
+ return resetCount;
340
+ }
341
+ //#endregion
144
342
  //#region extensions/discord/src/monitor/dm-command-auth.ts
145
343
  const DISCORD_ALLOW_LIST_PREFIXES = [
146
344
  "discord:",
@@ -253,8 +451,11 @@ async function resolveDiscordDmCommandAccess(params) {
253
451
  subject: createDiscordDmIngressSubject(params.sender),
254
452
  conversation: {
255
453
  kind: "direct",
256
- id: params.sender.id
454
+ id: params.conversationId ?? params.sender.id,
455
+ parentId: params.conversationParentId,
456
+ threadId: params.conversationThreadId
257
457
  },
458
+ ...params.contextBinding ? { contextBinding: params.contextBinding } : {},
258
459
  event: {
259
460
  kind: params.eventKind ?? "native-command",
260
461
  authMode: "inbound",
@@ -275,7 +476,7 @@ async function resolveDiscordTextCommandAccess(params) {
275
476
  const memberAccessGroup = "discord-member-access";
276
477
  const commandGroup = params.memberAccessConfigured ? [`accessGroup:${memberAccessGroup}`] : [];
277
478
  const accessGroupMembership = params.memberAccessConfigured ? [syntheticAccessGroupMembership(memberAccessGroup, params.memberAllowed)] : [];
278
- return (await createDiscordIngressResolver({
479
+ return await createDiscordIngressResolver({
279
480
  accountId: params.accountId,
280
481
  cfg: params.cfg,
281
482
  token: params.token,
@@ -283,9 +484,12 @@ async function resolveDiscordTextCommandAccess(params) {
283
484
  }).command({
284
485
  subject: createDiscordDmIngressSubject(params.sender),
285
486
  conversation: {
286
- kind: "group",
287
- id: "discord-command"
487
+ kind: "channel",
488
+ id: params.conversationId ?? "discord-command",
489
+ parentId: params.conversationParentId,
490
+ threadId: params.conversationThreadId
288
491
  },
492
+ ...params.contextBinding ? { contextBinding: params.contextBinding } : {},
289
493
  accessGroupMembership,
290
494
  dmPolicy: "allowlist",
291
495
  groupPolicy: "allowlist",
@@ -297,525 +501,38 @@ async function resolveDiscordTextCommandAccess(params) {
297
501
  hasControlCommand: params.hasControlCommand,
298
502
  modeWhenAccessGroupsOff: "configured"
299
503
  }
300
- })).commandAccess;
301
- }
302
- //#endregion
303
- //#region extensions/discord/src/monitor/thread-title.ts
304
- const DEFAULT_THREAD_TITLE_TIMEOUT_MS = 6e4;
305
- const MAX_THREAD_TITLE_SOURCE_CHARS = 600;
306
- const MAX_THREAD_TITLE_CHANNEL_NAME_CHARS = 120;
307
- const MAX_THREAD_TITLE_CHANNEL_DESCRIPTION_CHARS = 320;
308
- const DISCORD_THREAD_TITLE_MAX_TOKENS = 4096;
309
- const DISCORD_THREAD_TITLE_SYSTEM_PROMPT = "Generate a concise Discord thread title (3-6 words). Return only the title. Use channel context when provided and avoid redundant channel-name words unless needed for clarity.";
310
- async function generateThreadTitle(params) {
311
- const sourceText = params.messageText.trim();
312
- if (!sourceText) return null;
313
- const prepared = await prepareSimpleCompletionModelForAgent({
314
- cfg: params.cfg,
315
- agentId: params.agentId,
316
- ...params.modelRef ? { modelRef: params.modelRef } : {},
317
- useUtilityModel: true,
318
- allowMissingApiKeyModes: ["aws-sdk"]
319
- });
320
- if ("error" in prepared) {
321
- const modelLabel = prepared.selection ? `${prepared.selection.provider}/${prepared.selection.modelId}` : "unknown";
322
- logVerbose(`thread-title: ${prepared.error} (agent=${params.agentId}, model=${modelLabel})`);
323
- return null;
324
- }
325
- try {
326
- const userMessage = buildThreadTitleCompletionUserMessage({
327
- sourceText,
328
- channelName: params.channelName,
329
- channelDescription: params.channelDescription
330
- });
331
- const timeoutMs = resolveThreadTitleTimeoutMs(params.timeoutMs);
332
- return normalizeGeneratedThreadTitle(extractAssistantText(await completeThreadTitle({
333
- model: prepared.model,
334
- auth: prepared.auth,
335
- userMessage,
336
- timeoutMs
337
- }))) || null;
338
- } catch (err) {
339
- logVerbose(`thread-title: title generation failed for agent ${params.agentId}: ${String(err)}`);
340
- return null;
341
- }
342
- }
343
- async function completeThreadTitle(params) {
344
- const maxTokens = Math.min(DISCORD_THREAD_TITLE_MAX_TOKENS, Math.floor(params.model.maxTokens));
345
- return await withAbortTimeout({
346
- timeoutMs: params.timeoutMs,
347
- createTimeoutError: () => /* @__PURE__ */ new Error(`thread-title timed out after ${params.timeoutMs}ms`),
348
- run: async (signal) => await completeWithPreparedSimpleCompletionModel({
349
- model: params.model,
350
- auth: params.auth,
351
- context: {
352
- systemPrompt: DISCORD_THREAD_TITLE_SYSTEM_PROMPT,
353
- messages: [{
354
- role: "user",
355
- content: params.userMessage,
356
- timestamp: Date.now()
357
- }]
358
- },
359
- options: {
360
- maxTokens,
361
- signal
362
- }
363
- })
364
- });
365
- }
366
- function buildThreadTitleCompletionUserMessage(params) {
367
- const sourceText = truncateThreadTitleSourceText(params.sourceText);
368
- const channelName = normalizeTitleContextField(params.channelName, MAX_THREAD_TITLE_CHANNEL_NAME_CHARS);
369
- const channelDescription = normalizeTitleContextField(params.channelDescription, MAX_THREAD_TITLE_CHANNEL_DESCRIPTION_CHARS);
370
- const messageLines = [];
371
- if (channelName) messageLines.push(`Channel: ${channelName}`);
372
- if (channelDescription) messageLines.push(`Channel description: ${channelDescription}`);
373
- messageLines.push(`Message:\n${sourceText}`);
374
- return messageLines.join("\n\n");
375
- }
376
- function truncateThreadTitleSourceText(sourceText) {
377
- if (sourceText.length <= MAX_THREAD_TITLE_SOURCE_CHARS) return sourceText;
378
- return `${truncateUtf16Safe(sourceText, MAX_THREAD_TITLE_SOURCE_CHARS)}...`;
379
- }
380
- function resolveThreadTitleTimeoutMs(timeoutMs) {
381
- return Math.max(100, Math.floor(timeoutMs ?? DEFAULT_THREAD_TITLE_TIMEOUT_MS));
382
- }
383
- function normalizeGeneratedThreadTitle(raw) {
384
- const lines = raw.replace(/\r/g, "").split("\n");
385
- let firstLine = "";
386
- for (const line of lines) {
387
- const trimmed = line.trim();
388
- if (!trimmed) continue;
389
- if (!firstLine && trimmed.startsWith("```")) continue;
390
- firstLine = trimmed;
391
- break;
392
- }
393
- return stripThreadTitleWrappers(firstLine);
394
- }
395
- function stripThreadTitleWrappers(raw) {
396
- let current = raw.trim();
397
- let previous = "";
398
- while (current && current !== previous) {
399
- previous = current;
400
- current = current.replace(/^["'`]+|["'`]+$/g, "").trim();
401
- current = stripBalancedWrapper(current, "**");
402
- current = stripBalancedWrapper(current, "__");
403
- current = stripBalancedWrapper(current, "*");
404
- current = stripBalancedWrapper(current, "_");
405
- current = stripBalancedWrapper(current, "~~");
406
- }
407
- return current;
408
- }
409
- function stripBalancedWrapper(text, marker) {
410
- if (text.length < marker.length * 2 + 1) return text;
411
- if (!text.startsWith(marker) || !text.endsWith(marker)) return text;
412
- const inner = text.slice(marker.length, text.length - marker.length);
413
- if (!inner || inner.includes(marker)) return text;
414
- return inner;
415
- }
416
- function normalizeTitleContextField(raw, maxChars) {
417
- const value = raw?.trim();
418
- if (!value) return;
419
- const singleLine = value.replace(/\s+/g, " ");
420
- if (singleLine.length <= maxChars) return singleLine;
421
- return `${truncateUtf16Safe(singleLine, maxChars)}...`;
422
- }
423
- //#endregion
424
- //#region extensions/discord/src/monitor/threading.cache.ts
425
- const DISCORD_THREAD_STARTER_CACHE_TTL_MS = 300 * 1e3;
426
- const DISCORD_THREAD_STARTER_CACHE_MAX = 500;
427
- const DISCORD_THREAD_STARTER_CACHE = /* @__PURE__ */ new Map();
428
- function resetDiscordThreadStarterCacheForTest() {
429
- DISCORD_THREAD_STARTER_CACHE.clear();
430
- }
431
- function getCachedThreadStarter(key, now) {
432
- const entry = DISCORD_THREAD_STARTER_CACHE.get(key);
433
- if (!entry) return;
434
- if (now - entry.updatedAt > DISCORD_THREAD_STARTER_CACHE_TTL_MS) {
435
- DISCORD_THREAD_STARTER_CACHE.delete(key);
436
- return;
437
- }
438
- DISCORD_THREAD_STARTER_CACHE.delete(key);
439
- DISCORD_THREAD_STARTER_CACHE.set(key, {
440
- ...entry,
441
- updatedAt: now
442
- });
443
- return entry.value;
444
- }
445
- function setCachedThreadStarter(key, value, now) {
446
- DISCORD_THREAD_STARTER_CACHE.delete(key);
447
- DISCORD_THREAD_STARTER_CACHE.set(key, {
448
- value,
449
- updatedAt: now
450
504
  });
451
- while (DISCORD_THREAD_STARTER_CACHE.size > DISCORD_THREAD_STARTER_CACHE_MAX) {
452
- const iter = DISCORD_THREAD_STARTER_CACHE.keys().next();
453
- if (iter.done) break;
454
- DISCORD_THREAD_STARTER_CACHE.delete(iter.value);
455
- }
456
505
  }
457
506
  //#endregion
458
- //#region extensions/discord/src/monitor/threading.starter.ts
459
- function isDiscordThreadType(type) {
507
+ //#region extensions/discord/src/monitor/thread-channel-context.ts
508
+ function isDiscordThreadChannelType(type) {
460
509
  return type === discord_exports.ChannelType.PublicThread || type === discord_exports.ChannelType.PrivateThread || type === discord_exports.ChannelType.AnnouncementThread;
461
510
  }
462
- function isDiscordForumParentType(parentType) {
463
- return parentType === discord_exports.ChannelType.GuildForum || parentType === discord_exports.ChannelType.GuildMedia;
464
- }
465
- function resolveDiscordThreadChannel(params) {
466
- if (!params.isGuildMessage) return null;
467
- const { message, channelInfo } = params;
468
- const channel = "channel" in message ? message.channel : void 0;
469
- if (channel && typeof channel === "object" && "isThread" in channel && typeof channel.isThread === "function" && channel.isThread()) return channel;
470
- if (!isDiscordThreadType(channelInfo?.type)) return null;
471
- const messageChannelId = params.messageChannelId || resolveDiscordMessageChannelId({ message });
472
- if (!messageChannelId) return null;
473
- return {
474
- id: messageChannelId,
475
- name: channelInfo?.name ?? void 0,
476
- parentId: channelInfo?.parentId ?? void 0,
477
- parent: void 0,
478
- ownerId: channelInfo?.ownerId ?? void 0
479
- };
480
- }
481
- async function resolveDiscordThreadParentInfo(params) {
482
- const { threadChannel, channelInfo, client } = params;
483
- const parent = resolveDiscordChannelParentSafe(threadChannel);
484
- let parentId = resolveDiscordChannelParentIdSafe(threadChannel) ?? resolveDiscordChannelIdSafe(parent) ?? channelInfo?.parentId ?? void 0;
485
- if (!parentId && threadChannel.id) parentId = (await resolveDiscordChannelInfo(client, threadChannel.id))?.parentId ?? void 0;
486
- if (!parentId) return {};
487
- let parentName = resolveDiscordChannelNameSafe(parent);
488
- const parentInfo = await resolveDiscordChannelInfo(client, parentId);
489
- parentName = parentName ?? parentInfo?.name;
490
- const parentType = parentInfo?.type;
511
+ function buildFetchedChannelInfo(channel) {
512
+ const channelInfo = resolveDiscordChannelInfoSafe(channel);
513
+ if (channelInfo.type === void 0) return null;
491
514
  return {
492
- id: parentId,
493
- name: parentName,
494
- type: parentType
515
+ type: channelInfo.type,
516
+ name: channelInfo.name,
517
+ topic: channelInfo.topic,
518
+ parentId: channelInfo.parentId,
519
+ ownerId: channelInfo.ownerId
495
520
  };
496
521
  }
497
- async function resolveDiscordThreadStarter(params) {
498
- const cacheKey = params.channel.id;
499
- const cached = getCachedThreadStarter(cacheKey, Date.now());
500
- if (cached) return cached;
501
- try {
502
- const messageChannelId = resolveDiscordThreadStarterMessageChannelId(params);
503
- if (!messageChannelId) return null;
504
- const starter = await fetchDiscordThreadStarterMessage({
505
- client: params.client,
506
- messageChannelId,
507
- threadId: params.channel.id
508
- });
509
- if (!starter) return null;
510
- const payload = buildDiscordThreadStarterPayload({
511
- starter,
512
- resolveTimestampMs: params.resolveTimestampMs
513
- });
514
- if (!payload) return null;
515
- setCachedThreadStarter(cacheKey, payload, Date.now());
516
- return payload;
517
- } catch {
518
- return null;
519
- }
520
- }
521
- function resolveDiscordThreadStarterMessageChannelId(params) {
522
- return isDiscordForumParentType(params.parentType) ? params.channel.id : params.parentId;
523
- }
524
- async function fetchDiscordThreadStarterMessage(params) {
525
- const starter = await getChannelMessage(params.client.rest, params.messageChannelId, params.threadId);
526
- return starter ? starter : null;
527
- }
528
- function buildDiscordThreadStarterPayload(params) {
529
- const text = resolveDiscordThreadStarterText(params.starter);
530
- if (!text) return null;
531
- return {
532
- text,
533
- ...resolveDiscordThreadStarterIdentity(params.starter),
534
- timestamp: params.resolveTimestampMs(params.starter.timestamp) ?? void 0
535
- };
536
- }
537
- function resolveDiscordThreadStarterText(starter) {
538
- const content = normalizeOptionalString(starter.content) ?? "";
539
- const embedText = resolveDiscordEmbedText(starter.embeds?.[0]);
540
- const forwardedText = resolveDiscordForwardedMessagesTextFromSnapshots(starter.message_snapshots);
541
- return [content || embedText || forwardedText, formatDiscordMediaText({
542
- attachments: starter.attachments ?? void 0,
543
- stickers: starter.sticker_items ?? void 0
544
- })].filter(Boolean).join("\n");
545
- }
546
- function resolveDiscordThreadStarterIdentity(starter) {
547
- return {
548
- author: resolveDiscordThreadStarterAuthor(starter),
549
- authorId: starter.author?.id ?? void 0,
550
- authorName: starter.author?.username ?? void 0,
551
- authorTag: resolveDiscordThreadStarterAuthorTag(starter.author),
552
- memberRoleIds: resolveDiscordThreadStarterRoleIds(starter.member)
553
- };
554
- }
555
- function resolveDiscordThreadStarterAuthor(starter) {
556
- return starter.member?.nick ?? starter.member?.displayName ?? resolveDiscordThreadStarterAuthorTag(starter.author) ?? starter.author?.username ?? starter.author?.id ?? "Unknown";
557
- }
558
- function resolveDiscordThreadStarterAuthorTag(author) {
559
- if (!author?.username || !author.discriminator) return;
560
- if (author.discriminator !== "0") return `${author.username}#${author.discriminator}`;
561
- return author.username;
562
- }
563
- function resolveDiscordThreadStarterRoleIds(member) {
564
- return Array.isArray(member?.roles) ? member.roles : void 0;
565
- }
566
- function resolveDiscordReplyTarget(opts) {
567
- if (opts.replyToMode === "off") return;
568
- const replyToId = normalizeOptionalString(opts.replyToId);
569
- if (!replyToId) return;
570
- if (opts.replyToMode === "all") return replyToId;
571
- return opts.hasReplied ? void 0 : replyToId;
572
- }
573
- function sanitizeDiscordThreadName(rawName, fallbackId) {
574
- return truncateUtf16Safe(truncateUtf16Safe(rawName.replace(/<@!?\d+>/g, "").replace(/<@&\d+>/g, "").replace(/<#\d+>/g, "").replace(/\s+/g, " ").trim() || `Thread ${fallbackId}`, 80), 100) || `Thread ${fallbackId}`;
575
- }
576
- function resolveDiscordReplyDeliveryPlan(params) {
577
- const originalReplyTarget = params.replyTarget;
578
- let deliverTarget = originalReplyTarget;
579
- let replyTarget = originalReplyTarget;
580
- if (params.createdThreadId) {
581
- deliverTarget = `channel:${params.createdThreadId}`;
582
- replyTarget = deliverTarget;
583
- }
584
- const allowReference = deliverTarget === originalReplyTarget;
585
- const replyReference = createReplyReferencePlanner({
586
- replyToMode: allowReference ? params.replyToMode : "off",
587
- existingId: params.threadChannel ? params.messageId : void 0,
588
- startId: params.messageId,
589
- allowReference
590
- });
591
- return {
592
- deliverTarget,
593
- replyTarget,
594
- replyReference
595
- };
596
- }
597
- //#endregion
598
- //#region extensions/discord/src/monitor/threading.auto-thread.ts
599
- function resolveTrimmedDiscordMessageChannelId(params) {
600
- return (params.messageChannelId || resolveDiscordMessageChannelId({ message: params.message })).trim();
601
- }
602
- function resolveDiscordAutoThreadContext(params) {
603
- const createdThreadId = normalizeOptionalStringifiedId(params.createdThreadId) ?? "";
604
- if (!createdThreadId) return null;
605
- const messageChannelId = normalizeOptionalString(params.messageChannelId) ?? "";
606
- if (!messageChannelId) return null;
607
- const threadSessionKey = buildAgentSessionKey({
608
- agentId: params.agentId,
609
- channel: params.channel,
610
- peer: {
611
- kind: "channel",
612
- id: createdThreadId
613
- }
614
- });
615
- const parentSessionKey = buildAgentSessionKey({
616
- agentId: params.agentId,
617
- channel: params.channel,
618
- peer: {
619
- kind: "channel",
620
- id: messageChannelId
621
- }
622
- });
623
- return {
624
- createdThreadId,
625
- From: `${params.channel}:channel:${createdThreadId}`,
626
- To: `channel:${createdThreadId}`,
627
- OriginatingTo: `channel:${createdThreadId}`,
628
- SessionKey: threadSessionKey,
629
- ModelParentSessionKey: parentSessionKey,
630
- ...params.parentInheritanceEnabled === true ? { ParentSessionKey: parentSessionKey } : {}
631
- };
632
- }
633
- async function resolveDiscordAutoThreadReplyPlan(params) {
634
- const messageChannelId = resolveTrimmedDiscordMessageChannelId(params);
635
- const originalReplyTarget = `channel:${params.threadChannel?.id ?? (messageChannelId || "unknown")}`;
636
- const createdThreadId = await maybeCreateDiscordAutoThread({
637
- client: params.client,
638
- message: params.message,
639
- messageChannelId: messageChannelId || void 0,
640
- channel: params.channel,
641
- isGuildMessage: params.isGuildMessage,
642
- channelConfig: params.channelConfig,
643
- threadChannel: params.threadChannel,
644
- channelType: params.channelType,
645
- channelName: params.channelName,
646
- channelDescription: params.channelDescription,
647
- baseText: params.baseText,
648
- combinedBody: params.combinedBody,
649
- cfg: params.cfg,
650
- agentId: params.agentId
651
- });
652
- const deliveryPlan = resolveDiscordReplyDeliveryPlan({
653
- replyTarget: originalReplyTarget,
654
- replyToMode: params.replyToMode,
655
- messageId: params.message.id,
656
- threadChannel: params.threadChannel,
657
- createdThreadId
658
- });
659
- const autoThreadContext = params.isGuildMessage ? resolveDiscordAutoThreadContext({
660
- agentId: params.agentId,
661
- channel: params.channel,
662
- messageChannelId,
663
- createdThreadId,
664
- parentInheritanceEnabled: params.threadParentInheritanceEnabled
665
- }) : null;
666
- return {
667
- ...deliveryPlan,
668
- createdThreadId,
669
- autoThreadContext
670
- };
671
- }
672
- async function maybeCreateDiscordAutoThread(params) {
673
- if (!params.isGuildMessage) return;
674
- if (!params.channelConfig?.autoThread) return;
675
- if (params.threadChannel) return;
676
- if (params.channelType === discord_exports.ChannelType.GuildForum || params.channelType === discord_exports.ChannelType.GuildMedia || params.channelType === discord_exports.ChannelType.GuildVoice || params.channelType === discord_exports.ChannelType.GuildStageVoice) return;
677
- const messageChannelId = resolveTrimmedDiscordMessageChannelId(params);
678
- if (!messageChannelId) return;
679
- try {
680
- try {
681
- const existingThreadId = (await getChannelMessage(params.client.rest, messageChannelId, params.message.id))?.thread?.id;
682
- if (existingThreadId) {
683
- logVerbose(`discord: autoThread reusing existing thread ${existingThreadId} on ${messageChannelId}/${params.message.id}`);
684
- return existingThreadId;
685
- }
686
- } catch {}
687
- if (params.message.author?.bot) {
688
- logVerbose(`discord: autoThread skipped for bot-authored message ${messageChannelId}/${params.message.id}`);
689
- return;
690
- }
691
- const rawThreadSource = params.baseText || params.combinedBody || "Thread";
692
- const threadName = sanitizeDiscordThreadName(rawThreadSource, params.message.id);
693
- const archiveDuration = params.channelConfig?.autoArchiveDuration ? Number(params.channelConfig.autoArchiveDuration) : 60;
694
- const createdId = (await createThread(params.client.rest, messageChannelId, { body: {
695
- name: threadName,
696
- auto_archive_duration: archiveDuration
697
- } }, params.message.id))?.id || "";
698
- if (createdId && params.channelConfig?.autoThreadName === "generated" && params.cfg && params.agentId) {
699
- const modelRef = resolveDiscordThreadTitleModelRef({
700
- cfg: params.cfg,
701
- channel: params.channel,
702
- agentId: params.agentId,
703
- threadId: createdId,
704
- messageChannelId,
705
- channelName: params.channelName
706
- });
707
- maybeRenameDiscordAutoThread({
708
- client: params.client,
709
- threadId: createdId,
710
- currentName: threadName,
711
- fallbackId: params.message.id,
712
- sourceText: rawThreadSource,
713
- modelRef,
714
- channelName: params.channelName,
715
- channelDescription: params.channelDescription,
716
- cfg: params.cfg,
717
- agentId: params.agentId
718
- });
719
- }
720
- return createdId || void 0;
721
- } catch (err) {
722
- logVerbose(`discord: autoThread creation failed for ${messageChannelId}/${params.message.id}: ${String(err)}`);
723
- try {
724
- const existingThreadId = (await getChannelMessage(params.client.rest, messageChannelId, params.message.id))?.thread?.id || "";
725
- if (existingThreadId) {
726
- logVerbose(`discord: autoThread reusing existing thread ${existingThreadId} on ${messageChannelId}/${params.message.id}`);
727
- return existingThreadId;
728
- }
729
- } catch {}
730
- return;
731
- }
732
- }
733
- function resolveDiscordThreadTitleModelRef(params) {
734
- const channel = params.channel?.trim();
735
- if (!channel) return;
736
- const parentSessionKey = buildAgentSessionKey({
737
- agentId: params.agentId,
738
- channel,
739
- peer: {
740
- kind: "channel",
741
- id: params.messageChannelId
742
- }
743
- });
744
- const channelLabel = params.channelName?.trim();
745
- const groupChannel = channelLabel ? `#${channelLabel}` : void 0;
746
- return resolveChannelModelOverride({
747
- cfg: params.cfg,
748
- channel,
749
- groupId: params.threadId,
750
- groupChatType: "channel",
751
- groupChannel,
752
- groupSubject: groupChannel,
753
- parentSessionKey
754
- })?.model;
755
- }
756
- async function maybeRenameDiscordAutoThread(params) {
757
- try {
758
- const fallbackName = sanitizeDiscordThreadName("", params.fallbackId);
759
- const generated = await generateThreadTitle({
760
- cfg: params.cfg,
761
- agentId: params.agentId,
762
- messageText: params.sourceText,
763
- modelRef: params.modelRef,
764
- channelName: params.channelName,
765
- channelDescription: params.channelDescription
766
- });
767
- if (!generated) return;
768
- const nextName = sanitizeDiscordThreadName(generated, params.fallbackId);
769
- if (!nextName || nextName === params.currentName || nextName === fallbackName) return;
770
- await editChannel(params.client.rest, params.threadId, { body: { name: nextName } });
771
- } catch (err) {
772
- logVerbose(`discord: autoThread rename failed for ${params.threadId}: ${String(err)}`);
773
- }
774
- }
775
- //#endregion
776
- //#region extensions/discord/src/monitor/threading.ts
777
- var threading_exports = /* @__PURE__ */ __exportAll({
778
- maybeCreateDiscordAutoThread: () => maybeCreateDiscordAutoThread,
779
- resetDiscordThreadStarterCacheForTest: () => resetDiscordThreadStarterCacheForTest,
780
- resolveDiscordAutoThreadContext: () => resolveDiscordAutoThreadContext,
781
- resolveDiscordAutoThreadReplyPlan: () => resolveDiscordAutoThreadReplyPlan,
782
- resolveDiscordReplyDeliveryPlan: () => resolveDiscordReplyDeliveryPlan,
783
- resolveDiscordReplyTarget: () => resolveDiscordReplyTarget,
784
- resolveDiscordThreadChannel: () => resolveDiscordThreadChannel,
785
- resolveDiscordThreadParentInfo: () => resolveDiscordThreadParentInfo,
786
- resolveDiscordThreadStarter: () => resolveDiscordThreadStarter,
787
- sanitizeDiscordThreadName: () => sanitizeDiscordThreadName
788
- });
789
- //#endregion
790
- //#region extensions/discord/src/monitor/thread-channel-context.ts
791
- function isDiscordThreadChannelType(type) {
792
- return type === discord_exports.ChannelType.PublicThread || type === discord_exports.ChannelType.PrivateThread || type === discord_exports.ChannelType.AnnouncementThread;
793
- }
794
- function buildFetchedChannelInfo(channel) {
795
- const channelInfo = resolveDiscordChannelInfoSafe(channel);
796
- if (channelInfo.type === void 0) return null;
797
- return {
798
- type: channelInfo.type,
799
- name: channelInfo.name,
800
- topic: channelInfo.topic,
801
- parentId: channelInfo.parentId,
802
- ownerId: channelInfo.ownerId
803
- };
804
- }
805
- async function resolveDiscordThreadLikeChannelContext(params) {
806
- const safeChannelInfo = resolveDiscordChannelInfoSafe(params.channel);
807
- const channelId = resolveDiscordChannelIdSafe(params.channel) ?? params.channelIdFallback ?? "";
808
- const channelInfo = params.channelInfo !== void 0 ? params.channelInfo : channelId ? await resolveDiscordChannelInfo(params.client, channelId) : null;
809
- const channelType = safeChannelInfo.type ?? channelInfo?.type;
810
- const channelName = safeChannelInfo.name ?? channelInfo?.name;
811
- const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
812
- const parentId = resolveDiscordChannelParentIdSafe(params.channel) ?? channelInfo?.parentId;
813
- const isThreadChannel = isDiscordThreadChannelType(channelType);
814
- let threadParentId;
815
- let threadParentName;
816
- let threadParentSlug = "";
817
- if (channelId && isThreadChannel) {
818
- const parentInfo = await resolveDiscordThreadParentInfo({
522
+ async function resolveDiscordThreadLikeChannelContext(params) {
523
+ const safeChannelInfo = resolveDiscordChannelInfoSafe(params.channel);
524
+ const channelId = resolveDiscordChannelIdSafe(params.channel) ?? params.channelIdFallback ?? "";
525
+ const channelInfo = params.channelInfo !== void 0 ? params.channelInfo : channelId ? await resolveDiscordChannelInfo(params.client, channelId) : null;
526
+ const channelType = safeChannelInfo.type ?? channelInfo?.type;
527
+ const channelName = safeChannelInfo.name ?? channelInfo?.name;
528
+ const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
529
+ const parentId = resolveDiscordChannelParentIdSafe(params.channel) ?? channelInfo?.parentId;
530
+ const isThreadChannel = isDiscordThreadChannelType(channelType);
531
+ let threadParentId;
532
+ let threadParentName;
533
+ let threadParentSlug = "";
534
+ if (channelId && isThreadChannel) {
535
+ const parentInfo = await resolveDiscordThreadParentInfo({
819
536
  client: params.client,
820
537
  threadChannel: {
821
538
  id: channelId,
@@ -1113,7 +830,7 @@ async function handleDiscordReactionEvent(params) {
1113
830
  const resolveReactionBase = () => {
1114
831
  if (reactionBase) return reactionBase;
1115
832
  const emojiLabel = formatDiscordReactionEmoji(data.emoji);
1116
- const actorLabel = formatDiscordUserTag(user);
833
+ const actorLabel = formatDiscordUserTag(user) || user.id;
1117
834
  const guildSlug = guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : data.guild_id ?? (isGroupDm ? "group-dm" : "dm"));
1118
835
  const channelLabel = channelSlug ? `#${channelSlug}` : channelName ? `#${normalizeDiscordSlug(channelName)}` : `#${data.channel_id}`;
1119
836
  reactionBase = {
@@ -1124,24 +841,21 @@ async function handleDiscordReactionEvent(params) {
1124
841
  };
1125
842
  const emitReaction = (text, parentPeerId) => {
1126
843
  const { contextKey } = resolveReactionBase();
1127
- enqueueSystemEvent(text, {
1128
- sessionKey: resolveAgentRoute({
1129
- cfg: params.cfg,
1130
- channel: "discord",
1131
- accountId: params.accountId,
1132
- guildId: data.guild_id ?? void 0,
1133
- memberRoleIds,
1134
- peer: {
1135
- kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
1136
- id: isDirectMessage ? user.id : data.channel_id
1137
- },
1138
- parentPeer: parentPeerId ? {
1139
- kind: "channel",
1140
- id: parentPeerId
1141
- } : void 0
1142
- }).sessionKey,
1143
- contextKey
1144
- });
844
+ enqueueRoutedSystemEvent(text, resolveAgentRoute({
845
+ cfg: params.cfg,
846
+ channel: "discord",
847
+ accountId: params.accountId,
848
+ guildId: data.guild_id ?? void 0,
849
+ memberRoleIds,
850
+ peer: {
851
+ kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
852
+ id: isDirectMessage ? user.id : data.channel_id
853
+ },
854
+ parentPeer: parentPeerId ? {
855
+ kind: "channel",
856
+ id: parentPeerId
857
+ } : void 0
858
+ }), { contextKey });
1145
859
  };
1146
860
  const shouldNotifyReaction = (options) => shouldEmitDiscordReactionNotification({
1147
861
  mode: options.mode,
@@ -1222,232 +936,39 @@ async function handleDiscordReactionEvent(params) {
1222
936
  }
1223
937
  }
1224
938
  //#endregion
1225
- //#region extensions/discord/src/monitor/presence-events.ts
1226
- const DISCORD_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
1227
- function isDiscordOnlineStatus(status) {
1228
- return status === "online" || status === "idle" || status === "dnd";
1229
- }
1230
- function isDiscordOfflineStatus(status) {
1231
- return status === "offline";
1232
- }
1233
- function resolveDiscordOnlinePresenceEvent(params) {
1234
- const config = params.config;
1235
- const userId = params.data.user?.id?.trim();
1236
- if (!config || config.enabled === false || !userId || userId === params.botUserId || params.data.user.bot === true || !isDiscordOnlineStatus(params.data.status) || !params.availabilityKind) return null;
1237
- if (config.users !== void 0 && !config.users.includes(userId)) return null;
1238
- if (params.lastEmittedAtMs !== void 0 && params.nowMs - params.lastEmittedAtMs < 288e5) return null;
1239
- const lines = [
1240
- "Discord online-presence event:",
1241
- 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.`,
1242
- `The authorized greeting target is channel_id=${JSON.stringify(config.channelId)}.`,
1243
- "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.",
1244
- "Send at most one short, natural greeting to the target channel. Do not reveal private memory. If no greeting is appropriate, stay silent."
1245
- ];
1246
- return {
1247
- channelId: config.channelId,
1248
- userId,
1249
- text: lines.join("\n")
1250
- };
1251
- }
1252
- //#endregion
1253
- //#region extensions/discord/src/monitor/presence-cooldown-store.ts
1254
- const DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
1255
- function openDiscordPresenceCooldownStore() {
1256
- return getDiscordRuntime().state.openSyncKeyedStore({
1257
- namespace: "presence-greeting-cooldowns",
1258
- maxEntries: DISCORD_PRESENCE_COOLDOWN_MAX_ENTRIES,
1259
- overflowPolicy: "reject-new",
1260
- defaultTtlMs: DISCORD_PRESENCE_GREETING_COOLDOWN_MS
1261
- });
1262
- }
1263
- //#endregion
1264
- //#region extensions/discord/src/monitor/presence-emission-gate.ts
1265
- const DISCORD_PRESENCE_RECONNECT_SUPPRESS_MS = 300 * 1e3;
1266
- const DISCORD_PRESENCE_BURST_LIMIT = 8;
1267
- const DISCORD_PRESENCE_BURST_WINDOW_MS = 60 * 1e3;
1268
- function resolveDiscordPresenceGateOptions(config) {
1269
- return {
1270
- reconnectSuppressMs: config?.reconnectSuppressSeconds !== void 0 ? config.reconnectSuppressSeconds * 1e3 : DISCORD_PRESENCE_RECONNECT_SUPPRESS_MS,
1271
- burstLimit: config?.burstLimit ?? DISCORD_PRESENCE_BURST_LIMIT,
1272
- burstWindowMs: config?.burstWindowSeconds !== void 0 ? config.burstWindowSeconds * 1e3 : DISCORD_PRESENCE_BURST_WINDOW_MS
1273
- };
939
+ //#region extensions/discord/src/monitor/listeners.ts
940
+ function registerDiscordListener(listeners, listener) {
941
+ if (listeners.some((existing) => existing.constructor === listener.constructor)) return false;
942
+ listeners.push(listener);
943
+ return true;
1274
944
  }
1275
- /**
1276
- * Per-account lifecycle gate for online-presence events. Reconnect state belongs to the Gateway
1277
- * session, while burst state follows the guild-owned configuration that supplies its limits.
1278
- */
1279
- var DiscordPresenceEmissionGate = class {
1280
- constructor() {
1281
- this.reconnectLogged = false;
1282
- this.burstByGuild = /* @__PURE__ */ new Map();
1283
- this.nextReservationId = 0;
1284
- }
1285
- noteGatewaySessionReset(nowMs) {
1286
- this.lastSessionResetAtMs = nowMs;
1287
- this.reconnectLogged = false;
1288
- }
1289
- evaluateReconnectWindow(nowMs, options) {
1290
- if (this.lastSessionResetAtMs !== void 0 && options.reconnectSuppressMs > 0 && nowMs - this.lastSessionResetAtMs < options.reconnectSuppressMs) {
1291
- const shouldLog = !this.reconnectLogged;
1292
- this.reconnectLogged = true;
1293
- return {
1294
- allowed: false,
1295
- reason: "reconnect-window",
1296
- shouldLog
1297
- };
1298
- }
1299
- return { allowed: true };
945
+ var DiscordMessageListener = class extends MessageCreateListener {
946
+ constructor(handler, logger, onEvent) {
947
+ super();
948
+ this.handler = handler;
949
+ this.logger = logger;
950
+ this.onEvent = onEvent;
1300
951
  }
1301
- reserveBurst(guildId, nowMs, options) {
1302
- const state = this.burstByGuild.get(guildId) ?? {
1303
- reservations: [],
1304
- logged: false
1305
- };
1306
- state.reservations = state.reservations.filter((reservation) => !reservation.committed || nowMs - reservation.atMs < options.burstWindowMs);
1307
- this.burstByGuild.set(guildId, state);
1308
- if (state.reservations.length >= options.burstLimit) {
1309
- const shouldLog = !state.logged;
1310
- state.logged = true;
1311
- return {
1312
- allowed: false,
1313
- reason: state.reservations.some((reservation) => !reservation.committed) ? "burst-pending" : "burst",
1314
- shouldLog
1315
- };
952
+ async handle(data, client) {
953
+ this.onEvent?.();
954
+ try {
955
+ await this.handler(data, client);
956
+ } catch (err) {
957
+ (this.logger ?? discordEventQueueLog).error(danger(`discord handler failed: ${String(err)}`));
1316
958
  }
1317
- state.logged = false;
1318
- const reservation = {
1319
- id: this.nextReservationId++,
1320
- atMs: nowMs,
1321
- committed: false
1322
- };
1323
- state.reservations.push(reservation);
1324
- return {
1325
- allowed: true,
1326
- reservation: reservation.id
1327
- };
1328
959
  }
1329
- commitBurst(guildId, reservation, nowMs) {
1330
- const entry = this.burstByGuild.get(guildId)?.reservations.find((candidate) => candidate.id === reservation);
1331
- if (!entry) return;
1332
- entry.atMs = nowMs;
1333
- entry.committed = true;
960
+ };
961
+ var DiscordInteractionListener = class extends InteractionCreateListener {
962
+ constructor(logger, onEvent) {
963
+ super();
964
+ this.logger = logger;
965
+ this.onEvent = onEvent;
1334
966
  }
1335
- releaseBurst(guildId, reservation) {
1336
- const state = this.burstByGuild.get(guildId);
1337
- if (!state) return;
1338
- state.reservations = state.reservations.filter((candidate) => candidate.id !== reservation);
1339
- if (state.reservations.length === 0 && !state.logged) this.burstByGuild.delete(guildId);
1340
- }
1341
- };
1342
- //#endregion
1343
- //#region extensions/discord/src/monitor/presence-transition-cache.ts
1344
- const DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES = 75e3;
1345
- var DiscordPresenceBaselineCache = class {
1346
- constructor(maxEntries = DEFAULT_PRESENCE_BASELINE_MAX_ENTRIES) {
1347
- this.maxEntries = maxEntries;
1348
- this.offlineByKey = /* @__PURE__ */ new Map();
1349
- this.onlineByKey = /* @__PURE__ */ new Map();
1350
- }
1351
- clear() {
1352
- this.offlineByKey.clear();
1353
- this.onlineByKey.clear();
1354
- }
1355
- clearScope(scope) {
1356
- for (const markers of [this.offlineByKey, this.onlineByKey]) for (const [key, markerScope] of markers) if (markerScope === scope) markers.delete(key);
1357
- }
1358
- isOffline(scope, key) {
1359
- return this.offlineByKey.get(key) === scope;
1360
- }
1361
- isOnline(scope, key) {
1362
- return this.onlineByKey.get(key) === scope;
1363
- }
1364
- observeOffline(scope, key) {
1365
- this.deleteMarker(this.onlineByKey, scope, key);
1366
- return this.observe(this.offlineByKey, scope, key);
1367
- }
1368
- observeOnline(scope, key) {
1369
- this.deleteMarker(this.offlineByKey, scope, key);
1370
- return this.observe(this.onlineByKey, scope, key);
1371
- }
1372
- deleteMarker(markers, scope, key) {
1373
- if (markers.get(key) === scope) markers.delete(key);
1374
- }
1375
- observe(markers, scope, key) {
1376
- markers.delete(key);
1377
- markers.set(key, scope);
1378
- let evictedScope;
1379
- while (markers.size > this.maxEntries) {
1380
- const oldestKey = markers.keys().next().value;
1381
- if (oldestKey === void 0) break;
1382
- evictedScope = markers.get(oldestKey);
1383
- markers.delete(oldestKey);
1384
- }
1385
- return evictedScope;
1386
- }
1387
- };
1388
- //#endregion
1389
- //#region extensions/discord/src/monitor/thread-session-close.ts
1390
- /**
1391
- * Closes every session entry in the store whose key contains {@link threadId}.
1392
- * The explicit lifecycle deletion archives the old transcript and guarantees
1393
- * that a later inbound message starts a fresh session in every reset mode.
1394
- */
1395
- async function closeDiscordThreadSessions(params) {
1396
- const { cfg, accountId, threadId } = params;
1397
- const normalizedThreadId = normalizeOptionalLowercaseString(threadId) ?? "";
1398
- if (!normalizedThreadId) return 0;
1399
- const segmentRe = new RegExp(`:${normalizedThreadId}(?::|$)`, "i");
1400
- function sessionKeyContainsThreadId(key) {
1401
- return segmentRe.test(key);
1402
- }
1403
- const storePath = resolveStorePath(cfg.session?.store, { agentId: accountId });
1404
- let resetCount = 0;
1405
- for (const { sessionKey, entry } of listSessionEntries({ storePath })) {
1406
- if (!sessionKeyContainsThreadId(sessionKey)) continue;
1407
- if (await deleteSessionEntry({
1408
- archiveTranscript: true,
1409
- expectedSessionId: entry.sessionId ?? null,
1410
- expectedUpdatedAt: entry.updatedAt,
1411
- sessionKey,
1412
- storePath
1413
- })) resetCount += 1;
1414
- }
1415
- return resetCount;
1416
- }
1417
- //#endregion
1418
- //#region extensions/discord/src/monitor/listeners.ts
1419
- function registerDiscordListener(listeners, listener) {
1420
- if (listeners.some((existing) => existing.constructor === listener.constructor)) return false;
1421
- listeners.push(listener);
1422
- return true;
1423
- }
1424
- var DiscordMessageListener = class extends MessageCreateListener {
1425
- constructor(handler, logger, onEvent) {
1426
- super();
1427
- this.handler = handler;
1428
- this.logger = logger;
1429
- this.onEvent = onEvent;
1430
- }
1431
- async handle(data, client) {
1432
- this.onEvent?.();
1433
- try {
1434
- await this.handler(data, client);
1435
- } catch (err) {
1436
- (this.logger ?? discordEventQueueLog).error(danger(`discord handler failed: ${String(err)}`));
1437
- }
1438
- }
1439
- };
1440
- var DiscordInteractionListener = class extends InteractionCreateListener {
1441
- constructor(logger, onEvent) {
1442
- super();
1443
- this.logger = logger;
1444
- this.onEvent = onEvent;
1445
- }
1446
- async handle(data, client) {
1447
- this.onEvent?.();
1448
- Promise.resolve().then(() => client.handleInteraction(data, {})).catch((err) => {
1449
- (this.logger ?? discordEventQueueLog).error(danger(`discord interaction handler failed: ${String(err)}`));
1450
- });
967
+ async handle(data, client) {
968
+ this.onEvent?.();
969
+ Promise.resolve().then(() => client.handleInteraction(data, {})).catch((err) => {
970
+ (this.logger ?? discordEventQueueLog).error(danger(`discord interaction handler failed: ${String(err)}`));
971
+ });
1451
972
  }
1452
973
  };
1453
974
  var DiscordPresenceListener = class extends PresenceUpdateListener {
@@ -1612,8 +1133,7 @@ var DiscordPresenceListener = class extends PresenceUpdateListener {
1612
1133
  (this.params.logger ?? discordEventQueueLog).warn(danger(`discord presence cooldown persistence failed: ${String(err)}`));
1613
1134
  return;
1614
1135
  }
1615
- if (!enqueueSystemEvent(presenceEvent.text, {
1616
- sessionKey: route.sessionKey,
1136
+ if (!enqueueRoutedSystemEvent(presenceEvent.text, route, {
1617
1137
  contextKey: `discord:presence-online:${this.params.accountId}:${data.guild_id}:${userId}`,
1618
1138
  deliveryContext: {
1619
1139
  channel: "discord",
@@ -1679,10 +1199,9 @@ var DiscordPresenceReadyListener = class extends ReadyListener {
1679
1199
  }
1680
1200
  };
1681
1201
  var DiscordThreadUpdateListener = class extends ThreadUpdateListener {
1682
- constructor(cfg, accountId, logger) {
1202
+ constructor(cfg, logger) {
1683
1203
  super();
1684
1204
  this.cfg = cfg;
1685
- this.accountId = accountId;
1686
1205
  this.logger = logger;
1687
1206
  }
1688
1207
  async handle(data) {
@@ -1697,7 +1216,6 @@ var DiscordThreadUpdateListener = class extends ThreadUpdateListener {
1697
1216
  const logger = this.logger ?? discordEventQueueLog;
1698
1217
  const count = await closeDiscordThreadSessions({
1699
1218
  cfg: this.cfg,
1700
- accountId: this.accountId,
1701
1219
  threadId
1702
1220
  });
1703
1221
  if (count > 0) logger.info("Discord thread archived — reset sessions", {
@@ -1711,6 +1229,40 @@ var DiscordThreadUpdateListener = class extends ThreadUpdateListener {
1711
1229
  });
1712
1230
  }
1713
1231
  };
1232
+ var DiscordThreadDeleteListener = class extends ThreadDeleteListener {
1233
+ constructor(cfg, accountId, logger) {
1234
+ super();
1235
+ this.cfg = cfg;
1236
+ this.accountId = accountId;
1237
+ this.logger = logger;
1238
+ }
1239
+ async handle(data) {
1240
+ await runDiscordListenerWithSlowLog({
1241
+ logger: this.logger,
1242
+ listener: this.constructor.name,
1243
+ event: this.type,
1244
+ run: async () => {
1245
+ const threadId = data.id;
1246
+ getThreadBindingManager(this.accountId)?.unbindThread({
1247
+ threadId,
1248
+ reason: "thread-delete",
1249
+ sendFarewell: false
1250
+ });
1251
+ const count = await closeDiscordThreadSessions({
1252
+ cfg: this.cfg,
1253
+ threadId
1254
+ });
1255
+ if (count > 0) (this.logger ?? discordEventQueueLog).info("Discord thread deleted — reset sessions", {
1256
+ threadId,
1257
+ count
1258
+ });
1259
+ },
1260
+ onError: (err) => {
1261
+ (this.logger ?? discordEventQueueLog).error(danger(`discord thread-delete handler failed: ${String(err)}`));
1262
+ }
1263
+ });
1264
+ }
1265
+ };
1714
1266
  //#endregion
1715
1267
  //#region extensions/discord/src/internal/gateway-dispatch.ts
1716
1268
  function dispatchVoiceGatewayEvent(client, type, data) {
@@ -1808,11 +1360,16 @@ function isDiscordUnknownInteraction(error) {
1808
1360
  if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) return true;
1809
1361
  return false;
1810
1362
  }
1363
+ function resolveDiscordInteractionMessageParts(payload) {
1364
+ const { components, embeds } = payload.channelData?.discord ?? {};
1365
+ return {
1366
+ components: Array.isArray(components) && components.length > 0 ? components : void 0,
1367
+ embeds: Array.isArray(embeds) && embeds.length > 0 ? embeds : void 0
1368
+ };
1369
+ }
1811
1370
  function hasRenderableReplyPayload(payload) {
1812
- if (resolveSendableOutboundReplyParts(payload).hasContent) return true;
1813
- const discordData = payload.channelData?.discord;
1814
- if (Array.isArray(discordData?.components) && discordData.components.length > 0) return true;
1815
- return false;
1371
+ const { components, embeds } = resolveDiscordInteractionMessageParts(payload);
1372
+ return resolveSendableOutboundReplyParts(payload).hasContent || Boolean(components || embeds);
1816
1373
  }
1817
1374
  async function safeDiscordInteractionCall(label, fn) {
1818
1375
  try {
@@ -1832,43 +1389,25 @@ async function settleDiscordInteractionWithoutVisibleReply(interaction) {
1832
1389
  async function deliverDiscordInteractionReply(params) {
1833
1390
  const { interaction, payload, textLimit, maxLinesPerMessage, preferFollowUp, chunkMode } = params;
1834
1391
  const reply = resolveSendableOutboundReplyParts(payload);
1835
- const discordData = payload.channelData?.discord;
1836
- let firstMessageComponents = Array.isArray(discordData?.components) && discordData.components.length > 0 ? discordData.components : void 0;
1392
+ let { components: firstMessageComponents, embeds: firstMessageEmbeds } = resolveDiscordInteractionMessageParts(payload);
1837
1393
  let payloadDelivered = false;
1838
- const sendMessage = async (content, files, components) => {
1839
- const contentPayload = content ? { content } : {};
1840
- const payloadLocal = files && files.length > 0 ? {
1841
- ...contentPayload,
1394
+ const sendMessage = async (content, files, components, embeds) => {
1395
+ const hasV2 = hasDiscordV2Components(components);
1396
+ const payloadLocal = {
1397
+ ...content && !hasV2 ? { content } : {},
1842
1398
  ...components ? { components } : {},
1399
+ ...embeds && !hasV2 ? { embeds } : {},
1843
1400
  ...params.responseEphemeral !== void 0 ? { ephemeral: params.responseEphemeral } : {},
1844
- files: files.map((file) => {
1845
- if (file.data instanceof Blob) return {
1846
- name: file.name,
1847
- data: file.data
1848
- };
1849
- const arrayBuffer = Uint8Array.from(file.data).buffer;
1850
- return {
1851
- name: file.name,
1852
- data: new Blob([arrayBuffer])
1853
- };
1854
- })
1855
- } : {
1856
- ...contentPayload,
1857
- ...components ? { components } : {},
1858
- ...params.responseEphemeral !== void 0 ? { ephemeral: params.responseEphemeral } : {}
1401
+ ...files?.length ? { files } : {}
1859
1402
  };
1860
1403
  let result;
1861
1404
  try {
1862
1405
  result = await safeDiscordInteractionCall("interaction send", async () => {
1863
- if (!preferFollowUp && !payloadDelivered) {
1864
- await interaction.reply(payloadLocal);
1865
- payloadDelivered = true;
1866
- firstMessageComponents = void 0;
1867
- return;
1868
- }
1869
- await interaction.followUp(payloadLocal);
1406
+ if (!preferFollowUp && !payloadDelivered) await interaction.reply(payloadLocal);
1407
+ else await interaction.followUp(payloadLocal);
1870
1408
  payloadDelivered = true;
1871
1409
  firstMessageComponents = void 0;
1410
+ firstMessageEmbeds = void 0;
1872
1411
  });
1873
1412
  } catch (error) {
1874
1413
  if (!payloadDelivered) throw error;
@@ -1884,7 +1423,8 @@ async function deliverDiscordInteractionReply(params) {
1884
1423
  const loaded = await loadWebMedia(url, { localRoots: params.mediaLocalRoots });
1885
1424
  return {
1886
1425
  name: loaded.fileName ?? "upload",
1887
- data: loaded.buffer
1426
+ data: loaded.buffer,
1427
+ contentType: loaded.contentType
1888
1428
  };
1889
1429
  }));
1890
1430
  const chunks = resolveTextChunksWithFallback(reply.text, chunkDiscordTextWithMode(reply.text, {
@@ -1892,99 +1432,27 @@ async function deliverDiscordInteractionReply(params) {
1892
1432
  maxLines: maxLinesPerMessage,
1893
1433
  chunkMode
1894
1434
  }));
1895
- await sendMessage(chunks[0] ?? "", media, firstMessageComponents);
1435
+ await sendMessage(chunks[0] ?? "", media, firstMessageComponents, firstMessageEmbeds);
1896
1436
  for (const chunk of chunks.slice(1)) {
1897
1437
  if (!chunk.trim()) continue;
1898
1438
  await sendMessage(chunk);
1899
1439
  }
1900
1440
  return payloadDelivered;
1901
1441
  }
1902
- if (!reply.hasText && !firstMessageComponents) return false;
1903
- let chunks = reply.text || firstMessageComponents ? resolveTextChunksWithFallback(reply.text, chunkDiscordTextWithMode(reply.text, {
1442
+ if (!reply.hasText && !firstMessageComponents && !firstMessageEmbeds) return false;
1443
+ const chunks = resolveTextChunksWithFallback(reply.text, chunkDiscordTextWithMode(reply.text, {
1904
1444
  maxChars: textLimit,
1905
1445
  maxLines: maxLinesPerMessage,
1906
1446
  chunkMode
1907
- })) : [];
1908
- if (chunks.length === 0 && firstMessageComponents) chunks = [""];
1447
+ }));
1448
+ if (chunks.length === 0) chunks.push("");
1909
1449
  for (const chunk of chunks) {
1910
- if (!chunk.trim() && !firstMessageComponents) continue;
1911
- await sendMessage(chunk, void 0, firstMessageComponents);
1450
+ if (!chunk.trim() && !firstMessageComponents && !firstMessageEmbeds) continue;
1451
+ await sendMessage(chunk, void 0, firstMessageComponents, firstMessageEmbeds);
1912
1452
  }
1913
1453
  return payloadDelivered;
1914
1454
  }
1915
1455
  //#endregion
1916
- //#region extensions/discord/src/monitor/route-resolution.ts
1917
- function buildDiscordRoutePeer(params) {
1918
- return {
1919
- kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
1920
- id: params.isDirectMessage ? params.directUserId?.trim() || params.conversationId : params.conversationId
1921
- };
1922
- }
1923
- function resolveDiscordConversationRoute(params) {
1924
- return resolveAgentRoute({
1925
- cfg: params.cfg,
1926
- channel: "discord",
1927
- accountId: params.accountId,
1928
- guildId: params.guildId ?? void 0,
1929
- memberRoleIds: params.memberRoleIds,
1930
- peer: params.peer,
1931
- parentPeer: params.parentConversationId ? {
1932
- kind: "channel",
1933
- id: params.parentConversationId
1934
- } : void 0
1935
- });
1936
- }
1937
- function resolveDiscordBoundConversationRoute(params) {
1938
- return resolveDiscordEffectiveRoute({
1939
- route: resolveDiscordConversationRoute({
1940
- cfg: params.cfg,
1941
- accountId: params.accountId,
1942
- guildId: params.guildId,
1943
- memberRoleIds: params.memberRoleIds,
1944
- peer: buildDiscordRoutePeer({
1945
- isDirectMessage: params.isDirectMessage,
1946
- isGroupDm: params.isGroupDm,
1947
- directUserId: params.directUserId,
1948
- conversationId: params.conversationId
1949
- }),
1950
- parentConversationId: params.parentConversationId
1951
- }),
1952
- boundSessionKey: params.boundSessionKey,
1953
- configuredRoute: params.configuredRoute,
1954
- matchedBy: params.matchedBy
1955
- });
1956
- }
1957
- function resolveDiscordEffectiveRoute(params) {
1958
- const boundSessionKey = params.boundSessionKey?.trim();
1959
- if (!boundSessionKey) return params.configuredRoute?.route ?? params.route;
1960
- return {
1961
- ...params.route,
1962
- sessionKey: boundSessionKey,
1963
- agentId: resolveAgentIdFromSessionKey(boundSessionKey),
1964
- lastRoutePolicy: deriveLastRoutePolicy({
1965
- sessionKey: boundSessionKey,
1966
- mainSessionKey: params.route.mainSessionKey
1967
- }),
1968
- ...params.matchedBy ? { matchedBy: params.matchedBy } : {}
1969
- };
1970
- }
1971
- function hasExplicitRuntimeBindingIntent(record) {
1972
- if (record.targetKind === "subagent") return true;
1973
- if (isAcpSessionKey(record.targetSessionKey) || isSubagentSessionKey(record.targetSessionKey)) return true;
1974
- const metadata = record.metadata;
1975
- if (!metadata || typeof metadata !== "object") return false;
1976
- return typeof metadata.boundBy === "string" || typeof metadata.label === "string" || typeof metadata.threadName === "string" || metadata.pluginBindingOwner === "plugin";
1977
- }
1978
- function shouldIgnoreStaleDiscordRouteBinding(params) {
1979
- const bindingRecord = params.bindingRecord;
1980
- const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
1981
- if (!bindingRecord || !boundSessionKey || hasExplicitRuntimeBindingIntent(bindingRecord)) return false;
1982
- const bound = parseAgentSessionKey(boundSessionKey);
1983
- const routed = parseAgentSessionKey(params.route.sessionKey);
1984
- if (!bound || !routed || bound.rest !== routed.rest) return false;
1985
- return bound.agentId !== params.route.agentId;
1986
- }
1987
- //#endregion
1988
1456
  //#region extensions/discord/src/monitor/native-command-route.ts
1989
1457
  async function resolveDiscordNativeInteractionRouteState(params) {
1990
1458
  const route = resolveDiscordBoundConversationRoute({
@@ -2031,8 +1499,6 @@ async function resolveDiscordNativeInteractionRouteState(params) {
2031
1499
  //#endregion
2032
1500
  //#region extensions/discord/src/monitor/native-command.runtime.ts
2033
1501
  const nativeCommandRuntime = {
2034
- matchPluginCommand: pluginRuntime.matchPluginCommand,
2035
- executePluginCommand: pluginRuntime.executePluginCommand,
2036
1502
  dispatchChannelInboundTurn,
2037
1503
  resolveDirectStatusReplyForSession,
2038
1504
  resolveDiscordNativeInteractionRouteState,
@@ -2043,7 +1509,8 @@ const nativeCommandRuntime = {
2043
1509
  async function dispatchDiscordNativeAgentReply(params) {
2044
1510
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(params.discordConfig);
2045
1511
  let didReply = false;
2046
- let settledWithoutVisibleReply = false;
1512
+ let finalReplyOutcome;
1513
+ let hiddenFinalReply;
2047
1514
  const turnResult = await nativeCommandRuntime.dispatchChannelInboundTurn({
2048
1515
  cfg: params.cfg,
2049
1516
  channel: "discord",
@@ -2079,14 +1546,17 @@ async function dispatchDiscordNativeAgentReply(params) {
2079
1546
  suppression: { reason: "no_visible_result" }
2080
1547
  };
2081
1548
  },
2082
- onDelivered: (_payload, _info, result) => {
2083
- if (result?.visibleReplySent === false) settledWithoutVisibleReply = true;
1549
+ onDelivered: (payload, info, result) => {
1550
+ if (params.suppressReplies && info.kind === "final" && result?.suppression?.reason === "no_visible_result" && payload.text?.trim()) hiddenFinalReply = payload;
1551
+ if (info.kind === "final" && result?.visibleReplySent !== void 0 && (result.visibleReplySent || finalReplyOutcome !== "failed")) finalReplyOutcome = result.visibleReplySent ? "accepted" : "suppressed";
2084
1552
  },
2085
1553
  onError: (err, info) => {
2086
- if (isChannelPartialDeliveryError(err)) {
1554
+ const partialDelivery = isChannelPartialDeliveryError(err);
1555
+ if (partialDelivery) {
2087
1556
  didReply = true;
2088
1557
  logVerbose("discord: interaction reply partially delivered before expiry");
2089
1558
  }
1559
+ if (info.kind === "final") finalReplyOutcome = partialDelivery ? "accepted" : "failed";
2090
1560
  const message = err instanceof Error ? err.stack ?? err.message : String(err);
2091
1561
  params.log.error(`discord slash ${info.kind} reply failed: ${message}`);
2092
1562
  }
@@ -2095,14 +1565,20 @@ async function dispatchDiscordNativeAgentReply(params) {
2095
1565
  dispatcherOptions: { humanDelay: resolveHumanDelayConfig(params.cfg, params.effectiveRoute.agentId) },
2096
1566
  replyOptions: {
2097
1567
  skillFilter: params.channelConfig?.skills,
1568
+ [PLUGIN_COMMAND_DISPATCH]: params.pluginCommandDispatch,
2098
1569
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
2099
1570
  }
2100
1571
  });
2101
- if (!didReply && (params.suppressReplies || settledWithoutVisibleReply)) {
1572
+ const deliberateSilentTerminalReply = turnResult.dispatched && turnResult.dispatchResult.deliberateSilentTerminalReply === true;
1573
+ const dispatchResult = {
1574
+ dispatched: turnResult.dispatched,
1575
+ ...hiddenFinalReply ? { hiddenFinalReply } : {}
1576
+ };
1577
+ if (!didReply && (params.suppressReplies || finalReplyOutcome === "suppressed" || deliberateSilentTerminalReply)) {
2102
1578
  await settleDiscordInteractionWithoutVisibleReply(params.interaction);
2103
- return;
1579
+ return dispatchResult;
2104
1580
  }
2105
- if (params.suppressReplies || didReply || settledWithoutVisibleReply || turnResult.dispatched && hasVisibleInboundReplyDispatch(turnResult.dispatchResult)) return;
1581
+ if (didReply || turnResult.dispatched && hasVisibleInboundReplyDispatch(turnResult.dispatchResult)) return dispatchResult;
2106
1582
  await safeDiscordInteractionCall("interaction empty fallback", async () => {
2107
1583
  const payload = {
2108
1584
  content: DISCORD_EMPTY_VISIBLE_REPLY_WARNING,
@@ -2114,6 +1590,7 @@ async function dispatchDiscordNativeAgentReply(params) {
2114
1590
  }
2115
1591
  await params.interaction.reply(payload);
2116
1592
  });
1593
+ return dispatchResult;
2117
1594
  }
2118
1595
  //#endregion
2119
1596
  //#region extensions/discord/src/monitor/native-interaction-channel-context.ts
@@ -2391,11 +1868,11 @@ async function resolveDiscordNativeAutocompleteAuthorized(params) {
2391
1868
  //#endregion
2392
1869
  //#region extensions/discord/src/monitor/native-command-bypass.ts
2393
1870
  function shouldBypassConfiguredAcpEnsure(commandName) {
2394
- return normalizeLowercaseStringOrEmpty(commandName) === "acp";
1871
+ return commandName.trim().toLowerCase() === "acp";
2395
1872
  }
2396
1873
  function shouldBypassConfiguredAcpGuildGuards(commandName) {
2397
- const normalized = normalizeLowercaseStringOrEmpty(commandName);
2398
- return normalized === "new" || normalized === "reset";
1874
+ const command = commandName.trim().toLowerCase();
1875
+ return command === "new" || command === "reset";
2399
1876
  }
2400
1877
  //#endregion
2401
1878
  //#region extensions/discord/src/monitor/inbound-context.ts
@@ -2474,6 +1951,14 @@ function buildDiscordNativeCommandContext(params) {
2474
1951
  CommandTargetSessionKey: params.commandTargetSessionKey,
2475
1952
  AccountId: params.accountId ?? void 0,
2476
1953
  ChatType: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
1954
+ ...buildDiscordConversationRouteContext({
1955
+ isDirectMessage: params.isDirectMessage,
1956
+ isGroupDm: params.isGroupDm,
1957
+ directUserId: params.user.id,
1958
+ conversationId: params.channelId,
1959
+ isThread: params.isThreadChannel,
1960
+ parentConversationId: params.threadParentId
1961
+ }),
2477
1962
  ConversationLabel: conversationLabel,
2478
1963
  GroupSubject: params.isGuild ? params.guildName : void 0,
2479
1964
  GroupSpace: params.isGuild ? params.guildInfo?.id ?? params.guildInfo?.slug ?? params.guildId : void 0,
@@ -2489,7 +1974,6 @@ function buildDiscordNativeCommandContext(params) {
2489
1974
  Surface: "discord",
2490
1975
  WasMentioned: true,
2491
1976
  MessageSid: params.interactionId,
2492
- MessageThreadId: params.isThreadChannel ? params.channelId : void 0,
2493
1977
  Timestamp: params.timestampMs ?? Date.now(),
2494
1978
  CommandAuthorized: params.commandAuthorized,
2495
1979
  CommandTurn: {
@@ -2504,8 +1988,7 @@ function buildDiscordNativeCommandContext(params) {
2504
1988
  isDirectMessage: params.isDirectMessage,
2505
1989
  userId: params.user.id,
2506
1990
  channelId: params.channelId
2507
- }) ?? (params.isDirectMessage ? `user:${params.user.id}` : `channel:${params.channelId}`),
2508
- ThreadParentId: params.isThreadChannel ? params.threadParentId : void 0
1991
+ }) ?? (params.isDirectMessage ? `user:${params.user.id}` : `channel:${params.channelId}`)
2509
1992
  });
2510
1993
  }
2511
1994
  //#endregion
@@ -2623,7 +2106,8 @@ async function handleDiscordCommandArgInteraction(params) {
2623
2106
  sessionPrefix: ctx.sessionPrefix,
2624
2107
  preferFollowUp: true,
2625
2108
  threadBindings: ctx.threadBindings,
2626
- responseEphemeral: resolveDiscordSlashCommandConfig(ctx.discordConfig?.slashCommand).ephemeral
2109
+ responseEphemeral: resolveDiscordSlashCommandConfig(ctx.discordConfig?.slashCommand).ephemeral,
2110
+ pluginCommandDispatch: { kind: "non-plugin" }
2627
2111
  });
2628
2112
  }
2629
2113
  async function runDiscordCommandArgButton(params) {
@@ -2730,10 +2214,10 @@ function timestampOrder(value) {
2730
2214
  return value !== void 0 && value >= 0 ? value : 0;
2731
2215
  }
2732
2216
  function comparePreferenceEntries(left, right) {
2733
- return timestampMs(right.value.updatedAt) - timestampMs(left.value.updatedAt) || timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) || left.key.localeCompare(right.key);
2217
+ return preferenceTimestampMs(right.value.updatedAt) - preferenceTimestampMs(left.value.updatedAt) || timestampOrder(right.value.updatedOrder) - timestampOrder(left.value.updatedOrder) || left.key.localeCompare(right.key);
2734
2218
  }
2735
2219
  function nextPreferenceTimestamp(existingEntries) {
2736
- const existingMaxTimestampMs = existingEntries.reduce((max, entry) => Math.max(max, timestampMs(entry.updatedAt)), 0);
2220
+ const existingMaxTimestampMs = existingEntries.reduce((max, entry) => Math.max(max, preferenceTimestampMs(entry.updatedAt)), 0);
2737
2221
  lastPreferenceTimestampMs = Math.min(Math.max(resolveDateTimestampMs(Date.now(), 0), lastPreferenceTimestampMs + 1, existingMaxTimestampMs + 1), MAX_DATE_TIMESTAMP_MS);
2738
2222
  const existingMaxOrder = existingEntries.reduce((max, entry) => Math.max(max, timestampOrder(entry.updatedOrder)), 0);
2739
2223
  lastPreferenceOrder = Math.max(lastPreferenceOrder + 1, existingMaxOrder + 1);
@@ -2782,8 +2266,6 @@ async function recordDiscordModelPickerRecentModel(params) {
2782
2266
  const DISCORD_MODEL_PICKER_CUSTOM_ID_KEY = "mdlpk";
2783
2267
  const DISCORD_CUSTOM_ID_MAX_CHARS = 100;
2784
2268
  const DISCORD_COMPONENT_MAX_SELECT_OPTIONS = 25;
2785
- const DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2786
- const DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2787
2269
  function compareBucketItems(left, right) {
2788
2270
  const normalized = left.toLowerCase().localeCompare(right.toLowerCase());
2789
2271
  return normalized === 0 ? left.localeCompare(right) : normalized;
@@ -2843,15 +2325,15 @@ function parseRawPage(value) {
2843
2325
  }
2844
2326
  return 1;
2845
2327
  }
2846
- function parseRawPositiveInt(value) {
2847
- return parseStrictPositiveInteger(value);
2848
- }
2849
2328
  function coerceString(value) {
2850
2329
  return typeof value === "string" || typeof value === "number" ? String(value) : "";
2851
2330
  }
2852
- function clampPageSize(rawPageSize, max, fallback) {
2853
- if (!Number.isFinite(rawPageSize)) return fallback;
2854
- return Math.min(max, Math.max(1, Math.floor(rawPageSize ?? fallback)));
2331
+ function clampPageSize(rawPageSize) {
2332
+ if (!Number.isFinite(rawPageSize)) return DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
2333
+ return Math.min(DISCORD_COMPONENT_MAX_SELECT_OPTIONS, Math.max(1, Math.floor(rawPageSize ?? DISCORD_COMPONENT_MAX_SELECT_OPTIONS)));
2334
+ }
2335
+ function normalizeOptionalModelPickerIndex(value) {
2336
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(1, Math.floor(value)) : void 0;
2855
2337
  }
2856
2338
  function paginateItems(params) {
2857
2339
  const totalItems = params.items.length;
@@ -2877,10 +2359,10 @@ function buildDiscordModelPickerCustomId(params) {
2877
2359
  const userId = params.userId.trim();
2878
2360
  if (!userId) throw new Error("Discord model picker custom_id requires userId");
2879
2361
  const page = normalizeModelPickerPage(params.page);
2880
- const providerPage = typeof params.providerPage === "number" && Number.isFinite(params.providerPage) ? Math.max(1, Math.floor(params.providerPage)) : void 0;
2362
+ const providerPage = normalizeOptionalModelPickerIndex(params.providerPage);
2881
2363
  const normalizedProvider = params.provider ? normalizeProviderId(params.provider) : void 0;
2882
- const modelIndex = typeof params.modelIndex === "number" && Number.isFinite(params.modelIndex) ? Math.max(1, Math.floor(params.modelIndex)) : void 0;
2883
- const recentSlot = typeof params.recentSlot === "number" && Number.isFinite(params.recentSlot) ? Math.max(1, Math.floor(params.recentSlot)) : void 0;
2364
+ const modelIndex = normalizeOptionalModelPickerIndex(params.modelIndex);
2365
+ const recentSlot = normalizeOptionalModelPickerIndex(params.recentSlot);
2884
2366
  const modelToken = params.modelToken?.trim();
2885
2367
  if (modelToken && !DISCORD_MODEL_PICKER_MODEL_TOKEN_PATTERN.test(modelToken)) throw new Error("Discord model picker model token is invalid");
2886
2368
  const parts = [
@@ -2893,7 +2375,7 @@ function buildDiscordModelPickerCustomId(params) {
2893
2375
  if (normalizedProvider) parts.push(`p=${encodeCustomIdComponent(normalizedProvider)}`);
2894
2376
  const runtime = params.runtime?.trim();
2895
2377
  if (runtime) parts.push(`r=${encodeCustomIdComponent(runtime)}`);
2896
- const runtimeIndex = typeof params.runtimeIndex === "number" && Number.isFinite(params.runtimeIndex) ? Math.max(1, Math.floor(params.runtimeIndex)) : void 0;
2378
+ const runtimeIndex = normalizeOptionalModelPickerIndex(params.runtimeIndex);
2897
2379
  if (runtimeIndex) parts.push(`ri=${String(runtimeIndex)}`);
2898
2380
  if (providerPage) parts.push(`pp=${String(providerPage)}`);
2899
2381
  if (modelToken) parts.push(`m=${modelToken}`);
@@ -2917,13 +2399,13 @@ function parseDiscordModelPickerData(data) {
2917
2399
  const userId = decodeCustomIdComponent(coerceString(data.u));
2918
2400
  const providerRaw = decodeCustomIdComponent(coerceString(data.p));
2919
2401
  const runtimeRaw = decodeCustomIdComponent(coerceString(data.r));
2920
- const runtimeIndex = parseRawPositiveInt(data.ri);
2402
+ const runtimeIndex = parseStrictPositiveInteger(data.ri);
2921
2403
  const page = parseRawPage(data.g ?? data.pg);
2922
- const providerPage = parseRawPositiveInt(data.pp);
2923
- const modelIndex = parseRawPositiveInt(data.mi);
2404
+ const providerPage = parseStrictPositiveInteger(data.pp);
2405
+ const modelIndex = parseStrictPositiveInteger(data.mi);
2924
2406
  const modelTokenRaw = coerceString(data.m).trim();
2925
2407
  const modelToken = DISCORD_MODEL_PICKER_MODEL_TOKEN_PATTERN.test(modelTokenRaw) ? modelTokenRaw : void 0;
2926
- const recentSlot = parseRawPositiveInt(data.rs);
2408
+ const recentSlot = parseStrictPositiveInteger(data.rs);
2927
2409
  const providerBucketRaw = decodeCustomIdComponent(coerceString(data.pb)).trim().toLowerCase();
2928
2410
  const modelBucketRaw = decodeCustomIdComponent(coerceString(data.mb)).trim().toLowerCase();
2929
2411
  if (!isValidCommandContext(command) || !isValidPickerAction(action) || !isValidPickerView(view)) return null;
@@ -3039,17 +2521,7 @@ function findProviderBucketId(data, provider) {
3039
2521
  return findProviderBucketLocation(data, provider)?.bucket;
3040
2522
  }
3041
2523
  function findProviderBucketLocation(data, provider) {
3042
- const normalized = normalizeProviderId(provider);
3043
- const sorted = [...data.providers].toSorted();
3044
- const idx = sorted.indexOf(normalized);
3045
- if (idx < 0) return;
3046
- const containing = computeAlphaBuckets(sorted).find((bucket) => idx >= bucket.start && idx < bucket.end);
3047
- if (!containing) return;
3048
- const page = Math.floor((idx - containing.start) / DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE) + 1;
3049
- return {
3050
- ...containing.id !== "all" ? { bucket: containing.id } : {},
3051
- page
3052
- };
2524
+ return findModelPickerBucketLocation([...data.providers].toSorted(), normalizeProviderId(provider));
3053
2525
  }
3054
2526
  /**
3055
2527
  * Derive the alpha-bucket id that contains a given model id within the
@@ -3059,28 +2531,24 @@ function findProviderBucketLocation(data, provider) {
3059
2531
  */
3060
2532
  function findModelBucketId(data, provider, model) {
3061
2533
  const modelSet = data.byProvider.get(normalizeProviderId(provider));
3062
- if (!modelSet) return;
3063
- const sorted = [...modelSet].toSorted(compareBucketItems);
3064
- const idx = sorted.indexOf(model);
3065
- if (idx < 0) return;
3066
- const containing = computeAlphaBuckets(sorted).find((bucket) => idx >= bucket.start && idx < bucket.end);
3067
- return containing && containing.id !== "all" ? containing.id : void 0;
3068
- }
3069
- function buildDiscordModelPickerProviderItems(data) {
3070
- return [...data.providers].toSorted().map((provider) => ({
3071
- id: provider,
3072
- count: data.byProvider.get(provider)?.size ?? 0
3073
- }));
3074
- }
3075
- function getDiscordModelPickerProviderPage(params) {
3076
- const allItems = buildDiscordModelPickerProviderItems(params.data);
3077
- const buckets = computeAlphaBuckets(allItems.map((item) => item.id));
2534
+ return modelSet ? findModelPickerBucketLocation([...modelSet].toSorted(compareBucketItems), model)?.bucket : void 0;
2535
+ }
2536
+ function findModelPickerBucketLocation(sortedItems, item, pageSize = DISCORD_COMPONENT_MAX_SELECT_OPTIONS) {
2537
+ const index = sortedItems.indexOf(item);
2538
+ const bucket = index < 0 ? void 0 : computeAlphaBuckets(sortedItems).find((entry) => index >= entry.start && index < entry.end);
2539
+ return bucket ? {
2540
+ ...bucket.id === "all" ? {} : { bucket: bucket.id },
2541
+ page: Math.floor((index - bucket.start) / pageSize) + 1
2542
+ } : void 0;
2543
+ }
2544
+ function paginateDiscordModelPickerBucket(params) {
2545
+ const buckets = computeAlphaBuckets(params.itemLabels);
3078
2546
  const bucket = resolveBucket(buckets, params.bucket);
3079
- const bucketItems = bucket ? allItems.slice(bucket.start, bucket.end) : allItems;
3080
- const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE, DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE);
2547
+ const items = bucket ? params.items.slice(bucket.start, bucket.end) : params.items;
2548
+ const pageSize = clampPageSize(params.pageSize);
3081
2549
  return {
3082
2550
  ...paginateItems({
3083
- items: bucketItems,
2551
+ items,
3084
2552
  page: normalizeModelPickerPage(params.page),
3085
2553
  pageSize
3086
2554
  }),
@@ -3088,24 +2556,29 @@ function getDiscordModelPickerProviderPage(params) {
3088
2556
  buckets
3089
2557
  };
3090
2558
  }
2559
+ function getDiscordModelPickerProviderPage(params) {
2560
+ const providers = [...params.data.providers].toSorted();
2561
+ return paginateDiscordModelPickerBucket({
2562
+ ...params,
2563
+ itemLabels: providers,
2564
+ items: providers.map((provider) => ({
2565
+ id: provider,
2566
+ count: params.data.byProvider.get(provider)?.size ?? 0
2567
+ }))
2568
+ });
2569
+ }
3091
2570
  function getDiscordModelPickerModelPage(params) {
3092
2571
  const provider = normalizeProviderId(params.provider);
3093
2572
  const modelSet = params.data.byProvider.get(provider);
3094
2573
  if (!modelSet) return null;
3095
2574
  const allModels = [...modelSet].toSorted(compareBucketItems);
3096
- const buckets = computeAlphaBuckets(allModels);
3097
- const bucket = resolveBucket(buckets, params.bucket);
3098
- const bucketItems = bucket ? allModels.slice(bucket.start, bucket.end) : allModels;
3099
- const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
3100
2575
  return {
3101
- ...paginateItems({
3102
- items: bucketItems,
3103
- page: normalizeModelPickerPage(params.page),
3104
- pageSize
2576
+ ...paginateDiscordModelPickerBucket({
2577
+ ...params,
2578
+ items: allModels,
2579
+ itemLabels: allModels
3105
2580
  }),
3106
- provider,
3107
- bucket,
3108
- buckets
2581
+ provider
3109
2582
  };
3110
2583
  }
3111
2584
  function resolveDiscordModelPickerPageForModel(params) {
@@ -3113,16 +2586,8 @@ function resolveDiscordModelPickerPageForModel(params) {
3113
2586
  const modelSet = params.data.byProvider.get(provider);
3114
2587
  if (!modelSet) return { page: 1 };
3115
2588
  const sorted = [...modelSet].toSorted(compareBucketItems);
3116
- const index = sorted.indexOf(params.model);
3117
- if (index < 0) return { page: 1 };
3118
- const pageSize = clampPageSize(params.pageSize, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE, DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE);
3119
- const containingBucket = computeAlphaBuckets(sorted).find((bucket) => index >= bucket.start && index < bucket.end);
3120
- if (!containingBucket) return { page: Math.floor(index / pageSize) + 1 };
3121
- const offsetInBucket = index - containingBucket.start;
3122
- return {
3123
- page: Math.floor(offsetInBucket / pageSize) + 1,
3124
- bucket: containingBucket.id === "all" ? void 0 : containingBucket.id
3125
- };
2589
+ const pageSize = clampPageSize(params.pageSize);
2590
+ return findModelPickerBucketLocation(sorted, params.model, pageSize) ?? { page: 1 };
3126
2591
  }
3127
2592
  //#endregion
3128
2593
  //#region extensions/discord/src/monitor/model-picker.view.ts
@@ -3302,53 +2767,25 @@ function buildProviderSelectRow(params) {
3302
2767
  }
3303
2768
  function buildPaginationRow(params) {
3304
2769
  if (params.totalPages <= 1) return null;
3305
- return new Row([
3306
- createModelPickerButton({
3307
- label: "◀ Prev",
3308
- style: ButtonStyle.Secondary,
3309
- disabled: !params.hasPrev,
3310
- customId: buildDiscordModelPickerCustomId({
3311
- command: params.command,
3312
- action: "nav",
3313
- view: params.view,
3314
- provider: params.provider,
3315
- runtime: params.runtime,
3316
- runtimeIndex: params.runtimeIndex,
3317
- page: Math.max(1, params.page - 1),
3318
- providerPage: params.providerPage,
3319
- modelIndex: params.modelIndex,
3320
- modelToken: params.modelToken,
3321
- providerBucket: params.providerBucket,
3322
- modelBucket: params.modelBucket,
3323
- userId: params.userId
3324
- })
3325
- }),
3326
- createModelPickerButton({
3327
- label: `Page ${params.page}/${params.totalPages}`,
3328
- style: ButtonStyle.Secondary,
3329
- disabled: true,
3330
- customId: DISCORD_MODEL_PICKER_PAGE_INDICATOR_CUSTOM_ID
3331
- }),
3332
- createModelPickerButton({
3333
- label: "Next ▶",
3334
- style: ButtonStyle.Secondary,
3335
- disabled: !params.hasNext,
3336
- customId: buildDiscordModelPickerCustomId({
3337
- command: params.command,
3338
- action: "nav",
3339
- view: params.view,
3340
- provider: params.provider,
3341
- runtime: params.runtime,
3342
- runtimeIndex: params.runtimeIndex,
3343
- page: Math.min(params.totalPages, params.page + 1),
3344
- providerPage: params.providerPage,
3345
- modelIndex: params.modelIndex,
3346
- modelToken: params.modelToken,
3347
- providerBucket: params.providerBucket,
3348
- modelBucket: params.modelBucket,
3349
- userId: params.userId
3350
- })
2770
+ const { page, totalPages, hasPrev, hasNext, ...navigationState } = params;
2771
+ const createNavigationButton = (label, targetPage, enabled) => createModelPickerButton({
2772
+ label,
2773
+ disabled: !enabled,
2774
+ customId: buildDiscordModelPickerCustomId({
2775
+ ...navigationState,
2776
+ action: "nav",
2777
+ page: targetPage
3351
2778
  })
2779
+ });
2780
+ const indicatorButton = createModelPickerButton({
2781
+ label: `Page ${page}/${totalPages}`,
2782
+ disabled: true,
2783
+ customId: DISCORD_MODEL_PICKER_PAGE_INDICATOR_CUSTOM_ID
2784
+ });
2785
+ return new Row([
2786
+ createNavigationButton("◀ Prev", Math.max(1, page - 1), hasPrev),
2787
+ indicatorButton,
2788
+ createNavigationButton("Next ▶", Math.min(totalPages, page + 1), hasNext)
3352
2789
  ]);
3353
2790
  }
3354
2791
  function buildModelRows(params) {
@@ -3461,10 +2898,17 @@ function buildModelRows(params) {
3461
2898
  const resolvedDefault = params.data.resolvedDefault;
3462
2899
  const shouldDisableReset = Boolean(parsedCurrentModel) && parsedCurrentModel?.provider === resolvedDefault.provider && parsedCurrentModel?.model === resolvedDefault.model;
3463
2900
  const hasPendingSelection = Boolean(parsedPendingModel) && parsedPendingModel?.provider === params.modelPage.provider && typeof params.pendingModelIndex === "number" && params.pendingModelIndex > 0;
2901
+ const modelActionState = {
2902
+ command: params.command,
2903
+ provider: params.modelPage.provider,
2904
+ ...compactRuntime,
2905
+ page: params.modelPage.page,
2906
+ providerPage: providerPage.page,
2907
+ userId: params.userId
2908
+ };
3464
2909
  const buttonRowItems = [
3465
2910
  createModelPickerButton({
3466
2911
  label: "Providers",
3467
- style: ButtonStyle.Secondary,
3468
2912
  customId: buildDiscordModelPickerCustomId({
3469
2913
  command: params.command,
3470
2914
  action: "back",
@@ -3476,47 +2920,29 @@ function buildModelRows(params) {
3476
2920
  }),
3477
2921
  createModelPickerButton({
3478
2922
  label: "Cancel",
3479
- style: ButtonStyle.Secondary,
3480
2923
  customId: buildDiscordModelPickerCustomId({
3481
- command: params.command,
2924
+ ...modelActionState,
3482
2925
  action: "cancel",
3483
- view: "models",
3484
- provider: params.modelPage.provider,
3485
- ...compactRuntime,
3486
- page: params.modelPage.page,
3487
- providerPage: providerPage.page,
3488
- userId: params.userId
2926
+ view: "models"
3489
2927
  })
3490
2928
  }),
3491
2929
  createModelPickerButton({
3492
2930
  label: "Reset to default",
3493
- style: ButtonStyle.Secondary,
3494
2931
  disabled: shouldDisableReset,
3495
2932
  customId: buildDiscordModelPickerCustomId({
3496
- command: params.command,
2933
+ ...modelActionState,
3497
2934
  action: "reset",
3498
- view: "models",
3499
- provider: params.modelPage.provider,
3500
- ...compactRuntime,
3501
- page: params.modelPage.page,
3502
- providerPage: providerPage.page,
3503
- userId: params.userId
2935
+ view: "models"
3504
2936
  })
3505
2937
  })
3506
2938
  ];
3507
2939
  if (hasQuickModels) buttonRowItems.push(createModelPickerButton({
3508
2940
  label: "Recents",
3509
- style: ButtonStyle.Secondary,
3510
2941
  customId: buildDiscordModelPickerCustomId({
3511
- command: params.command,
2942
+ ...modelActionState,
3512
2943
  action: "recents",
3513
2944
  view: "recents",
3514
- provider: params.modelPage.provider,
3515
- ...compactRuntime,
3516
- page: params.modelPage.page,
3517
- providerPage: providerPage.page,
3518
- modelBucket: activeModelBucket,
3519
- userId: params.userId
2945
+ modelBucket: activeModelBucket
3520
2946
  })
3521
2947
  }));
3522
2948
  buttonRowItems.push(createModelPickerButton({
@@ -3524,16 +2950,11 @@ function buildModelRows(params) {
3524
2950
  style: ButtonStyle.Primary,
3525
2951
  disabled: !hasPendingSelection,
3526
2952
  customId: buildDiscordModelPickerCustomId({
3527
- command: params.command,
2953
+ ...modelActionState,
3528
2954
  action: "submit",
3529
2955
  view: "models",
3530
- provider: params.modelPage.provider,
3531
- ...compactRuntime,
3532
- page: params.modelPage.page,
3533
- providerPage: providerPage.page,
3534
2956
  modelIndex: params.pendingModelIndex,
3535
- modelToken: pendingModelToken,
3536
- userId: params.userId
2957
+ modelToken: pendingModelToken
3537
2958
  })
3538
2959
  }));
3539
2960
  return {
@@ -3680,32 +3101,14 @@ function createModelRefToken(modelRef) {
3680
3101
  function renderDiscordModelPickerRecentsView(params) {
3681
3102
  const defaultModelRef = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
3682
3103
  const rows = [];
3683
- const dedupedQuickModels = params.quickModels.filter((modelRef) => modelRef !== defaultModelRef);
3684
- rows.push(new Row([createModelPickerButton({
3685
- label: formatRecentsButtonLabel(defaultModelRef, "(default)"),
3686
- style: ButtonStyle.Secondary,
3687
- customId: buildDiscordModelPickerCustomId({
3688
- command: params.command,
3689
- action: "submit",
3690
- view: "recents",
3691
- recentSlot: 1,
3692
- modelToken: createModelRefToken(defaultModelRef),
3693
- provider: params.provider,
3694
- runtime: params.runtime,
3695
- runtimeIndex: params.runtimeIndex,
3696
- page: params.page,
3697
- providerPage: params.providerPage,
3698
- userId: params.userId
3699
- })
3700
- })]));
3701
- for (const [i, modelRef] of dedupedQuickModels.entries()) rows.push(new Row([createModelPickerButton({
3702
- label: formatRecentsButtonLabel(modelRef),
3703
- style: ButtonStyle.Secondary,
3104
+ const recentModels = [defaultModelRef, ...params.quickModels.filter((modelRef) => modelRef !== defaultModelRef)];
3105
+ for (const [index, modelRef] of recentModels.entries()) rows.push(new Row([createModelPickerButton({
3106
+ label: formatRecentsButtonLabel(modelRef, index === 0 ? "(default)" : void 0),
3704
3107
  customId: buildDiscordModelPickerCustomId({
3705
3108
  command: params.command,
3706
3109
  action: "submit",
3707
3110
  view: "recents",
3708
- recentSlot: i + 2,
3111
+ recentSlot: index + 1,
3709
3112
  modelToken: createModelRefToken(modelRef),
3710
3113
  provider: params.provider,
3711
3114
  runtime: params.runtime,
@@ -3717,7 +3120,6 @@ function renderDiscordModelPickerRecentsView(params) {
3717
3120
  })]));
3718
3121
  const backRow = new Row([createModelPickerButton({
3719
3122
  label: "Back",
3720
- style: ButtonStyle.Secondary,
3721
3123
  customId: buildDiscordModelPickerCustomId({
3722
3124
  command: params.command,
3723
3125
  action: "back",
@@ -3749,43 +3151,10 @@ function toDiscordModelPickerMessagePayload(view) {
3749
3151
  }
3750
3152
  //#endregion
3751
3153
  //#region extensions/discord/src/monitor/native-command-model-picker-apply.ts
3752
- async function persistDiscordModelPickerOverride(params) {
3753
- const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId });
3754
- let persisted = false;
3755
- await patchSessionEntry({
3756
- storePath,
3757
- sessionKey: params.route.sessionKey,
3758
- fallbackEntry: {
3759
- sessionId: randomUUID(),
3760
- updatedAt: Date.now()
3761
- },
3762
- replaceEntry: true,
3763
- update: (entry) => {
3764
- persisted = applyModelOverrideToSessionEntry({
3765
- entry,
3766
- selection: {
3767
- provider: params.provider,
3768
- model: params.model,
3769
- isDefault: params.isDefault
3770
- },
3771
- markLiveSwitchPending: true
3772
- }).updated || persisted;
3773
- const runtime = params.runtime?.trim();
3774
- if (runtime && runtime !== "auto" && runtime !== "default") {
3775
- if (entry.agentRuntimeOverride !== runtime) {
3776
- entry.agentRuntimeOverride = runtime;
3777
- delete entry.agentHarnessId;
3778
- persisted = true;
3779
- }
3780
- } else if (runtime && entry.agentRuntimeOverride) {
3781
- delete entry.agentRuntimeOverride;
3782
- delete entry.agentHarnessId;
3783
- persisted = true;
3784
- }
3785
- return entry;
3786
- }
3787
- });
3788
- return persisted;
3154
+ function normalizeExpectedRuntime(value) {
3155
+ const runtime = value?.trim();
3156
+ if (!runtime) return;
3157
+ return runtime === "auto" || runtime === "default" ? "auto" : runtime;
3789
3158
  }
3790
3159
  async function applyDiscordModelPickerSelection(params) {
3791
3160
  try {
@@ -3800,79 +3169,42 @@ async function applyDiscordModelPickerSelection(params) {
3800
3169
  sessionPrefix: params.sessionPrefix,
3801
3170
  preferFollowUp: true,
3802
3171
  threadBindings: params.threadBindings,
3803
- suppressReplies: true
3172
+ suppressReplies: true,
3173
+ pluginCommandDispatch: { kind: "non-plugin" }
3804
3174
  }), 12e3);
3805
3175
  if (!dispatchResult.accepted) return {
3806
3176
  status: "rejected",
3807
3177
  noticeMessage: `❌ Failed to apply ${params.resolvedModelRef}. Try /model ${params.resolvedModelRef} directly.`
3808
3178
  };
3809
- const fallbackRoute = dispatchResult.effectiveRoute ?? params.route;
3179
+ const hiddenFinalReply = dispatchResult.hiddenFinalReply;
3180
+ const effectiveRoute = dispatchResult.effectiveRoute ?? params.route;
3810
3181
  if (params.settleMs > 0) await new Promise((resolve) => {
3811
3182
  setTimeout(resolve, params.settleMs);
3812
3183
  });
3813
- let effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
3814
- let persisted = effectiveModelRef === params.resolvedModelRef;
3815
- if (params.selectedRuntime?.trim()) {
3816
- await persistDiscordModelPickerOverride({
3817
- cfg: params.cfg,
3818
- route: fallbackRoute,
3819
- provider: params.selectedProvider,
3820
- model: params.selectedModel,
3821
- isDefault: params.selectedProvider === params.defaultProvider && params.selectedModel === params.defaultModel,
3822
- runtime: params.selectedRuntime
3823
- });
3824
- await new Promise((resolve) => {
3825
- setTimeout(resolve, 100);
3826
- });
3827
- effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
3828
- persisted = effectiveModelRef === params.resolvedModelRef;
3829
- }
3830
- if (!persisted) {
3831
- logVerbose(`discord: model picker override mismatch — expected ${params.resolvedModelRef} but read ${effectiveModelRef} from session key ${fallbackRoute.sessionKey}; attempting direct session override persist`);
3832
- try {
3833
- const directlyPersisted = await persistDiscordModelPickerOverride({
3834
- cfg: params.cfg,
3835
- route: fallbackRoute,
3836
- provider: params.selectedProvider,
3837
- model: params.selectedModel,
3838
- isDefault: params.selectedProvider === params.defaultProvider && params.selectedModel === params.defaultModel,
3839
- runtime: params.selectedRuntime
3840
- });
3841
- await new Promise((resolve) => {
3842
- setTimeout(resolve, 100);
3843
- });
3844
- effectiveModelRef = params.resolveCurrentModel(fallbackRoute);
3845
- persisted = effectiveModelRef === params.resolvedModelRef;
3846
- if (!persisted) logVerbose(`discord: direct session override persist failed — expected ${params.resolvedModelRef} but read ${effectiveModelRef} from session key ${fallbackRoute.sessionKey}`);
3847
- 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}`);
3848
- } catch (error) {
3849
- if (error instanceof ModelSelectionLockedError) return {
3850
- status: "rejected",
3851
- noticeMessage: `❌ ${error.message}`
3852
- };
3853
- const message = error instanceof Error ? error.message : String(error);
3854
- logVerbose(`discord: direct session override persist threw for session key ${fallbackRoute.sessionKey}: ${message}`);
3855
- }
3856
- }
3857
- if (persisted) await recordDiscordModelPickerRecentModel({
3184
+ const effectiveModelRef = params.resolveCurrentModel(effectiveRoute);
3185
+ const effectiveRuntime = params.resolveCurrentRuntime(effectiveRoute);
3186
+ const currentSelection = `Current selection: ${effectiveModelRef} with runtime ${effectiveRuntime}.`;
3187
+ if (hiddenFinalReply?.isError) return {
3188
+ status: "rejected",
3189
+ noticeMessage: `${hiddenFinalReply.text?.trim()}\n${currentSelection}`
3190
+ };
3191
+ const expectedRuntime = normalizeExpectedRuntime(params.selectedRuntime);
3192
+ const verified = effectiveModelRef === params.resolvedModelRef && (expectedRuntime === void 0 || effectiveRuntime === expectedRuntime);
3193
+ if (verified) await recordDiscordModelPickerRecentModel({
3858
3194
  scope: params.preferenceScope,
3859
3195
  modelRef: params.resolvedModelRef,
3860
3196
  limit: 5
3861
3197
  }).catch(() => void 0);
3862
- return persisted ? {
3198
+ return verified ? {
3863
3199
  status: "success",
3864
3200
  effectiveModelRef,
3865
- noticeMessage: `✅ Model set to ${params.resolvedModelRef}.`
3201
+ noticeMessage: hiddenFinalReply?.text?.trim() || `✅ Model set to ${params.resolvedModelRef}.`
3866
3202
  } : {
3867
3203
  status: "mismatch",
3868
3204
  effectiveModelRef,
3869
- noticeMessage: `⚠️ Tried to set ${params.resolvedModelRef}, but current model is ${effectiveModelRef}.`
3205
+ noticeMessage: `⚠️ Tried to set ${params.resolvedModelRef}${expectedRuntime ? ` with runtime ${expectedRuntime}` : ""}, but current selection is ${effectiveModelRef} with runtime ${effectiveRuntime}.`
3870
3206
  };
3871
3207
  } catch (error) {
3872
- if (error instanceof ModelSelectionLockedError) return {
3873
- status: "rejected",
3874
- noticeMessage: `❌ ${error.message}`
3875
- };
3876
3208
  if (error instanceof Error && error.message === "timeout") return {
3877
3209
  status: "timeout",
3878
3210
  noticeMessage: `⏳ Model change to ${params.resolvedModelRef} is still processing. Check /status in a few seconds.`
@@ -3902,9 +3234,6 @@ function shouldOpenDiscordModelPickerFromCommand(params) {
3902
3234
  if (context === "model") return !resolveCommandArgStringValue(params.commandArgs, "model") && !serializedArgs ? context : null;
3903
3235
  return serializedArgs ? null : context;
3904
3236
  }
3905
- function buildDiscordModelPickerCurrentModel(defaultProvider, defaultModel) {
3906
- return `${defaultProvider}/${defaultModel}`;
3907
- }
3908
3237
  function buildDiscordModelPickerAllowedModelRefs(data) {
3909
3238
  const out = /* @__PURE__ */ new Set();
3910
3239
  for (const provider of data.providers) {
@@ -4000,7 +3329,7 @@ async function resolveDiscordNativeChoiceContext(params) {
4000
3329
  }
4001
3330
  }
4002
3331
  function resolveDiscordModelPickerCurrentModel(params) {
4003
- const fallback = buildDiscordModelPickerCurrentModel(params.data.resolvedDefault.provider, params.data.resolvedDefault.model);
3332
+ const fallback = `${params.data.resolvedDefault.provider}/${params.data.resolvedDefault.model}`;
4004
3333
  try {
4005
3334
  const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.route.agentId });
4006
3335
  const override = resolveStoredModelOverride({
@@ -4175,7 +3504,7 @@ function buildDiscordModelPickerSelectionCommand(params) {
4175
3504
  if (!commandDefinition) return null;
4176
3505
  const commandArgs = {
4177
3506
  values: { model: params.modelRef },
4178
- raw: params.modelRef
3507
+ raw: params.runtime ? `${params.modelRef} --runtime ${params.runtime}` : params.modelRef
4179
3508
  };
4180
3509
  return {
4181
3510
  command: commandDefinition,
@@ -4186,7 +3515,7 @@ function buildDiscordModelPickerSelectionCommand(params) {
4186
3515
  function listDiscordModelPickerProviderModels(data, provider) {
4187
3516
  const modelSet = data.byProvider.get(provider);
4188
3517
  if (!modelSet) return [];
4189
- return [...modelSet].toSorted();
3518
+ return [...modelSet].toSorted((left, right) => left < right ? -1 : left > right ? 1 : 0);
4190
3519
  }
4191
3520
  function resolveDiscordModelPickerModelRefByToken(data, modelToken) {
4192
3521
  const matchingRefs = [];
@@ -4224,10 +3553,6 @@ function resolveDiscordModelPickerSubmissionRuntime(params) {
4224
3553
  provider: params.provider,
4225
3554
  runtime: params.parsedRuntime,
4226
3555
  allowResetRuntime: true
4227
- }) ?? resolveDiscordModelPickerRuntimeForProvider({
4228
- data: params.data,
4229
- provider: params.provider,
4230
- runtime: params.currentRuntime
4231
3556
  });
4232
3557
  }
4233
3558
  async function handleDiscordModelPickerInteraction(params) {
@@ -4277,6 +3602,22 @@ async function handleDiscordModelPickerInteraction(params) {
4277
3602
  });
4278
3603
  const updatePicker = async (payload) => await params.safeInteractionCall("model picker update", () => deferredUpdate ? interaction.editReply(payload) : interaction.update(payload));
4279
3604
  const showNotice = async (message) => await updatePicker(buildDiscordModelPickerNoticePayload(message));
3605
+ const updateModelsView = async (provider, state = {}) => {
3606
+ const rendered = renderDiscordModelPickerModelsView({
3607
+ command: parsed.command,
3608
+ userId: parsed.userId,
3609
+ data: pickerData,
3610
+ provider,
3611
+ page: parsed.page,
3612
+ providerPage: parsed.providerPage ?? 1,
3613
+ providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
3614
+ currentModel: currentModelRef,
3615
+ currentRuntime,
3616
+ quickModels,
3617
+ ...state
3618
+ });
3619
+ return await updatePicker(toDiscordModelPickerMessagePayload(rendered));
3620
+ };
4280
3621
  if (parsed.action === "recents") {
4281
3622
  await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerRecentsView({
4282
3623
  command: parsed.command,
@@ -4293,35 +3634,14 @@ async function handleDiscordModelPickerInteraction(params) {
4293
3634
  })));
4294
3635
  return;
4295
3636
  }
4296
- if (parsed.action === "back" && parsed.view === "providers") {
4297
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
4298
- command: parsed.command,
4299
- userId: parsed.userId,
4300
- data: pickerData,
4301
- page: parsed.page,
4302
- providerBucket: parsed.providerBucket,
4303
- currentModel: currentModelRef
4304
- })));
4305
- return;
4306
- }
4307
- if (parsed.action === "nav" && parsed.view === "providers") {
4308
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
4309
- command: parsed.command,
4310
- userId: parsed.userId,
4311
- data: pickerData,
4312
- page: parsed.page,
4313
- providerBucket: parsed.providerBucket,
4314
- currentModel: currentModelRef
4315
- })));
4316
- return;
4317
- }
4318
- if (parsed.action === "bucket" && parsed.view === "providers") {
3637
+ if (parsed.view === "providers" && (parsed.action === "back" || parsed.action === "nav" || parsed.action === "bucket")) {
3638
+ const selectingBucket = parsed.action === "bucket";
4319
3639
  await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerProvidersView({
4320
3640
  command: parsed.command,
4321
3641
  userId: parsed.userId,
4322
3642
  data: pickerData,
4323
- page: 1,
4324
- providerBucket: resolveSelectedBucket(interaction),
3643
+ page: selectingBucket ? 1 : parsed.page,
3644
+ providerBucket: selectingBucket ? resolveSelectedBucket(interaction) : parsed.providerBucket,
4325
3645
  currentModel: currentModelRef
4326
3646
  })));
4327
3647
  return;
@@ -4332,24 +3652,15 @@ async function handleDiscordModelPickerInteraction(params) {
4332
3652
  currentModelRef,
4333
3653
  data: pickerData
4334
3654
  });
4335
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4336
- command: parsed.command,
4337
- userId: parsed.userId,
4338
- data: pickerData,
4339
- provider,
3655
+ await updateModelsView(provider, {
4340
3656
  page: 1,
4341
- providerPage: parsed.providerPage ?? 1,
4342
- providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
4343
3657
  modelBucket: resolveSelectedBucket(interaction),
4344
- currentModel: currentModelRef,
4345
- currentRuntime,
4346
3658
  pendingRuntime: resolvePendingRuntime({
4347
3659
  data: pickerData,
4348
3660
  provider,
4349
3661
  parsed
4350
- }),
4351
- quickModels
4352
- })));
3662
+ })
3663
+ });
4353
3664
  return;
4354
3665
  }
4355
3666
  if (parsed.action === "nav" && parsed.view === "models") {
@@ -4374,26 +3685,16 @@ async function handleDiscordModelPickerInteraction(params) {
4374
3685
  provider,
4375
3686
  model: pendingModel
4376
3687
  }) : void 0;
4377
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4378
- command: parsed.command,
4379
- userId: parsed.userId,
4380
- data: pickerData,
4381
- provider,
4382
- page: parsed.page,
4383
- providerPage: parsed.providerPage ?? 1,
4384
- providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
3688
+ await updateModelsView(provider, {
4385
3689
  modelBucket: parsed.modelBucket,
4386
- currentModel: currentModelRef,
4387
- currentRuntime,
4388
3690
  ...pendingModel ? { pendingModel: `${provider}/${pendingModel}` } : {},
4389
3691
  pendingModelIndex: pendingModelIndex ?? void 0,
4390
3692
  pendingRuntime: resolvePendingRuntime({
4391
3693
  data: pickerData,
4392
3694
  provider,
4393
3695
  parsed
4394
- }),
4395
- quickModels
4396
- })));
3696
+ })
3697
+ });
4397
3698
  return;
4398
3699
  }
4399
3700
  if (parsed.action === "back" && parsed.view === "models") {
@@ -4402,24 +3703,14 @@ async function handleDiscordModelPickerInteraction(params) {
4402
3703
  currentModelRef,
4403
3704
  data: pickerData
4404
3705
  });
4405
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4406
- command: parsed.command,
4407
- userId: parsed.userId,
4408
- data: pickerData,
4409
- provider,
4410
- page: parsed.page ?? 1,
4411
- providerPage: parsed.providerPage ?? 1,
4412
- providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, provider),
3706
+ await updateModelsView(provider, {
4413
3707
  modelBucket: parsed.modelBucket,
4414
- currentModel: currentModelRef,
4415
- currentRuntime,
4416
3708
  pendingRuntime: resolvePendingRuntime({
4417
3709
  data: pickerData,
4418
3710
  provider,
4419
3711
  parsed
4420
- }),
4421
- quickModels
4422
- })));
3712
+ })
3713
+ });
4423
3714
  return;
4424
3715
  }
4425
3716
  if (parsed.action === "provider") {
@@ -4428,18 +3719,10 @@ async function handleDiscordModelPickerInteraction(params) {
4428
3719
  await showNotice("Sorry, that provider isn't available anymore.");
4429
3720
  return;
4430
3721
  }
4431
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4432
- command: parsed.command,
4433
- userId: parsed.userId,
4434
- data: pickerData,
4435
- provider: selectedProvider,
3722
+ await updateModelsView(selectedProvider, {
4436
3723
  page: 1,
4437
- providerPage: parsed.providerPage ?? parsed.page,
4438
- providerBucket: parsed.providerBucket ?? findProviderBucketId(pickerData, selectedProvider),
4439
- currentModel: currentModelRef,
4440
- currentRuntime,
4441
- quickModels
4442
- })));
3724
+ providerPage: parsed.providerPage ?? parsed.page
3725
+ });
4443
3726
  return;
4444
3727
  }
4445
3728
  if (parsed.action === "model") {
@@ -4459,28 +3742,16 @@ async function handleDiscordModelPickerInteraction(params) {
4459
3742
  return;
4460
3743
  }
4461
3744
  const modelRef = `${provider}/${selectedModel}`;
4462
- const derivedProviderBucket = parsed.providerBucket ?? findProviderBucketId(pickerData, provider);
4463
- const derivedModelBucket = parsed.modelBucket ?? findModelBucketId(pickerData, provider, selectedModel);
4464
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4465
- command: parsed.command,
4466
- userId: parsed.userId,
4467
- data: pickerData,
4468
- provider,
4469
- page: parsed.page,
4470
- providerPage: parsed.providerPage ?? 1,
4471
- providerBucket: derivedProviderBucket,
4472
- modelBucket: derivedModelBucket,
4473
- currentModel: currentModelRef,
4474
- currentRuntime,
3745
+ await updateModelsView(provider, {
3746
+ modelBucket: parsed.modelBucket ?? findModelBucketId(pickerData, provider, selectedModel),
4475
3747
  pendingModel: modelRef,
4476
3748
  pendingModelIndex: modelIndex,
4477
3749
  pendingRuntime: resolvePendingRuntime({
4478
3750
  data: pickerData,
4479
3751
  provider,
4480
3752
  parsed
4481
- }),
4482
- quickModels
4483
- })));
3753
+ })
3754
+ });
4484
3755
  return;
4485
3756
  }
4486
3757
  if (parsed.action === "runtime") {
@@ -4507,25 +3778,13 @@ async function handleDiscordModelPickerInteraction(params) {
4507
3778
  provider,
4508
3779
  model: selectedModel
4509
3780
  }) : void 0;
4510
- const derivedProviderBucket = parsed.providerBucket ?? findProviderBucketId(pickerData, provider);
4511
3781
  const currentModelOnly = splitDiscordModelRef(currentModelRef ?? "");
4512
- const derivedModelBucket = parsed.modelBucket ?? (selectedModel ? findModelBucketId(pickerData, provider, selectedModel) : currentModelOnly && currentModelOnly.provider === provider ? findModelBucketId(pickerData, provider, currentModelOnly.model) : void 0);
4513
- await updatePicker(toDiscordModelPickerMessagePayload(renderDiscordModelPickerModelsView({
4514
- command: parsed.command,
4515
- userId: parsed.userId,
4516
- data: pickerData,
4517
- provider,
4518
- page: parsed.page,
4519
- providerPage: parsed.providerPage ?? 1,
4520
- providerBucket: derivedProviderBucket,
4521
- modelBucket: derivedModelBucket,
4522
- currentModel: currentModelRef,
4523
- currentRuntime,
3782
+ await updateModelsView(provider, {
3783
+ modelBucket: parsed.modelBucket ?? (selectedModel ? findModelBucketId(pickerData, provider, selectedModel) : currentModelOnly && currentModelOnly.provider === provider ? findModelBucketId(pickerData, provider, currentModelOnly.model) : void 0),
4524
3784
  ...pendingModel ? { pendingModel } : {},
4525
3785
  pendingModelIndex: pendingModelIndex ?? void 0,
4526
- pendingRuntime: selectedRuntime,
4527
- quickModels
4528
- })));
3786
+ pendingRuntime: selectedRuntime
3787
+ });
4529
3788
  return;
4530
3789
  }
4531
3790
  if (parsed.action === "submit" || parsed.action === "reset" || parsed.action === "quick") {
@@ -4548,10 +3807,12 @@ async function handleDiscordModelPickerInteraction(params) {
4548
3807
  data: pickerData,
4549
3808
  parsed,
4550
3809
  selectedProvider: parsedModelRef.provider
4551
- }),
4552
- currentRuntime
3810
+ })
3811
+ });
3812
+ const selectionCommand = buildDiscordModelPickerSelectionCommand({
3813
+ modelRef: resolvedModelRef,
3814
+ runtime: selectedRuntime
4553
3815
  });
4554
- const selectionCommand = buildDiscordModelPickerSelectionCommand({ modelRef: resolvedModelRef });
4555
3816
  if (!selectionCommand) {
4556
3817
  await showNotice("Sorry, /model is unavailable right now.");
4557
3818
  return;
@@ -4568,17 +3829,17 @@ async function handleDiscordModelPickerInteraction(params) {
4568
3829
  threadBindings: ctx.threadBindings,
4569
3830
  route,
4570
3831
  resolvedModelRef,
4571
- selectedProvider: parsedModelRef.provider,
4572
- selectedModel: parsedModelRef.model,
4573
3832
  selectedRuntime,
4574
- defaultProvider: pickerData.resolvedDefault.provider,
4575
- defaultModel: pickerData.resolvedDefault.model,
4576
3833
  preferenceScope,
4577
3834
  settleMs: ctx.postApplySettleMs ?? 250,
4578
3835
  resolveCurrentModel: (currentRoute) => resolveDiscordModelPickerCurrentModel({
4579
3836
  cfg,
4580
3837
  route: currentRoute,
4581
3838
  data: pickerData
3839
+ }),
3840
+ resolveCurrentRuntime: (currentRoute) => resolveDiscordModelPickerCurrentRuntime({
3841
+ cfg,
3842
+ route: currentRoute
4582
3843
  })
4583
3844
  });
4584
3845
  await params.safeInteractionCall("model picker follow-up", () => interaction.followUp({
@@ -4661,7 +3922,7 @@ const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
4661
3922
  function truncateDiscordCommandDescription(params) {
4662
3923
  const { value, label } = params;
4663
3924
  if (value.length <= DISCORD_COMMAND_DESCRIPTION_MAX) return value;
4664
- log$1.warn(`discord: truncating native command description (${label}) from ${value.length} to ${DISCORD_COMMAND_DESCRIPTION_MAX}: ${JSON.stringify(value)}`);
3925
+ log$1.debug(`discord: truncating native command description (${label}) from ${value.length} to ${DISCORD_COMMAND_DESCRIPTION_MAX}: ${JSON.stringify(value)}`);
4665
3926
  return truncateUtf16Safe(value, DISCORD_COMMAND_DESCRIPTION_MAX);
4666
3927
  }
4667
3928
  function truncateDiscordCommandDescriptionLocalizations(params) {
@@ -4756,12 +4017,13 @@ function buildDiscordCommandOptions(params) {
4756
4017
  //#endregion
4757
4018
  //#region extensions/discord/src/monitor/native-command.ts
4758
4019
  const log = createSubsystemLogger("discord/native-command");
4020
+ const NON_PLUGIN_COMMAND_DISPATCH = Object.freeze({ kind: "non-plugin" });
4759
4021
  function createDiscordNativeCommand(params) {
4760
4022
  const { command, cfg, discordConfig, accountId, sessionPrefix, ephemeralDefault, threadBindings } = params;
4761
4023
  const fallbackCommandDefinition = createNativeCommandDefinition(command);
4762
- const pluginCommandMatch = nativeCommandRuntime.matchPluginCommand(`/${command.name}`);
4763
- const commandDefinition = pluginCommandMatch !== null ? fallbackCommandDefinition : findCommandByNativeName$1(command.name, "discord", { includeBundledChannelFallback: false }) ?? fallbackCommandDefinition;
4764
- const argDefinitions = commandDefinition.args ?? command.args;
4024
+ const pluginCommandCandidate = "prepareDispatch" in command ? command : void 0;
4025
+ const commandDefinition = pluginCommandCandidate ? fallbackCommandDefinition : findCommandByNativeName$1(command.name, "discord", { includeBundledChannelFallback: false }) ?? fallbackCommandDefinition;
4026
+ const argDefinitions = commandDefinition.args ?? ("args" in command ? command.args : void 0);
4765
4027
  const resolveCurrentConfig = () => getRuntimeConfigSnapshot() ?? cfg;
4766
4028
  const commandOptions = buildDiscordCommandOptions({
4767
4029
  command: commandDefinition,
@@ -4772,7 +4034,7 @@ function createDiscordNativeCommand(params) {
4772
4034
  cfg: resolveCurrentConfig(),
4773
4035
  discordConfig,
4774
4036
  accountId,
4775
- skipCommandOwnerAllowFrom: pluginCommandMatch !== null
4037
+ skipCommandOwnerAllowFrom: pluginCommandCandidate !== void 0
4776
4038
  }),
4777
4039
  resolveChoiceContext: async (interaction) => resolveDiscordNativeChoiceContext({
4778
4040
  interaction,
@@ -4810,9 +4072,11 @@ function createDiscordNativeCommand(params) {
4810
4072
  ...commandArgs,
4811
4073
  raw: serializeCommandArgs$1(commandDefinition, commandArgs) ?? commandArgs.raw
4812
4074
  } : void 0;
4075
+ const prompt = buildCommandTextFromArgs$1(commandDefinition, commandArgsWithRaw);
4076
+ const preparedPluginCommand = pluginCommandCandidate?.prepareDispatch(commandArgsWithRaw?.raw);
4813
4077
  await dispatchDiscordCommandInteraction({
4814
4078
  interaction,
4815
- prompt: buildCommandTextFromArgs$1(commandDefinition, commandArgsWithRaw),
4079
+ prompt,
4816
4080
  command: commandDefinition,
4817
4081
  commandArgs: commandArgsWithRaw,
4818
4082
  cfg,
@@ -4821,7 +4085,8 @@ function createDiscordNativeCommand(params) {
4821
4085
  sessionPrefix,
4822
4086
  preferFollowUp: true,
4823
4087
  threadBindings,
4824
- responseEphemeral: ephemeralDefault
4088
+ responseEphemeral: ephemeralDefault,
4089
+ pluginCommandDispatch: preparedPluginCommand ?? NON_PLUGIN_COMMAND_DISPATCH
4825
4090
  });
4826
4091
  }
4827
4092
  }();
@@ -4884,7 +4149,8 @@ async function dispatchDiscordCommandInteraction(params) {
4884
4149
  allowNameMatching
4885
4150
  });
4886
4151
  const commandOwnerAllowAll = commandOwnerAllowFrom?.includes("*") === true;
4887
- const senderIsCommandOwner = commandOwnerOk || commandOwnerAllowAll;
4152
+ const senderIsCommandOwner = commandOwnerOk;
4153
+ const commandOwnerAccessAllowed = senderIsCommandOwner || commandOwnerAllowAll;
4888
4154
  const ownerAllowListConfigured = discordOwnerAllowList != null;
4889
4155
  const ownerOk = discordOwnerOk;
4890
4156
  const { commandsAllowFromAccess, guildInfo, channelConfig } = resolveDiscordNativeCommandChannelAccessContext({
@@ -5024,8 +4290,7 @@ async function dispatchDiscordCommandInteraction(params) {
5024
4290
  return { accepted: false };
5025
4291
  }
5026
4292
  }
5027
- const pluginMatch = nativeCommandRuntime.matchPluginCommand(prompt);
5028
- if (commandOwnerAllowFrom && !senderIsCommandOwner && !commandsAllowFromAccess.allowed && commandName !== "status" && !pluginMatch) {
4293
+ if (commandOwnerAllowFrom && !commandOwnerAccessAllowed && !commandsAllowFromAccess.allowed && commandName !== "status" && params.pluginCommandDispatch.kind !== "plugin") {
5029
4294
  await respond("You are not authorized to use this command.", { ephemeral: true });
5030
4295
  return { accepted: false };
5031
4296
  }
@@ -5085,7 +4350,7 @@ async function dispatchDiscordCommandInteraction(params) {
5085
4350
  }));
5086
4351
  return { accepted: true };
5087
4352
  }
5088
- if (pluginMatch && commandName !== "status") {
4353
+ if (params.pluginCommandDispatch.kind === "plugin" && commandName !== "status") {
5089
4354
  if (suppressReplies) {
5090
4355
  await settleDiscordInteractionWithoutVisibleReply(interaction);
5091
4356
  return { accepted: true };
@@ -5099,9 +4364,7 @@ async function dispatchDiscordCommandInteraction(params) {
5099
4364
  agentId: pluginCommandAgentId,
5100
4365
  sessionKey: effectiveRoute.sessionKey
5101
4366
  });
5102
- const pluginReply = await nativeCommandRuntime.executePluginCommand({
5103
- command: pluginMatch.command,
5104
- args: pluginMatch.args,
4367
+ const pluginReply = await params.pluginCommandDispatch.execute({
5105
4368
  senderId: sender.id,
5106
4369
  channel: "discord",
5107
4370
  channelId,
@@ -5242,7 +4505,7 @@ async function dispatchDiscordCommandInteraction(params) {
5242
4505
  respond
5243
4506
  });
5244
4507
  if (directStatusResult) return directStatusResult;
5245
- await dispatchDiscordNativeAgentReply({
4508
+ const { dispatched, hiddenFinalReply } = await dispatchDiscordNativeAgentReply({
5246
4509
  cfg,
5247
4510
  discordConfig,
5248
4511
  accountId,
@@ -5254,11 +4517,13 @@ async function dispatchDiscordCommandInteraction(params) {
5254
4517
  preferFollowUp,
5255
4518
  responseEphemeral,
5256
4519
  suppressReplies,
5257
- log
4520
+ log,
4521
+ pluginCommandDispatch: params.pluginCommandDispatch
5258
4522
  });
5259
4523
  return {
5260
- accepted: true,
5261
- effectiveRoute
4524
+ accepted: dispatched,
4525
+ effectiveRoute,
4526
+ hiddenFinalReply
5262
4527
  };
5263
4528
  }
5264
4529
  function createDiscordCommandArgFallbackButton(params) {
@@ -5516,7 +4781,7 @@ var DiscordGatewayVoiceStateCache = class {
5516
4781
  }
5517
4782
  listVoiceChannelStates(guildId, channelId) {
5518
4783
  const states = this.statesByGuild.get(guildId);
5519
- if (!states) return [];
4784
+ if (!states) return null;
5520
4785
  const result = [];
5521
4786
  for (const state of states.values()) if (state.channel_id === channelId) result.push({ ...state });
5522
4787
  return result;
@@ -5729,7 +4994,10 @@ var GatewayPlugin = class extends Plugin {
5729
4994
  if (payload.s !== null && payload.s !== void 0) this.sequence = payload.s;
5730
4995
  switch (payload.op) {
5731
4996
  case GatewayOpcodes.Hello: {
5732
- this.startHeartbeat(payload.d.heartbeat_interval ?? 45e3);
4997
+ this.startHeartbeat(asSafeIntegerInRange(asOptionalRecord(payload.d)?.heartbeat_interval, {
4998
+ min: 1,
4999
+ max: MAX_TIMER_TIMEOUT_MS
5000
+ }) ?? 45e3);
5733
5001
  const resumeState = resume ? this.getResumeState() : null;
5734
5002
  if (resumeState) this.send({
5735
5003
  op: GatewayOpcodes.Resume,
@@ -5932,12 +5200,6 @@ var GatewayPlugin = class extends Plugin {
5932
5200
  }
5933
5201
  };
5934
5202
  //#endregion
5935
- //#region extensions/discord/src/voice/config.ts
5936
- function resolveDiscordVoiceEnabled(voice) {
5937
- if (voice?.enabled !== void 0) return voice.enabled;
5938
- return voice !== void 0;
5939
- }
5940
- //#endregion
5941
5203
  //#region extensions/discord/src/monitor/presence.ts
5942
5204
  const DEFAULT_CUSTOM_ACTIVITY_TYPE$1 = 4;
5943
5205
  const CUSTOM_STATUS_NAME$1 = "Custom Status";
@@ -6429,8 +5691,7 @@ function readStringProperty(value, key) {
6429
5691
  return typeof property === "string" && property ? property : void 0;
6430
5692
  }
6431
5693
  function readNumberProperty(value, key) {
6432
- const property = value[key];
6433
- return typeof property === "number" && Number.isFinite(property) ? property : void 0;
5694
+ return asFiniteNumber(value[key]);
6434
5695
  }
6435
5696
  function describeDiscordGatewayTransportError(error) {
6436
5697
  const code = readStringProperty(error, "code");
@@ -6480,7 +5741,8 @@ function resolveDiscordGatewayIntents(params) {
6480
5741
  const intentsConfig = params?.intentsConfig;
6481
5742
  const voiceEnabled = params?.voiceEnabled;
6482
5743
  const voiceStatesEnabled = intentsConfig?.voiceStates ?? voiceEnabled ?? false;
6483
- let intents = GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.MessageContent | GatewayIntents.DirectMessages | GatewayIntents.GuildMessageReactions | GatewayIntents.DirectMessageReactions;
5744
+ let intents = GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.DirectMessages | GatewayIntents.GuildMessageReactions | GatewayIntents.DirectMessageReactions;
5745
+ if (intentsConfig?.messageContent !== false) intents |= GatewayIntents.MessageContent;
6484
5746
  if (voiceStatesEnabled) intents |= GatewayIntents.GuildVoiceStates;
6485
5747
  if (intentsConfig?.presence) intents |= GatewayIntents.GuildPresences;
6486
5748
  if (intentsConfig?.guildMembers) intents |= GatewayIntents.GuildMembers;
@@ -6860,7 +6122,7 @@ async function waitForDiscordGatewayStop(params) {
6860
6122
  gateway?.disconnect?.();
6861
6123
  } finally {
6862
6124
  cleanup();
6863
- reject(toLintErrorObject(err, "Non-Error rejection"));
6125
+ reject(toErrorObject(err, "Non-Error rejection"));
6864
6126
  }
6865
6127
  };
6866
6128
  const onAbort = () => {
@@ -6881,12 +6143,15 @@ async function waitForDiscordGatewayStop(params) {
6881
6143
  params.registerForceStop?.(onForceStop);
6882
6144
  });
6883
6145
  }
6884
- function toLintErrorObject(value, fallbackMessage) {
6885
- if (value instanceof Error) return value;
6886
- if (typeof value === "string") return new Error(value);
6887
- const error = new Error(fallbackMessage, { cause: value });
6888
- if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
6889
- return error;
6146
+ //#endregion
6147
+ //#region extensions/discord/src/monitor/status.ts
6148
+ /** READY proves a prior terminal failure was repaired, so the account is restartable again. */
6149
+ function createDiscordReadyStatusPatch(at = Date.now()) {
6150
+ return channelReadyPatch({
6151
+ lastConnectedAt: at,
6152
+ lastEventAt: at,
6153
+ lastDisconnect: null
6154
+ });
6890
6155
  }
6891
6156
  //#endregion
6892
6157
  //#region extensions/discord/src/monitor/provider.lifecycle.ts
@@ -7018,11 +6283,7 @@ function createGatewayStatusObserver(params) {
7018
6283
  queuedForceStopError = err;
7019
6284
  };
7020
6285
  const pushConnectedStatus = (at) => {
7021
- params.pushStatus({
7022
- ...createConnectedChannelStatusPatch(at),
7023
- lastDisconnect: null,
7024
- lastError: null
7025
- });
6286
+ params.pushStatus(createDiscordReadyStatusPatch(at));
7026
6287
  };
7027
6288
  const startReadyWatch = () => {
7028
6289
  clearReadyWatch();
@@ -7046,6 +6307,7 @@ function createGatewayStatusObserver(params) {
7046
6307
  const error = /* @__PURE__ */ new Error(`discord gateway opened but did not reach READY within ${params.runtimeReadyTimeoutMs}ms`);
7047
6308
  params.pushStatus({
7048
6309
  connected: false,
6310
+ lifecycle: "recovering",
7049
6311
  lastEventAt: at,
7050
6312
  lastDisconnect: {
7051
6313
  at,
@@ -7074,8 +6336,14 @@ function createGatewayStatusObserver(params) {
7074
6336
  if (message.includes("Gateway websocket closed")) {
7075
6337
  clearReadyWatch();
7076
6338
  const code = parseGatewayCloseCode(message);
6339
+ const terminalDisconnect = code !== void 0 && isFatalGatewayCloseCode(code);
7077
6340
  params.pushStatus({
7078
6341
  connected: false,
6342
+ lifecycle: terminalDisconnect ? "blocked" : "recovering",
6343
+ ...terminalDisconnect ? {
6344
+ terminalDisconnect: true,
6345
+ lastError: message
6346
+ } : {},
7079
6347
  lastEventAt: at,
7080
6348
  lastDisconnect: {
7081
6349
  at,
@@ -7088,6 +6356,7 @@ function createGatewayStatusObserver(params) {
7088
6356
  clearReadyWatch();
7089
6357
  params.pushStatus({
7090
6358
  connected: false,
6359
+ lifecycle: "recovering",
7091
6360
  lastEventAt: at,
7092
6361
  lastError: message
7093
6362
  });
@@ -7118,11 +6387,7 @@ async function waitForGatewayReady(params) {
7118
6387
  if (await params.beforePoll?.() === "stop") return "stopped";
7119
6388
  if (params.gateway?.isConnected === true) {
7120
6389
  const at = Date.now();
7121
- params.pushStatus?.({
7122
- ...createConnectedChannelStatusPatch(at),
7123
- lastDisconnect: null,
7124
- lastError: null
7125
- });
6390
+ params.pushStatus?.(createDiscordReadyStatusPatch(at));
7126
6391
  return "ready";
7127
6392
  }
7128
6393
  if (Date.now() >= deadlineAt) return "timeout";
@@ -7144,6 +6409,7 @@ async function waitForGatewayReady(params) {
7144
6409
  params.runtime.error?.(danger(`discord: gateway READY wait timed out after ${params.readyTimeoutMs}ms; reconnecting with backoff (attempt ${attempt})`));
7145
6410
  params.pushStatus?.({
7146
6411
  connected: false,
6412
+ lifecycle: "recovering",
7147
6413
  lastEventAt: restartAt,
7148
6414
  lastDisconnect: {
7149
6415
  at: restartAt,
@@ -7252,7 +6518,7 @@ async function runDiscordGatewayLifecycle(params) {
7252
6518
  gatewayEmitter?.removeListener(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, onGatewayTransportActivity);
7253
6519
  if (params.voiceManager) {
7254
6520
  await params.voiceManager.destroy();
7255
- const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-DIKGsTGT.js").then((n) => n.n);
6521
+ const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-DeIMNbRh.js").then((n) => n.n);
7256
6522
  setDiscordTranscriptsVoiceManager({
7257
6523
  accountId: params.accountId,
7258
6524
  manager: null
@@ -7267,7 +6533,7 @@ async function runDiscordGatewayLifecycle(params) {
7267
6533
  let discordVoiceRuntimePromise;
7268
6534
  let discordProviderSessionRuntimePromise;
7269
6535
  async function loadDiscordVoiceRuntime() {
7270
- const promise = discordVoiceRuntimePromise ?? import("./manager.runtime-Bjav3pfC.js");
6536
+ const promise = discordVoiceRuntimePromise ?? import("./voice-runtime-CjNFaynA.js");
7271
6537
  discordVoiceRuntimePromise = promise;
7272
6538
  try {
7273
6539
  return await promise;
@@ -7277,7 +6543,7 @@ async function loadDiscordVoiceRuntime() {
7277
6543
  }
7278
6544
  }
7279
6545
  async function loadDiscordProviderSessionRuntime() {
7280
- const promise = discordProviderSessionRuntimePromise ?? import("./provider-session.runtime-Bo88k0pV.js");
6546
+ const promise = discordProviderSessionRuntimePromise ?? import("./provider-session.runtime-CvvphHvs.js");
7281
6547
  discordProviderSessionRuntimePromise = promise;
7282
6548
  try {
7283
6549
  return await promise;
@@ -7287,13 +6553,12 @@ async function loadDiscordProviderSessionRuntime() {
7287
6553
  }
7288
6554
  }
7289
6555
  const discordProviderRuntime = {
7290
- fetchDiscordApplicationId,
6556
+ probeDiscordApplicationId,
7291
6557
  createDiscordNativeCommand,
7292
6558
  runDiscordGatewayLifecycle,
7293
6559
  loadDiscordVoiceRuntime,
7294
6560
  loadDiscordProviderSessionRuntime,
7295
6561
  createClient: (...args) => new Client(...args),
7296
- getPluginCommandSpecs: void 0,
7297
6562
  resolveDiscordAccount,
7298
6563
  resolveNativeCommandsEnabled,
7299
6564
  resolveNativeSkillsEnabled,
@@ -7639,80 +6904,303 @@ async function cleanupDiscordProviderStartup(params) {
7639
6904
  if (!params.lifecycleStarted) params.threadBindings.stop();
7640
6905
  }
7641
6906
  //#endregion
7642
- //#region extensions/discord/src/monitor/provider.commands.ts
7643
- const loadPluginCommandSpecs = createLazyRuntimeNamedExport(() => import("openclaw/plugin-sdk/plugin-runtime"), "getPluginCommandSpecs");
7644
- async function appendPluginCommandSpecs(params) {
7645
- const merged = [...params.commandSpecs];
7646
- const existingNames = new Set(normalizeStringEntriesLower(merged.map((spec) => spec.name)));
7647
- const getPluginCommandSpecs = params.getPluginCommandSpecs ?? await loadPluginCommandSpecs();
7648
- for (const pluginCommand of getPluginCommandSpecs("discord", { config: params.cfg })) {
7649
- const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name);
7650
- if (!normalizedName) continue;
7651
- if (existingNames.has(normalizedName)) {
7652
- params.runtime.error?.(danger(`discord: plugin command "/${normalizedName}" duplicates an existing native command. Skipping.`));
7653
- continue;
7654
- }
7655
- existingNames.add(normalizedName);
7656
- merged.push({
7657
- name: pluginCommand.name,
7658
- description: pluginCommand.description,
7659
- acceptsArgs: pluginCommand.acceptsArgs
7660
- });
7661
- }
7662
- return merged;
7663
- }
7664
- async function resolveDiscordProviderCommandSpecs(params) {
7665
- const listSkillCommands = params.listSkillCommandsForAgents ?? listSkillCommandsForAgents;
7666
- const listNativeCommandSpecs = params.listNativeCommandSpecsForConfig ?? listNativeCommandSpecsForConfig;
7667
- const maxDiscordCommands = params.maxDiscordCommands ?? 100;
7668
- let skillCommands = params.nativeEnabled && params.nativeSkillsEnabled ? listSkillCommands({ cfg: params.cfg }) : [];
7669
- let commandSpecs = params.nativeEnabled ? listNativeCommandSpecs(params.cfg, {
7670
- skillCommands,
7671
- provider: "discord"
7672
- }) : [];
7673
- if (params.nativeEnabled) commandSpecs = await appendPluginCommandSpecs({
7674
- commandSpecs,
7675
- runtime: params.runtime,
6907
+ //#region extensions/discord/src/voice/command.ts
6908
+ const VOICE_CHANNEL_TYPES = [ChannelType.GuildVoice, ChannelType.GuildStageVoice];
6909
+ const DISCORD_VOICE_COMMAND_SPEC = {
6910
+ name: "vc",
6911
+ description: "Voice channel controls",
6912
+ acceptsArgs: false
6913
+ };
6914
+ async function authorizeVoiceCommand(interaction, params, options) {
6915
+ const channelOverride = options?.channelOverride;
6916
+ const channel = channelOverride ? void 0 : interaction.channel;
6917
+ if (!interaction.guild) return {
6918
+ ok: false,
6919
+ message: "Voice commands are only available in guilds."
6920
+ };
6921
+ const user = interaction.user;
6922
+ if (!user) return {
6923
+ ok: false,
6924
+ message: "Unable to resolve command user."
6925
+ };
6926
+ const channelId = channelOverride?.id ?? channel?.id ?? "";
6927
+ const channelContext = await resolveDiscordThreadLikeChannelContext({
6928
+ client: interaction.client,
6929
+ channel: channelOverride ?? channel,
6930
+ channelIdFallback: channelId
6931
+ });
6932
+ const channelName = channelOverride?.name ?? channelContext.channelName;
6933
+ const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
6934
+ const sender = resolveDiscordSenderIdentity({
6935
+ author: user,
6936
+ member: interaction.rawData.member
6937
+ });
6938
+ const voiceAccess = resolveDiscordVoiceAccess(params);
6939
+ const access = await authorizeDiscordVoiceIngress({
7676
6940
  cfg: params.cfg,
7677
- getPluginCommandSpecs: params.getPluginCommandSpecs
6941
+ discordConfig: params.discordConfig,
6942
+ accountId: params.accountId,
6943
+ groupPolicy: params.groupPolicy,
6944
+ useAccessGroups: params.useAccessGroups,
6945
+ guild: interaction.guild,
6946
+ guildId: interaction.guild.id,
6947
+ channelId,
6948
+ channelName,
6949
+ channelSlug: channelContext.channelSlug,
6950
+ parentId: channelOverride?.parentId ?? channelContext.threadParentId,
6951
+ parentName: channelContext.threadParentName,
6952
+ parentSlug: channelContext.threadParentSlug,
6953
+ scope: channelContext.isThreadChannel ? "thread" : "channel",
6954
+ channelLabel: channelId ? formatMention({ channelId }) : "This channel",
6955
+ memberRoleIds,
6956
+ admissionAllowFrom: voiceAccess.admissionAllowFrom,
6957
+ sender: {
6958
+ id: sender.id,
6959
+ name: sender.name,
6960
+ tag: sender.tag
6961
+ }
7678
6962
  });
7679
- const initialCommandCount = commandSpecs.length;
7680
- if (params.nativeEnabled && params.nativeSkillsEnabled && commandSpecs.length > maxDiscordCommands) {
7681
- skillCommands = [];
7682
- commandSpecs = listNativeCommandSpecs(params.cfg, {
7683
- skillCommands: [],
7684
- provider: "discord"
6963
+ if (!access.ok) return {
6964
+ ok: false,
6965
+ message: access.message
6966
+ };
6967
+ return {
6968
+ ok: true,
6969
+ guildId: interaction.guild.id
6970
+ };
6971
+ }
6972
+ async function resolveVoiceCommandRuntimeContext(interaction, params) {
6973
+ const guildId = interaction.guild?.id;
6974
+ if (!guildId) {
6975
+ await interaction.reply({
6976
+ content: "Unable to resolve guild for this command.",
6977
+ ephemeral: true
7685
6978
  });
7686
- commandSpecs = await appendPluginCommandSpecs({
7687
- commandSpecs,
7688
- runtime: params.runtime,
7689
- cfg: params.cfg,
7690
- getPluginCommandSpecs: params.getPluginCommandSpecs
6979
+ return null;
6980
+ }
6981
+ const manager = params.getManager();
6982
+ if (!manager) {
6983
+ await interaction.reply({
6984
+ content: "Voice manager is not available yet.",
6985
+ ephemeral: true
7691
6986
  });
7692
- params.runtime.log?.(warn(`${initialCommandCount} commands exceed the ${maxDiscordCommands}-command Discord limit; removing per-skill commands and keeping /skill.`));
6987
+ return null;
7693
6988
  }
7694
- 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.`));
7695
6989
  return {
7696
- skillCommands,
7697
- commandSpecs
6990
+ guildId,
6991
+ manager
7698
6992
  };
7699
6993
  }
7700
- //#endregion
7701
- //#region extensions/discord/src/monitor/provider.config-log.ts
7702
- function formatThreadBindingDurationForConfigLabel(durationMs) {
7703
- const label = formatThreadBindingDurationLabel(durationMs);
7704
- return label === "disabled" ? "off" : label;
7705
- }
7706
- function logDiscordResolvedConfig(params) {
7707
- const allowFromSummary = summarizeStringEntries({
7708
- entries: params.allowFrom ?? [],
7709
- limit: 4,
7710
- emptyText: "any"
7711
- });
7712
- const groupDmChannelSummary = summarizeStringEntries({
7713
- entries: params.groupDmChannels ?? [],
7714
- limit: 4,
7715
- emptyText: "any"
6994
+ async function ensureVoiceCommandAccess(params) {
6995
+ const access = await authorizeVoiceCommand(params.interaction, params.context, { channelOverride: params.channelOverride });
6996
+ if (access.ok) return true;
6997
+ await params.interaction.reply({
6998
+ content: access.message ?? "Not authorized.",
6999
+ ephemeral: true
7000
+ });
7001
+ return false;
7002
+ }
7003
+ function createDiscordVoiceCommand(params) {
7004
+ const resolveSessionChannelId = (manager, guildId) => manager.status().find((entry) => entry.guildId === guildId)?.channelId;
7005
+ class JoinCommand extends Command {
7006
+ constructor(..._args) {
7007
+ super(..._args);
7008
+ this.name = "join";
7009
+ this.description = "Join a voice channel";
7010
+ this.defer = true;
7011
+ this.ephemeral = params.ephemeralDefault;
7012
+ this.options = [{
7013
+ name: "channel",
7014
+ description: "Voice channel to join",
7015
+ type: ApplicationCommandOptionType.Channel,
7016
+ required: true,
7017
+ channel_types: VOICE_CHANNEL_TYPES
7018
+ }];
7019
+ }
7020
+ async run(interaction) {
7021
+ const channel = await interaction.options.getChannel("channel", true);
7022
+ if (!channel || !("id" in channel)) {
7023
+ await interaction.reply({
7024
+ content: "Voice channel not found.",
7025
+ ephemeral: true
7026
+ });
7027
+ return;
7028
+ }
7029
+ const access = await authorizeVoiceCommand(interaction, params, { channelOverride: {
7030
+ id: channel.id,
7031
+ name: resolveDiscordChannelNameSafe(channel)
7032
+ } });
7033
+ if (!access.ok) {
7034
+ await interaction.reply({
7035
+ content: access.message ?? "Not authorized.",
7036
+ ephemeral: true
7037
+ });
7038
+ return;
7039
+ }
7040
+ if (!isVoiceChannelType(channel.type)) {
7041
+ await interaction.reply({
7042
+ content: "That is not a voice channel.",
7043
+ ephemeral: true
7044
+ });
7045
+ return;
7046
+ }
7047
+ const guildId = access.guildId ?? ("guildId" in channel ? channel.guildId : void 0);
7048
+ if (!guildId) {
7049
+ await interaction.reply({
7050
+ content: "Unable to resolve guild for this voice channel.",
7051
+ ephemeral: true
7052
+ });
7053
+ return;
7054
+ }
7055
+ const manager = params.getManager();
7056
+ if (!manager) {
7057
+ await interaction.reply({
7058
+ content: "Voice manager is not available yet.",
7059
+ ephemeral: true
7060
+ });
7061
+ return;
7062
+ }
7063
+ const result = await manager.join({
7064
+ guildId,
7065
+ channelId: channel.id
7066
+ });
7067
+ await interaction.reply({
7068
+ content: result.message,
7069
+ ephemeral: true
7070
+ });
7071
+ }
7072
+ }
7073
+ class LeaveCommand extends Command {
7074
+ constructor(..._args2) {
7075
+ super(..._args2);
7076
+ this.name = "leave";
7077
+ this.description = "Leave the current voice channel";
7078
+ this.defer = true;
7079
+ this.ephemeral = params.ephemeralDefault;
7080
+ }
7081
+ async run(interaction) {
7082
+ const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
7083
+ if (!runtimeContext) return;
7084
+ const sessionChannelId = resolveSessionChannelId(runtimeContext.manager, runtimeContext.guildId);
7085
+ if (!await ensureVoiceCommandAccess({
7086
+ interaction,
7087
+ context: params,
7088
+ channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
7089
+ })) return;
7090
+ const result = await runtimeContext.manager.leave({ guildId: runtimeContext.guildId });
7091
+ await interaction.reply({
7092
+ content: result.message,
7093
+ ephemeral: true
7094
+ });
7095
+ }
7096
+ }
7097
+ class StatusCommand extends Command {
7098
+ constructor(..._args3) {
7099
+ super(..._args3);
7100
+ this.name = "status";
7101
+ this.description = "Show active voice sessions";
7102
+ this.defer = true;
7103
+ this.ephemeral = params.ephemeralDefault;
7104
+ }
7105
+ async run(interaction) {
7106
+ const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
7107
+ if (!runtimeContext) return;
7108
+ const sessions = runtimeContext.manager.status().filter((entry) => entry.guildId === runtimeContext.guildId);
7109
+ const sessionChannelId = sessions[0]?.channelId;
7110
+ if (!await ensureVoiceCommandAccess({
7111
+ interaction,
7112
+ context: params,
7113
+ channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
7114
+ })) return;
7115
+ if (sessions.length === 0) {
7116
+ await interaction.reply({
7117
+ content: "No active voice sessions.",
7118
+ ephemeral: true
7119
+ });
7120
+ return;
7121
+ }
7122
+ const lines = sessions.map((entry) => `• ${formatMention({ channelId: entry.channelId })} (guild ${entry.guildId})`);
7123
+ await interaction.reply({
7124
+ content: lines.join("\n"),
7125
+ ephemeral: true
7126
+ });
7127
+ }
7128
+ }
7129
+ return new class extends CommandWithSubcommands {
7130
+ constructor(..._args4) {
7131
+ super(..._args4);
7132
+ this.name = DISCORD_VOICE_COMMAND_SPEC.name;
7133
+ this.description = DISCORD_VOICE_COMMAND_SPEC.description;
7134
+ this.subcommands = [
7135
+ new JoinCommand(),
7136
+ new LeaveCommand(),
7137
+ new StatusCommand()
7138
+ ];
7139
+ }
7140
+ }();
7141
+ }
7142
+ function isVoiceChannelType(type) {
7143
+ return type === ChannelType.GuildVoice || type === ChannelType.GuildStageVoice;
7144
+ }
7145
+ //#endregion
7146
+ //#region extensions/discord/src/monitor/provider.commands.ts
7147
+ const loadPluginCommandRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/plugin-command-runtime"));
7148
+ async function resolveDiscordProviderCommandSpecs(params) {
7149
+ const listSkillCommands = params.listSkillCommandsForAgents ?? listSkillCommandsForAgents;
7150
+ const listNativeCommandSpecs = params.listNativeCommandSpecsForConfig ?? listNativeCommandSpecsForConfig;
7151
+ const maxDiscordCommands = params.maxDiscordCommands ?? 100;
7152
+ let pluginCommandRuntime;
7153
+ if (params.nativeEnabled) pluginCommandRuntime = (await loadPluginCommandRuntime()).createPluginCommandRuntime();
7154
+ const pluginCommandSpecs = pluginCommandRuntime?.listNativeCandidates("discord") ?? [];
7155
+ const onCollision = (normalizedName) => {
7156
+ params.runtime.error?.(danger(`discord: plugin command "/${normalizedName}" duplicates an existing native command. Skipping.`));
7157
+ };
7158
+ const mergePluginCommandSpecs = (primary, collisionHandler) => mergeNativeCommandSpecs({
7159
+ primary,
7160
+ secondary: pluginCommandSpecs,
7161
+ onCollision: collisionHandler
7162
+ });
7163
+ const listPrimaryCommandSpecs = (skillCommands) => {
7164
+ const standardSpecs = listNativeCommandSpecs(params.cfg, {
7165
+ skillCommands,
7166
+ provider: "discord"
7167
+ });
7168
+ if (!params.voiceEnabled) return standardSpecs;
7169
+ const voiceName = DISCORD_VOICE_COMMAND_SPEC.name;
7170
+ return [...standardSpecs.filter((spec) => normalizeLowercaseStringOrEmpty(spec.name) !== voiceName), DISCORD_VOICE_COMMAND_SPEC];
7171
+ };
7172
+ const provisionalCollisions = [];
7173
+ let skillCommands = params.nativeEnabled && params.nativeSkillsEnabled ? listSkillCommands({ cfg: params.cfg }) : [];
7174
+ let commandSpecs = params.nativeEnabled ? mergePluginCommandSpecs(listPrimaryCommandSpecs(skillCommands), (normalizedName) => provisionalCollisions.push(normalizedName)) : [];
7175
+ const initialCommandCount = commandSpecs.length;
7176
+ if (params.nativeEnabled && params.nativeSkillsEnabled && commandSpecs.length > maxDiscordCommands) {
7177
+ skillCommands = [];
7178
+ commandSpecs = mergePluginCommandSpecs(listPrimaryCommandSpecs([]), onCollision);
7179
+ params.runtime.log?.(warn(`${initialCommandCount} commands exceed the ${maxDiscordCommands}-command Discord limit; removing per-skill commands and keeping /skill.`));
7180
+ } else for (const normalizedName of provisionalCollisions) onCollision(normalizedName);
7181
+ 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.`));
7182
+ if (commandSpecs.some((command) => "prepareDispatch" in command)) pluginCommandRuntime?.retainNativeCatalog("discord");
7183
+ return {
7184
+ skillCommands,
7185
+ commandSpecs
7186
+ };
7187
+ }
7188
+ //#endregion
7189
+ //#region extensions/discord/src/monitor/provider.config-log.ts
7190
+ function formatThreadBindingDurationForConfigLabel(durationMs) {
7191
+ const label = formatThreadBindingDurationLabel(durationMs);
7192
+ return label === "disabled" ? "off" : label;
7193
+ }
7194
+ function logDiscordResolvedConfig(params) {
7195
+ const allowFromSummary = summarizeStringEntries({
7196
+ entries: params.allowFrom ?? [],
7197
+ limit: 4,
7198
+ emptyText: "any"
7199
+ });
7200
+ const groupDmChannelSummary = summarizeStringEntries({
7201
+ entries: params.groupDmChannels ?? [],
7202
+ limit: 4,
7203
+ emptyText: "any"
7716
7204
  });
7717
7205
  const guildSummary = summarizeStringEntries({
7718
7206
  entries: Object.keys(params.guildEntries ?? {}),
@@ -7762,13 +7250,6 @@ function resolveDiscordRejectedDeployEntriesSource(rawBody) {
7762
7250
  function readDiscordDeployObjectField(value, field) {
7763
7251
  return value && typeof value === "object" && field in value ? value[field] : void 0;
7764
7252
  }
7765
- function readFiniteNumber(value) {
7766
- if (typeof value === "number" && Number.isFinite(value)) return value;
7767
- if (typeof value === "string" && value.trim().length > 0) return parseStrictFiniteNumber(value);
7768
- }
7769
- function readNonNegativeInteger(value) {
7770
- return parseStrictNonNegativeInteger(value);
7771
- }
7772
7253
  function isAbortLikeError(err) {
7773
7254
  if (!err || typeof err !== "object") return false;
7774
7255
  const name = "name" in err && typeof err.name === "string" ? err.name : void 0;
@@ -7786,8 +7267,8 @@ function formatDiscordDeployRestOperation(err) {
7786
7267
  function formatDiscordDeployErrorMessage(err) {
7787
7268
  if (!isAbortLikeError(err)) return formatErrorMessage$1(err);
7788
7269
  const deployErr = err && typeof err === "object" ? err : {};
7789
- const requestMs = readFiniteNumber(deployErr.deployRequestMs);
7790
- const timeoutMs = readFiniteNumber(deployErr.deployTimeoutMs);
7270
+ const requestMs = parseFiniteNumber$1(deployErr.deployRequestMs);
7271
+ const timeoutMs = parseFiniteNumber$1(deployErr.deployTimeoutMs);
7791
7272
  const operation = formatDiscordDeployRestOperation(deployErr);
7792
7273
  if (!(requestMs !== void 0 || timeoutMs !== void 0 || deployErr.deployRestMethod !== void 0 || deployErr.deployRestPath !== void 0)) return "Discord REST request was aborted";
7793
7274
  const timing = [];
@@ -7800,8 +7281,8 @@ function formatDiscordDeployErrorMessage(err) {
7800
7281
  function resolveDiscordDeployRateLimitDetails(err) {
7801
7282
  if (!err || typeof err !== "object") return;
7802
7283
  const deployErr = err;
7803
- const status = readNonNegativeInteger(deployErr.status) ?? readNonNegativeInteger(deployErr.statusCode);
7804
- const retryAfterSeconds = readFiniteNumber(deployErr.retryAfter) ?? readFiniteNumber(readDiscordDeployObjectField(deployErr.rawBody, "retry_after"));
7284
+ const status = parseStrictNonNegativeInteger$1(deployErr.status) ?? parseStrictNonNegativeInteger$1(deployErr.statusCode);
7285
+ const retryAfterSeconds = parseFiniteNumber$1(deployErr.retryAfter) ?? parseFiniteNumber$1(readDiscordDeployObjectField(deployErr.rawBody, "retry_after"));
7805
7286
  if (!(err instanceof RateLimitError || status === 429 || retryAfterSeconds !== void 0)) return;
7806
7287
  const rawGlobal = readDiscordDeployObjectField(deployErr.rawBody, "global");
7807
7288
  const scope = typeof deployErr.scope === "string" && deployErr.scope.trim().length > 0 ? deployErr.scope : rawGlobal === true ? "global" : rawGlobal === false ? "route" : void 0;
@@ -7891,8 +7372,8 @@ function formatDiscordDeployErrorDetails(err) {
7891
7372
  function isDiscordDeployDailyCreateLimit(err) {
7892
7373
  if (!err || typeof err !== "object") return false;
7893
7374
  const deployErr = err;
7894
- const discordCode = readNonNegativeInteger(deployErr.discordCode);
7895
- const rawCode = readNonNegativeInteger(readDiscordDeployObjectField(deployErr.rawBody, "code"));
7375
+ const discordCode = parseStrictNonNegativeInteger$1(deployErr.discordCode);
7376
+ const rawCode = parseStrictNonNegativeInteger$1(readDiscordDeployObjectField(deployErr.rawBody, "code"));
7896
7377
  return (discordCode === 30034 || rawCode === 30034) && /daily application command creates/i.test(formatErrorMessage$1(err));
7897
7378
  }
7898
7379
  //#endregion
@@ -8013,428 +7494,96 @@ function runDiscordCommandDeployInBackground(params) {
8013
7494
  logDiscordStartupPhase$1({
8014
7495
  runtime: params.runtime,
8015
7496
  accountId: params.accountId,
8016
- phase: "deploy-commands:scheduled",
8017
- startAt: params.startupStartedAt,
8018
- details: "mode=reconcile background=true",
8019
- isVerbose: params.isVerbose
8020
- });
8021
- deployDiscordCommands(params).then(() => {
8022
- logDiscordStartupPhase$1({
8023
- runtime: params.runtime,
8024
- accountId: params.accountId,
8025
- phase: "deploy-commands:done",
8026
- startAt: params.startupStartedAt,
8027
- details: "background=true",
8028
- isVerbose: params.isVerbose
8029
- });
8030
- }).catch((err) => {
8031
- params.runtime.log?.(warn(`[${params.accountId}] slash command deploy failed in background (message send/receive unaffected): ${formatErrorMessage$1(err)}`));
8032
- });
8033
- }
8034
- //#endregion
8035
- //#region extensions/discord/src/monitor/agent-components-reply.ts
8036
- async function replySilently(interaction, params) {
8037
- try {
8038
- await interaction.reply(params);
8039
- } catch {}
8040
- }
8041
- //#endregion
8042
- //#region extensions/discord/src/activities/interaction.ts
8043
- const REGISTRATION_WIDGET_ID = "AAAAAAAAAAAAAAAAAAAAAA";
8044
- const PENDING_LAUNCH_WRITE_BUDGET_MS = 250;
8045
- var DiscordActivityButton = class extends Button {
8046
- constructor(ctx, deps) {
8047
- super();
8048
- this.ctx = ctx;
8049
- this.deps = deps;
8050
- this.label = "Open widget";
8051
- this.customId = buildDiscordActivityCustomId(REGISTRATION_WIDGET_ID);
8052
- this.customIdParser = parseDiscordActivityCustomIdForInteraction;
8053
- this.pendingLaunchFailureLogged = false;
8054
- }
8055
- logPendingLaunchFailure(error) {
8056
- if (this.pendingLaunchFailureLogged) return;
8057
- this.pendingLaunchFailureLogged = true;
8058
- this.deps.logError(`discord activity: failed to record pending launch: ${String(error)}`);
8059
- }
8060
- async run(interaction, data) {
8061
- if (typeof data.widgetId !== "string") {
8062
- await this.deps.reply(interaction, {
8063
- content: "This widget is no longer valid.",
8064
- ephemeral: true
8065
- });
8066
- return;
8067
- }
8068
- const runtime = getDiscordActivitiesRuntime();
8069
- const channelId = interaction.rawData.channel_id;
8070
- const discordUserId = interaction.userId;
8071
- if (!runtime || !channelId || !discordUserId) this.logPendingLaunchFailure(/* @__PURE__ */ new Error("missing activity runtime or interaction identity"));
8072
- else {
8073
- const write = runtime.store.recordPendingLaunch({
8074
- accountId: this.ctx.accountId,
8075
- channelId,
8076
- discordUserId,
8077
- widgetId: data.widgetId,
8078
- createdAt: Date.now()
8079
- }).then(() => "written").catch((error) => {
8080
- this.logPendingLaunchFailure(error);
8081
- return "failed";
8082
- });
8083
- let timer;
8084
- const timeout = new Promise((resolve) => {
8085
- timer = setTimeout(() => resolve("timeout"), PENDING_LAUNCH_WRITE_BUDGET_MS);
8086
- timer.unref?.();
8087
- });
8088
- try {
8089
- if (await Promise.race([write, timeout]) === "timeout") this.logPendingLaunchFailure(/* @__PURE__ */ new Error(`pending launch write exceeded ${PENDING_LAUNCH_WRITE_BUDGET_MS}ms`));
8090
- } finally {
8091
- if (timer) clearTimeout(timer);
8092
- }
8093
- }
8094
- await interaction.launchActivity();
8095
- }
8096
- };
8097
- function createDiscordActivityButton(ctx, applicationId, deps = {}) {
8098
- const runtime = getDiscordActivitiesRuntime();
8099
- if (!runtime || !runtime.isAccountEnabled(ctx.accountId, ctx.cfg)) return null;
8100
- if (applicationId) runtime.registerApplicationId(ctx.accountId, applicationId);
8101
- if (!runtime.resolveAccount(ctx.accountId, ctx.cfg)) return null;
8102
- return new DiscordActivityButton(ctx, {
8103
- reply: deps.reply ?? replySilently,
8104
- logError: deps.logError ?? logError
8105
- });
8106
- }
8107
- //#endregion
8108
- //#region extensions/discord/src/voice/access.ts
8109
- async function authorizeDiscordVoiceIngress(params) {
8110
- const groupPolicy = params.groupPolicy ?? resolveOpenProviderRuntimeGroupPolicy({
8111
- providerConfigPresent: params.cfg.channels?.discord !== void 0,
8112
- groupPolicy: params.discordConfig.groupPolicy,
8113
- defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy
8114
- }).groupPolicy;
8115
- const guildInfo = resolveDiscordGuildEntry({
8116
- guild: params.guild ?? {
8117
- id: params.guildId,
8118
- ...params.guildName ? { name: params.guildName } : {}
8119
- },
8120
- guildId: params.guildId,
8121
- guildEntries: params.discordConfig.guilds
8122
- });
8123
- const channelConfig = params.channelId ? resolveDiscordChannelConfigWithFallback({
8124
- guildInfo,
8125
- channelId: params.channelId,
8126
- channelName: params.channelName,
8127
- channelSlug: params.channelSlug,
8128
- parentId: params.parentId,
8129
- parentName: params.parentName,
8130
- parentSlug: params.parentSlug,
8131
- scope: params.scope
8132
- }) : null;
8133
- if (channelConfig?.enabled === false) return {
8134
- ok: false,
8135
- message: "This channel is disabled."
8136
- };
8137
- const channelAllowlistConfigured = Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
8138
- if (!params.channelId && groupPolicy === "allowlist" && channelAllowlistConfigured) return {
8139
- ok: false,
8140
- message: `${params.channelLabel ?? "This channel"} is not allowlisted for voice commands.`
8141
- };
8142
- const channelAllowed = channelConfig ? channelConfig.allowed : !channelAllowlistConfigured;
8143
- if (!isDiscordGroupAllowedByPolicy({
8144
- groupPolicy,
8145
- guildAllowlisted: Boolean(guildInfo),
8146
- channelAllowlistConfigured,
8147
- channelAllowed
8148
- }) || channelConfig?.allowed === false) return {
8149
- ok: false,
8150
- message: `${params.channelLabel ?? "This channel"} is not allowlisted for voice commands.`
8151
- };
8152
- const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
8153
- channelConfig,
8154
- guildInfo,
8155
- memberRoleIds: params.memberRoleIds,
8156
- sender: params.sender,
8157
- allowNameMatching: false
8158
- });
8159
- const admissionAllowList = normalizeDiscordAllowList(params.admissionAllowFrom ?? params.discordConfig.allowFrom ?? params.discordConfig.allowFrom, [
8160
- "discord:",
8161
- "user:",
8162
- "pk:"
8163
- ]);
8164
- const admissionAllowed = admissionAllowList ? allowListMatches(admissionAllowList, params.sender, { allowNameMatching: false }) : false;
8165
- const useAccessGroups = params.useAccessGroups ?? true;
8166
- return resolveCommandAuthorizedFromAuthorizers({
8167
- useAccessGroups,
8168
- authorizers: useAccessGroups ? [{
8169
- configured: admissionAllowList != null,
8170
- allowed: admissionAllowed
8171
- }, {
8172
- configured: hasAccessRestrictions,
8173
- allowed: memberAllowed
8174
- }] : [{
8175
- configured: hasAccessRestrictions,
8176
- allowed: memberAllowed
8177
- }],
8178
- modeWhenAccessGroupsOff: "configured"
8179
- }) ? {
8180
- ok: true,
8181
- channelConfig
8182
- } : {
8183
- ok: false,
8184
- message: "You are not authorized to use this command."
8185
- };
8186
- }
8187
- //#endregion
8188
- //#region extensions/discord/src/voice/owner-access.ts
8189
- function resolveDiscordVoiceAccess(params) {
8190
- const commandOwnerAllowFrom = resolveDiscordCommandOwnerAllowFrom(params.cfg);
8191
- if (commandOwnerAllowFrom) return {
8192
- admissionAllowFrom: commandOwnerAllowFrom,
8193
- ownerAllowFrom: commandOwnerAllowFrom,
8194
- ownerAllowAll: commandOwnerAllowFrom.includes("*")
8195
- };
8196
- return {
8197
- admissionAllowFrom: resolveDiscordAccountAllowFrom({
8198
- cfg: params.cfg,
8199
- accountId: params.accountId
8200
- }) ?? params.discordConfig.allowFrom ?? params.discordConfig.allowFrom ?? [],
8201
- ownerAllowFrom: [],
8202
- ownerAllowAll: false
8203
- };
8204
- }
8205
- //#endregion
8206
- //#region extensions/discord/src/voice/command.ts
8207
- const VOICE_CHANNEL_TYPES = [ChannelType.GuildVoice, ChannelType.GuildStageVoice];
8208
- async function authorizeVoiceCommand(interaction, params, options) {
8209
- const channelOverride = options?.channelOverride;
8210
- const channel = channelOverride ? void 0 : interaction.channel;
8211
- if (!interaction.guild) return {
8212
- ok: false,
8213
- message: "Voice commands are only available in guilds."
8214
- };
8215
- const user = interaction.user;
8216
- if (!user) return {
8217
- ok: false,
8218
- message: "Unable to resolve command user."
8219
- };
8220
- const channelId = channelOverride?.id ?? channel?.id ?? "";
8221
- const channelContext = await resolveDiscordThreadLikeChannelContext({
8222
- client: interaction.client,
8223
- channel: channelOverride ?? channel,
8224
- channelIdFallback: channelId
8225
- });
8226
- const channelName = channelOverride?.name ?? channelContext.channelName;
8227
- const memberRoleIds = Array.isArray(interaction.rawData.member?.roles) ? interaction.rawData.member.roles.map((roleId) => roleId) : [];
8228
- const sender = resolveDiscordSenderIdentity({
8229
- author: user,
8230
- member: interaction.rawData.member
8231
- });
8232
- const voiceAccess = resolveDiscordVoiceAccess(params);
8233
- const access = await authorizeDiscordVoiceIngress({
8234
- cfg: params.cfg,
8235
- discordConfig: params.discordConfig,
8236
- accountId: params.accountId,
8237
- groupPolicy: params.groupPolicy,
8238
- useAccessGroups: params.useAccessGroups,
8239
- guild: interaction.guild,
8240
- guildId: interaction.guild.id,
8241
- channelId,
8242
- channelName,
8243
- channelSlug: channelContext.channelSlug,
8244
- parentId: channelOverride?.parentId ?? channelContext.threadParentId,
8245
- parentName: channelContext.threadParentName,
8246
- parentSlug: channelContext.threadParentSlug,
8247
- scope: channelContext.isThreadChannel ? "thread" : "channel",
8248
- channelLabel: channelId ? formatMention({ channelId }) : "This channel",
8249
- memberRoleIds,
8250
- admissionAllowFrom: voiceAccess.admissionAllowFrom,
8251
- sender: {
8252
- id: sender.id,
8253
- name: sender.name,
8254
- tag: sender.tag
8255
- }
8256
- });
8257
- if (!access.ok) return {
8258
- ok: false,
8259
- message: access.message
8260
- };
8261
- return {
8262
- ok: true,
8263
- guildId: interaction.guild.id
8264
- };
8265
- }
8266
- async function resolveVoiceCommandRuntimeContext(interaction, params) {
8267
- const guildId = interaction.guild?.id;
8268
- if (!guildId) {
8269
- await interaction.reply({
8270
- content: "Unable to resolve guild for this command.",
8271
- ephemeral: true
8272
- });
8273
- return null;
8274
- }
8275
- const manager = params.getManager();
8276
- if (!manager) {
8277
- await interaction.reply({
8278
- content: "Voice manager is not available yet.",
8279
- ephemeral: true
8280
- });
8281
- return null;
8282
- }
8283
- return {
8284
- guildId,
8285
- manager
8286
- };
8287
- }
8288
- async function ensureVoiceCommandAccess(params) {
8289
- const access = await authorizeVoiceCommand(params.interaction, params.context, { channelOverride: params.channelOverride });
8290
- if (access.ok) return true;
8291
- await params.interaction.reply({
8292
- content: access.message ?? "Not authorized.",
8293
- ephemeral: true
8294
- });
8295
- return false;
8296
- }
8297
- function createDiscordVoiceCommand(params) {
8298
- const resolveSessionChannelId = (manager, guildId) => manager.status().find((entry) => entry.guildId === guildId)?.channelId;
8299
- class JoinCommand extends Command {
8300
- constructor(..._args) {
8301
- super(..._args);
8302
- this.name = "join";
8303
- this.description = "Join a voice channel";
8304
- this.defer = true;
8305
- this.ephemeral = params.ephemeralDefault;
8306
- this.options = [{
8307
- name: "channel",
8308
- description: "Voice channel to join",
8309
- type: ApplicationCommandOptionType.Channel,
8310
- required: true,
8311
- channel_types: VOICE_CHANNEL_TYPES
8312
- }];
8313
- }
8314
- async run(interaction) {
8315
- const channel = await interaction.options.getChannel("channel", true);
8316
- if (!channel || !("id" in channel)) {
8317
- await interaction.reply({
8318
- content: "Voice channel not found.",
8319
- ephemeral: true
8320
- });
8321
- return;
8322
- }
8323
- const access = await authorizeVoiceCommand(interaction, params, { channelOverride: {
8324
- id: channel.id,
8325
- name: resolveDiscordChannelNameSafe(channel)
8326
- } });
8327
- if (!access.ok) {
8328
- await interaction.reply({
8329
- content: access.message ?? "Not authorized.",
8330
- ephemeral: true
8331
- });
8332
- return;
8333
- }
8334
- if (!isVoiceChannelType(channel.type)) {
8335
- await interaction.reply({
8336
- content: "That is not a voice channel.",
8337
- ephemeral: true
8338
- });
8339
- return;
8340
- }
8341
- const guildId = access.guildId ?? ("guildId" in channel ? channel.guildId : void 0);
8342
- if (!guildId) {
8343
- await interaction.reply({
8344
- content: "Unable to resolve guild for this voice channel.",
8345
- ephemeral: true
8346
- });
8347
- return;
8348
- }
8349
- const manager = params.getManager();
8350
- if (!manager) {
8351
- await interaction.reply({
8352
- content: "Voice manager is not available yet.",
8353
- ephemeral: true
8354
- });
8355
- return;
8356
- }
8357
- const result = await manager.join({
8358
- guildId,
8359
- channelId: channel.id
8360
- });
8361
- await interaction.reply({
8362
- content: result.message,
8363
- ephemeral: true
8364
- });
8365
- }
7497
+ phase: "deploy-commands:scheduled",
7498
+ startAt: params.startupStartedAt,
7499
+ details: "mode=reconcile background=true",
7500
+ isVerbose: params.isVerbose
7501
+ });
7502
+ deployDiscordCommands(params).then(() => {
7503
+ logDiscordStartupPhase$1({
7504
+ runtime: params.runtime,
7505
+ accountId: params.accountId,
7506
+ phase: "deploy-commands:done",
7507
+ startAt: params.startupStartedAt,
7508
+ details: "background=true",
7509
+ isVerbose: params.isVerbose
7510
+ });
7511
+ }).catch((err) => {
7512
+ params.runtime.log?.(warn(`[${params.accountId}] slash command deploy failed in background (message send/receive unaffected): ${formatErrorMessage$1(err)}`));
7513
+ });
7514
+ }
7515
+ //#endregion
7516
+ //#region extensions/discord/src/monitor/agent-components-reply.ts
7517
+ async function replySilently(interaction, params) {
7518
+ try {
7519
+ await interaction.reply(params);
7520
+ } catch {}
7521
+ }
7522
+ //#endregion
7523
+ //#region extensions/discord/src/activities/interaction.ts
7524
+ const REGISTRATION_WIDGET_ID = "AAAAAAAAAAAAAAAAAAAAAA";
7525
+ const PENDING_LAUNCH_WRITE_BUDGET_MS = 250;
7526
+ var DiscordActivityButton = class extends Button {
7527
+ constructor(ctx, deps) {
7528
+ super();
7529
+ this.ctx = ctx;
7530
+ this.deps = deps;
7531
+ this.label = "Open widget";
7532
+ this.customId = buildDiscordActivityCustomId(REGISTRATION_WIDGET_ID);
7533
+ this.customIdParser = parseDiscordActivityCustomIdForInteraction;
7534
+ this.pendingLaunchFailureLogged = false;
8366
7535
  }
8367
- class LeaveCommand extends Command {
8368
- constructor(..._args2) {
8369
- super(..._args2);
8370
- this.name = "leave";
8371
- this.description = "Leave the current voice channel";
8372
- this.defer = true;
8373
- this.ephemeral = params.ephemeralDefault;
8374
- }
8375
- async run(interaction) {
8376
- const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
8377
- if (!runtimeContext) return;
8378
- const sessionChannelId = resolveSessionChannelId(runtimeContext.manager, runtimeContext.guildId);
8379
- if (!await ensureVoiceCommandAccess({
8380
- interaction,
8381
- context: params,
8382
- channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
8383
- })) return;
8384
- const result = await runtimeContext.manager.leave({ guildId: runtimeContext.guildId });
8385
- await interaction.reply({
8386
- content: result.message,
7536
+ logPendingLaunchFailure(error) {
7537
+ if (this.pendingLaunchFailureLogged) return;
7538
+ this.pendingLaunchFailureLogged = true;
7539
+ this.deps.logError(`discord activity: failed to record pending launch: ${String(error)}`);
7540
+ }
7541
+ async run(interaction, data) {
7542
+ if (typeof data.widgetId !== "string") {
7543
+ await this.deps.reply(interaction, {
7544
+ content: "This widget is no longer valid.",
8387
7545
  ephemeral: true
8388
7546
  });
7547
+ return;
8389
7548
  }
8390
- }
8391
- class StatusCommand extends Command {
8392
- constructor(..._args3) {
8393
- super(..._args3);
8394
- this.name = "status";
8395
- this.description = "Show active voice sessions";
8396
- this.defer = true;
8397
- this.ephemeral = params.ephemeralDefault;
8398
- }
8399
- async run(interaction) {
8400
- const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
8401
- if (!runtimeContext) return;
8402
- const sessions = runtimeContext.manager.status().filter((entry) => entry.guildId === runtimeContext.guildId);
8403
- const sessionChannelId = sessions[0]?.channelId;
8404
- if (!await ensureVoiceCommandAccess({
8405
- interaction,
8406
- context: params,
8407
- channelOverride: sessionChannelId ? { id: sessionChannelId } : void 0
8408
- })) return;
8409
- if (sessions.length === 0) {
8410
- await interaction.reply({
8411
- content: "No active voice sessions.",
8412
- ephemeral: true
8413
- });
8414
- return;
8415
- }
8416
- const lines = sessions.map((entry) => `• ${formatMention({ channelId: entry.channelId })} (guild ${entry.guildId})`);
8417
- await interaction.reply({
8418
- content: lines.join("\n"),
8419
- ephemeral: true
7549
+ const runtime = getDiscordActivitiesRuntime();
7550
+ const channelId = interaction.rawData.channel_id;
7551
+ const discordUserId = interaction.userId;
7552
+ if (!runtime || !channelId || !discordUserId) this.logPendingLaunchFailure(/* @__PURE__ */ new Error("missing activity runtime or interaction identity"));
7553
+ else {
7554
+ const write = runtime.store.recordPendingLaunch({
7555
+ accountId: this.ctx.accountId,
7556
+ channelId,
7557
+ discordUserId,
7558
+ widgetId: data.widgetId,
7559
+ createdAt: Date.now()
7560
+ }).then(() => "written").catch((error) => {
7561
+ this.logPendingLaunchFailure(error);
7562
+ return "failed";
8420
7563
  });
7564
+ let timer;
7565
+ const timeout = new Promise((resolve) => {
7566
+ timer = setTimeout(() => resolve("timeout"), PENDING_LAUNCH_WRITE_BUDGET_MS);
7567
+ timer.unref?.();
7568
+ });
7569
+ try {
7570
+ if (await Promise.race([write, timeout]) === "timeout") this.logPendingLaunchFailure(/* @__PURE__ */ new Error(`pending launch write exceeded ${PENDING_LAUNCH_WRITE_BUDGET_MS}ms`));
7571
+ } finally {
7572
+ if (timer) clearTimeout(timer);
7573
+ }
8421
7574
  }
7575
+ await interaction.launchActivity();
8422
7576
  }
8423
- return new class extends CommandWithSubcommands {
8424
- constructor(..._args4) {
8425
- super(..._args4);
8426
- this.name = "vc";
8427
- this.description = "Voice channel controls";
8428
- this.subcommands = [
8429
- new JoinCommand(),
8430
- new LeaveCommand(),
8431
- new StatusCommand()
8432
- ];
8433
- }
8434
- }();
8435
- }
8436
- function isVoiceChannelType(type) {
8437
- return type === ChannelType.GuildVoice || type === ChannelType.GuildStageVoice;
7577
+ };
7578
+ function createDiscordActivityButton(ctx, applicationId, deps = {}) {
7579
+ const runtime = getDiscordActivitiesRuntime();
7580
+ if (!runtime || !runtime.isAccountEnabled(ctx.accountId, ctx.cfg)) return null;
7581
+ if (applicationId) runtime.registerApplicationId(ctx.accountId, applicationId);
7582
+ if (!runtime.resolveAccount(ctx.accountId, ctx.cfg)) return null;
7583
+ return new DiscordActivityButton(ctx, {
7584
+ reply: deps.reply ?? replySilently,
7585
+ logError: deps.logError ?? logError
7586
+ });
8438
7587
  }
8439
7588
  //#endregion
8440
7589
  //#region extensions/discord/src/monitor/agent-components-context.ts
@@ -8472,6 +7621,14 @@ async function ackComponentInteraction(params) {
8472
7621
  logError(`${params.label}: failed to acknowledge interaction: ${String(err)}`);
8473
7622
  }
8474
7623
  }
7624
+ async function replyUnavailableComponentInteraction(interaction, content) {
7625
+ try {
7626
+ await interaction.reply({
7627
+ content,
7628
+ ephemeral: true
7629
+ });
7630
+ } catch {}
7631
+ }
8475
7632
  function resolveDiscordChannelContext(interaction) {
8476
7633
  const channel = interaction.channel;
8477
7634
  const channelInfo = resolveDiscordChannelInfoSafe(channel);
@@ -8967,12 +8124,6 @@ function formatModalSubmissionText(entry, interaction) {
8967
8124
  if (lines.length === 1) lines.push("- (no values)");
8968
8125
  return lines.join("\n");
8969
8126
  }
8970
- function resolveDiscordInteractionId(interaction) {
8971
- const rawId = interaction.rawData && typeof interaction.rawData === "object" && "id" in interaction.rawData ? interaction.rawData.id : void 0;
8972
- if (typeof rawId === "string" && rawId.trim()) return rawId.trim();
8973
- if (typeof rawId === "number" && Number.isFinite(rawId)) return String(rawId);
8974
- return `discord-interaction:${Date.now()}`;
8975
- }
8976
8127
  //#endregion
8977
8128
  //#region extensions/discord/src/monitor/agent-components-helpers.ts
8978
8129
  const AGENT_BUTTON_KEY = "agent";
@@ -8983,7 +8134,8 @@ function resolveReplyContext(message, resolveDiscordMessageText) {
8983
8134
  const referenced = message.referencedMessage;
8984
8135
  if (!referenced?.author) return null;
8985
8136
  const referencedText = resolveDiscordMessageText(referenced, { includeForwarded: true });
8986
- if (!referencedText) return null;
8137
+ const hasVisibleMedia = referenced.attachments.length > 0 || !referencedText && resolveDiscordMessageStickers(referenced).length > 0;
8138
+ if (!referencedText && !hasVisibleMedia) return null;
8987
8139
  const sender = resolveDiscordSenderIdentity({
8988
8140
  author: referenced.author,
8989
8141
  pluralkitInfo: null
@@ -8999,7 +8151,7 @@ function resolveReplyContext(message, resolveDiscordMessageText) {
8999
8151
  const roles = referenced.member?.roles;
9000
8152
  return Array.isArray(roles) ? roles.map((roleId) => roleId) : void 0;
9001
8153
  })(),
9002
- body: referencedText,
8154
+ ...referencedText ? { body: referencedText } : {},
9003
8155
  timestamp: resolveTimestampMs(referenced.timestamp)
9004
8156
  };
9005
8157
  }
@@ -9046,7 +8198,7 @@ function stripDiscordInternalChannelLines(text) {
9046
8198
  return kept.join("\n");
9047
8199
  }
9048
8200
  function sanitizeDiscordFrontChannelText(text) {
9049
- return collapseExcessBlankLines(stripDiscordInternalChannelLines(stripPlainTextToolCallBlocks(sanitizeAssistantVisibleText(stripPlainTextToolCallBlocks(text))))).trim();
8201
+ return collapseExcessBlankLines(stripDiscordInternalChannelLines(stripPlainTextToolCallBlocks(sanitizeAssistantVisibleText(stripPlainTextToolCallBlocks(text, { resolveProtectedRanges: findCodeRegions })), { resolveProtectedRanges: findCodeRegions }))).trim();
9050
8202
  }
9051
8203
  function sanitizeDiscordFrontChannelReplyPayloads(payloads, options = {}) {
9052
8204
  const preserveVerboseToolProgress = options.kind === "tool";
@@ -9143,7 +8295,7 @@ function formatDiscordReasoningPayload(payload) {
9143
8295
  async function deliverDiscordReply(params) {
9144
8296
  params.runtime;
9145
8297
  const delivery = resolveDiscordDeliveryOptions(params);
9146
- const payloads = sanitizeDiscordFrontChannelReplyPayloads(params.replies, { kind: params.kind }).map(formatDiscordReasoningPayload);
8298
+ const payloads = sanitizeDiscordFrontChannelReplyPayloads(params.replies, { kind: params.kind }).map(formatDiscordReasoningPayload).map((payload) => params.bindPendingFinalDelivery?.(payload) ?? payload);
9147
8299
  if (payloads.length === 0) return {
9148
8300
  visibleReplySent: false,
9149
8301
  suppression: { reason: "no_visible_result" }
@@ -9173,7 +8325,7 @@ async function deliverDiscordReply(params) {
9173
8325
  requesterAccountId: params.accountId
9174
8326
  })
9175
8327
  });
9176
- if (send.status === "failed" || send.status === "partial_failed") throw send.error;
8328
+ if (send.status === "failed") throw send.error;
9177
8329
  if (send.status === "suppressed") {
9178
8330
  const hookEffect = send.payloadOutcomes?.find((outcome) => outcome.status === "suppressed")?.hookEffect;
9179
8331
  return {
@@ -9185,17 +8337,19 @@ async function deliverDiscordReply(params) {
9185
8337
  }
9186
8338
  };
9187
8339
  }
9188
- const results = send.results;
9189
- if (results.length === 0) throw new Error(`discord final reply produced no delivered message for ${delivery.to}`);
9190
- return {
9191
- messageIds: results.flatMap((result) => result.messageId ? [result.messageId] : []),
8340
+ if (send.results.length === 0) throw new Error(`discord final reply produced no delivered message for ${delivery.to}`);
8341
+ const deliveryResult = {
8342
+ messageIds: listMessageReceiptPlatformIds(send.receipt),
8343
+ receipt: send.receipt,
9192
8344
  visibleReplySent: true
9193
8345
  };
8346
+ if (send.status === "partial_failed") throw createChannelPartialDeliveryError(send.error, deliveryResult);
8347
+ return deliveryResult;
9194
8348
  }
9195
8349
  //#endregion
9196
8350
  //#region extensions/discord/src/monitor/agent-components.dispatch.ts
9197
8351
  const loadConversationRuntime$1 = createLazyRuntimeModule(() => import("./agent-components.runtime-CEPrf2SY.js"));
9198
- const loadTypingRuntime = createLazyRuntimeModule(() => import("./typing--Mzo2kM7.js").then((n) => n.n));
8352
+ const loadTypingRuntime = createLazyRuntimeModule(() => import("./typing-oLiS5Z13.js").then((n) => n.n));
9199
8353
  function buildDiscordComponentConversationLabel(params) {
9200
8354
  if (params.interactionCtx.isDirectMessage) return buildDirectLabel(params.interactionCtx.user);
9201
8355
  if (params.interactionCtx.isGroupDm) return `Group DM #${params.channelCtx.channelName ?? params.interactionCtx.channelId} channel id:${params.interactionCtx.channelId}`;
@@ -9317,6 +8471,14 @@ async function dispatchDiscordComponentEvent(params) {
9317
8471
  SessionKey: sessionKey,
9318
8472
  AccountId: accountId,
9319
8473
  ChatType: chatType,
8474
+ ...buildDiscordConversationRouteContext({
8475
+ isDirectMessage: interactionCtx.isDirectMessage,
8476
+ isGroupDm: interactionCtx.isGroupDm,
8477
+ directUserId: interactionCtx.userId,
8478
+ conversationId: interactionCtx.channelId,
8479
+ isThread: channelCtx.isThread,
8480
+ parentConversationId: channelCtx.parentId
8481
+ }),
9320
8482
  ConversationLabel: fromLabel,
9321
8483
  SenderName: senderName,
9322
8484
  SenderId: interactionCtx.userId,
@@ -9385,6 +8547,7 @@ async function dispatchDiscordComponentEvent(params) {
9385
8547
  sessionKey
9386
8548
  },
9387
8549
  ctxPayload,
8550
+ dispatchReplyFromConfig: ctx.channelRuntime?.reply?.dispatchReplyFromConfig,
9388
8551
  record: {
9389
8552
  updateLastRoute: interactionCtx.isDirectMessage ? {
9390
8553
  sessionKey: inboundLastRouteSessionKey,
@@ -9425,7 +8588,8 @@ async function dispatchDiscordComponentEvent(params) {
9425
8588
  tableMode,
9426
8589
  chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
9427
8590
  mediaLocalRoots,
9428
- kind: info.kind
8591
+ kind: info.kind,
8592
+ bindPendingFinalDelivery: info.bindPendingFinalDelivery
9429
8593
  });
9430
8594
  if (result.visibleReplySent) replyReference.markSent();
9431
8595
  return result;
@@ -9455,33 +8619,14 @@ async function dispatchDiscordComponentEvent(params) {
9455
8619
  }
9456
8620
  //#endregion
9457
8621
  //#region extensions/discord/src/interactive-dispatch.ts
8622
+ const dispatchDiscordInteractive = createChannelInteractiveDispatcher({
8623
+ channel: "discord",
8624
+ interactiveKey: "interaction"
8625
+ });
9458
8626
  async function dispatchDiscordPluginInteractiveHandler(params) {
9459
- return await dispatchPluginInteractiveHandler({
9460
- channel: "discord",
9461
- data: params.data,
9462
- dedupeId: params.interactionId,
9463
- onMatched: params.onMatched,
9464
- invoke: ({ registration, namespace, payload }) => registration.handler({
9465
- ...params.ctx,
9466
- channel: "discord",
9467
- interaction: {
9468
- ...params.ctx.interaction,
9469
- data: params.data,
9470
- namespace,
9471
- payload
9472
- },
9473
- respond: params.respond,
9474
- ...createInteractiveConversationBindingHelpers({
9475
- registration,
9476
- senderId: params.ctx.senderId,
9477
- conversation: {
9478
- channel: "discord",
9479
- accountId: params.ctx.accountId,
9480
- conversationId: params.ctx.conversationId,
9481
- parentConversationId: params.ctx.parentConversationId
9482
- }
9483
- })
9484
- })
8627
+ return await dispatchDiscordInteractive({
8628
+ ...params,
8629
+ dedupeId: params.interactionId
9485
8630
  });
9486
8631
  }
9487
8632
  //#endregion
@@ -9512,10 +8657,11 @@ async function dispatchPluginDiscordInteractiveEvent(params) {
9512
8657
  },
9513
8658
  reply: async ({ text, ephemeral = true }) => {
9514
8659
  responded = true;
9515
- await params.interaction.reply({
8660
+ const payload = {
9516
8661
  content: text,
9517
8662
  ephemeral
9518
- });
8663
+ };
8664
+ await (acknowledged ? params.interaction.followUp(payload) : params.interaction.reply(payload));
9519
8665
  },
9520
8666
  followUp: async ({ text, ephemeral = true }) => {
9521
8667
  responded = true;
@@ -9573,10 +8719,10 @@ async function dispatchPluginDiscordInteractiveEvent(params) {
9573
8719
  }
9574
8720
  const dispatched = await dispatchDiscordPluginInteractiveHandler({
9575
8721
  data: params.data,
9576
- interactionId: resolveDiscordInteractionId(params.interaction),
8722
+ interactionId: params.interaction.id,
9577
8723
  ctx: {
9578
8724
  accountId: params.ctx.accountId,
9579
- interactionId: resolveDiscordInteractionId(params.interaction),
8725
+ interactionId: params.interaction.id,
9580
8726
  conversationId: normalizedConversationId,
9581
8727
  parentConversationId: params.channelCtx.parentId,
9582
8728
  guildId: params.interactionCtx.rawGuildId,
@@ -9608,17 +8754,12 @@ async function dispatchPluginDiscordInteractiveEvent(params) {
9608
8754
  }
9609
8755
  //#endregion
9610
8756
  //#region extensions/discord/src/monitor/agent-components.handlers.ts
9611
- const loadComponentsRuntime = createLazyRuntimeModule(() => import("./outbound-session-route-H-HoyH5U.js").then((n) => n.n));
8757
+ const loadComponentsRuntime = createLazyRuntimeModule(() => import("./components-ezGKo_Yh.js").then((n) => n.t));
9612
8758
  async function handleDiscordComponentEvent(params) {
9613
8759
  const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
9614
8760
  if (!parsed) {
9615
8761
  logError(`${params.label}: failed to parse component data`);
9616
- try {
9617
- await params.interaction.reply({
9618
- content: "This component is no longer valid.",
9619
- ephemeral: true
9620
- });
9621
- } catch {}
8762
+ await replyUnavailableComponentInteraction(params.interaction, "This component is no longer valid.");
9622
8763
  return;
9623
8764
  }
9624
8765
  const entry = await resolveDiscordComponentEntryWithPersistence({
@@ -9626,12 +8767,7 @@ async function handleDiscordComponentEvent(params) {
9626
8767
  consume: false
9627
8768
  });
9628
8769
  if (!entry) {
9629
- try {
9630
- await params.interaction.reply({
9631
- content: "This component has expired.",
9632
- ephemeral: true
9633
- });
9634
- } catch {}
8770
+ await replyUnavailableComponentInteraction(params.interaction, "This component has expired.");
9635
8771
  return;
9636
8772
  }
9637
8773
  const unauthorizedReply = `You are not authorized to use this ${params.componentLabel}.`;
@@ -9659,21 +8795,11 @@ async function handleDiscordComponentEvent(params) {
9659
8795
  consume: !entry.reusable
9660
8796
  });
9661
8797
  if (!consumed) {
9662
- try {
9663
- await params.interaction.reply({
9664
- content: "This component has expired.",
9665
- ephemeral: true
9666
- });
9667
- } catch {}
8798
+ await replyUnavailableComponentInteraction(params.interaction, "This component has expired.");
9668
8799
  return;
9669
8800
  }
9670
8801
  if (consumed.kind === "modal-trigger") {
9671
- try {
9672
- await params.interaction.reply({
9673
- content: "This form is no longer available.",
9674
- ephemeral: true
9675
- });
9676
- } catch {}
8802
+ await replyUnavailableComponentInteraction(params.interaction, "This form is no longer available.");
9677
8803
  return;
9678
8804
  }
9679
8805
  const values = params.values ? mapSelectValues(consumed, params.values) : void 0;
@@ -9726,12 +8852,7 @@ async function handleDiscordModalTrigger(params) {
9726
8852
  const parsed = parseDiscordComponentData(params.data, resolveInteractionCustomId(params.interaction));
9727
8853
  if (!parsed) {
9728
8854
  logError(`${params.label}: failed to parse modal trigger data`);
9729
- try {
9730
- await params.interaction.reply({
9731
- content: "This button is no longer valid.",
9732
- ephemeral: true
9733
- });
9734
- } catch {}
8855
+ await replyUnavailableComponentInteraction(params.interaction, "This button is no longer valid.");
9735
8856
  return;
9736
8857
  }
9737
8858
  const entry = await resolveDiscordComponentEntryWithPersistence({
@@ -9739,22 +8860,12 @@ async function handleDiscordModalTrigger(params) {
9739
8860
  consume: false
9740
8861
  });
9741
8862
  if (!entry || entry.kind !== "modal-trigger") {
9742
- try {
9743
- await params.interaction.reply({
9744
- content: "This button has expired.",
9745
- ephemeral: true
9746
- });
9747
- } catch {}
8863
+ await replyUnavailableComponentInteraction(params.interaction, "This button has expired.");
9748
8864
  return;
9749
8865
  }
9750
8866
  const modalId = entry.modalId ?? parsed.modalId;
9751
8867
  if (!modalId) {
9752
- try {
9753
- await params.interaction.reply({
9754
- content: "This form is no longer available.",
9755
- ephemeral: true
9756
- });
9757
- } catch {}
8868
+ await replyUnavailableComponentInteraction(params.interaction, "This form is no longer available.");
9758
8869
  return;
9759
8870
  }
9760
8871
  const unauthorizedReply = "You are not authorized to use this form.";
@@ -9782,12 +8893,7 @@ async function handleDiscordModalTrigger(params) {
9782
8893
  consume: !entry.reusable
9783
8894
  });
9784
8895
  if (!consumed) {
9785
- try {
9786
- await params.interaction.reply({
9787
- content: "This form has expired.",
9788
- ephemeral: true
9789
- });
9790
- } catch {}
8896
+ await replyUnavailableComponentInteraction(params.interaction, "This form has expired.");
9791
8897
  return;
9792
8898
  }
9793
8899
  const modalEntry = await resolveDiscordModalEntryWithPersistence({
@@ -9795,18 +8901,14 @@ async function handleDiscordModalTrigger(params) {
9795
8901
  consume: false
9796
8902
  });
9797
8903
  if (!modalEntry) {
9798
- try {
9799
- await params.interaction.reply({
9800
- content: "This form has expired.",
9801
- ephemeral: true
9802
- });
9803
- } catch {}
8904
+ await replyUnavailableComponentInteraction(params.interaction, "This form has expired.");
9804
8905
  return;
9805
8906
  }
9806
8907
  try {
9807
8908
  await params.interaction.showModal((await loadComponentsRuntime()).createDiscordFormModal(modalEntry));
9808
8909
  } catch (err) {
9809
8910
  logError(`${params.label}: failed to show modal: ${String(err)}`);
8911
+ await replyUnavailableComponentInteraction(params.interaction, "Could not open this form. Request a new form and try again.");
9810
8912
  }
9811
8913
  }
9812
8914
  const discordComponentControlHandlers = {
@@ -9828,12 +8930,7 @@ var DiscordComponentModal = class extends Modal {
9828
8930
  const modalId = parseDiscordModalId(data, resolveInteractionCustomId(interaction));
9829
8931
  if (!modalId) {
9830
8932
  logError("discord component modal: missing modal id");
9831
- try {
9832
- await interaction.reply({
9833
- content: "This form is no longer valid.",
9834
- ephemeral: true
9835
- });
9836
- } catch {}
8933
+ await replyUnavailableComponentInteraction(interaction, "This form is no longer valid.");
9837
8934
  return;
9838
8935
  }
9839
8936
  const modalEntry = await resolveDiscordModalEntryWithPersistence({
@@ -9841,12 +8938,7 @@ var DiscordComponentModal = class extends Modal {
9841
8938
  consume: false
9842
8939
  });
9843
8940
  if (!modalEntry) {
9844
- try {
9845
- await interaction.reply({
9846
- content: "This form has expired.",
9847
- ephemeral: true
9848
- });
9849
- } catch {}
8941
+ await replyUnavailableComponentInteraction(interaction, "This form has expired.");
9850
8942
  return;
9851
8943
  }
9852
8944
  const unauthorizedReply = "You are not authorized to use this form.";
@@ -9879,12 +8971,7 @@ var DiscordComponentModal = class extends Modal {
9879
8971
  consume: !modalEntry.reusable
9880
8972
  });
9881
8973
  if (!consumed) {
9882
- try {
9883
- await interaction.reply({
9884
- content: "This form has expired.",
9885
- ephemeral: true
9886
- });
9887
- } catch {}
8974
+ await replyUnavailableComponentInteraction(interaction, "This form has expired.");
9888
8975
  return;
9889
8976
  }
9890
8977
  if (consumed.callbackData) {
@@ -9933,12 +9020,7 @@ async function runAgentSystemControlInteraction(params) {
9933
9020
  const parsed = parseAgentComponentData(params.data);
9934
9021
  if (!parsed) {
9935
9022
  logError(`${params.label}: failed to parse component data`);
9936
- try {
9937
- await params.interaction.reply({
9938
- content: params.invalidReply,
9939
- ephemeral: true
9940
- });
9941
- } catch {}
9023
+ await replyUnavailableComponentInteraction(params.interaction, params.invalidReply);
9942
9024
  return;
9943
9025
  }
9944
9026
  const { componentId } = parsed;
@@ -9979,10 +9061,7 @@ async function runAgentSystemControlInteraction(params) {
9979
9061
  userId
9980
9062
  });
9981
9063
  logDebug(`${params.label}: enqueuing event for channel ${channelId}: ${eventText}`);
9982
- enqueueSystemEvent$1(eventText, {
9983
- sessionKey: route.sessionKey,
9984
- contextKey: `${params.contextKeyPrefix}:${channelId}:${componentId}:${userId}`
9985
- });
9064
+ enqueueRoutedSystemEvent$1(eventText, route, { contextKey: `${params.contextKeyPrefix}:${channelId}:${componentId}:${userId}:${params.interaction.id}` });
9986
9065
  await ackComponentInteraction({
9987
9066
  interaction: params.interaction,
9988
9067
  replyOpts,
@@ -10237,7 +9316,7 @@ var ExecApprovalButton = class extends Button {
10237
9316
  try {
10238
9317
  await interaction.acknowledge();
10239
9318
  } catch {}
10240
- const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.approvalKind, parsed.action);
9319
+ const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.approvalKind, parsed.action, userId);
10241
9320
  if (!result.ok) {
10242
9321
  try {
10243
9322
  await interaction.followUp({
@@ -10274,7 +9353,7 @@ function createDiscordExecApprovalButtonContext(params) {
10274
9353
  accountId: params.accountId,
10275
9354
  configOverride: params.config
10276
9355
  }),
10277
- resolveApproval: async (approvalId, approvalKind, decision) => {
9356
+ resolveApproval: async (approvalId, approvalKind, decision, senderId) => {
10278
9357
  try {
10279
9358
  return {
10280
9359
  ok: true,
@@ -10283,8 +9362,10 @@ function createDiscordExecApprovalButtonContext(params) {
10283
9362
  approvalId,
10284
9363
  approvalKind,
10285
9364
  decision,
10286
- gatewayUrl: params.gatewayUrl,
10287
- clientDisplayName: `Discord approval (${params.accountId})`
9365
+ channel: "discord",
9366
+ accountId: params.accountId,
9367
+ senderId,
9368
+ gatewayUrl: params.gatewayUrl
10288
9369
  })
10289
9370
  };
10290
9371
  } catch (err) {
@@ -10368,24 +9449,26 @@ function createDiscordQuestionButton(params) {
10368
9449
  //#region extensions/discord/src/monitor/provider.interactions.ts
10369
9450
  function createDiscordProviderInteractionSurface(params) {
10370
9451
  const createNativeCommand = params.createNativeCommand ?? createDiscordNativeCommand;
10371
- const commands = params.commandSpecs.map((spec) => createNativeCommand({
10372
- command: spec,
10373
- cfg: params.cfg,
10374
- discordConfig: params.discordConfig,
10375
- accountId: params.accountId,
10376
- sessionPrefix: params.sessionPrefix,
10377
- ephemeralDefault: params.ephemeralDefault,
10378
- threadBindings: params.threadBindings
10379
- }));
10380
- if (params.nativeEnabled && params.voiceEnabled) commands.push(createDiscordVoiceCommand({
10381
- cfg: params.cfg,
10382
- discordConfig: params.discordConfig,
10383
- accountId: params.accountId,
10384
- groupPolicy: params.groupPolicy,
10385
- useAccessGroups: params.useAccessGroups,
10386
- getManager: () => params.voiceManagerRef.current,
10387
- ephemeralDefault: params.ephemeralDefault
10388
- }));
9452
+ const commands = params.commandSpecs.map((spec) => {
9453
+ if (params.nativeEnabled && params.voiceEnabled && spec.name === DISCORD_VOICE_COMMAND_SPEC.name) return createDiscordVoiceCommand({
9454
+ cfg: params.cfg,
9455
+ discordConfig: params.discordConfig,
9456
+ accountId: params.accountId,
9457
+ groupPolicy: params.groupPolicy,
9458
+ useAccessGroups: params.useAccessGroups,
9459
+ getManager: () => params.voiceManagerRef.current,
9460
+ ephemeralDefault: params.ephemeralDefault
9461
+ });
9462
+ return createNativeCommand({
9463
+ command: spec,
9464
+ cfg: params.cfg,
9465
+ discordConfig: params.discordConfig,
9466
+ accountId: params.accountId,
9467
+ sessionPrefix: params.sessionPrefix,
9468
+ ephemeralDefault: params.ephemeralDefault,
9469
+ threadBindings: params.threadBindings
9470
+ });
9471
+ });
10389
9472
  const execApprovalsConfig = params.discordConfig.execApprovals ?? {};
10390
9473
  const execApprovalsEnabled = isDiscordExecApprovalClientEnabled({
10391
9474
  cfg: params.cfg,
@@ -10453,6 +9536,7 @@ function createDiscordProviderInteractionSurface(params) {
10453
9536
  allowFrom: params.allowFrom,
10454
9537
  dmPolicy: params.dmPolicy,
10455
9538
  runtime: params.runtime,
9539
+ channelRuntime: params.channelRuntime,
10456
9540
  token: params.token
10457
9541
  }, params.applicationId);
10458
9542
  if (activityButton) components.push(activityButton);
@@ -10665,7 +9749,8 @@ function registerDiscordMonitorListeners(params) {
10665
9749
  registerDiscordListener(params.client.listeners, new DiscordReactionListener(reactionListenerOptions));
10666
9750
  registerDiscordListener(params.client.listeners, new DiscordReactionRemoveListener(reactionListenerOptions));
10667
9751
  }
10668
- registerDiscordListener(params.client.listeners, new DiscordThreadUpdateListener(params.cfg, params.accountId, params.logger));
9752
+ registerDiscordListener(params.client.listeners, new DiscordThreadUpdateListener(params.cfg, params.logger));
9753
+ registerDiscordListener(params.client.listeners, new DiscordThreadDeleteListener(params.cfg, params.accountId, params.logger));
10669
9754
  if (params.discordConfig.intents?.presence) {
10670
9755
  const presenceListener = new DiscordPresenceListener({
10671
9756
  cfg: params.cfg,
@@ -10866,8 +9951,31 @@ async function monitorDiscordProvider(opts = {}) {
10866
9951
  phase: "fetch-application-id:start",
10867
9952
  startAt: startupStartedAt
10868
9953
  });
10869
- const applicationId = (typeof discordCfg.applicationId === "string" && discordCfg.applicationId.trim() ? discordCfg.applicationId.trim() : void 0) ?? parseApplicationIdFromToken(token) ?? await discordProviderRuntime.fetchDiscordApplicationId(token, 4e3, discordRestFetch);
10870
- if (!applicationId) throw new Error("Failed to resolve Discord application id");
9954
+ const parsedApplicationId = (typeof discordCfg.applicationId === "string" && discordCfg.applicationId.trim() ? discordCfg.applicationId.trim() : void 0) ?? parseApplicationIdFromToken(token);
9955
+ const applicationIdProbe = parsedApplicationId ? {
9956
+ kind: "resolved",
9957
+ applicationId: parsedApplicationId
9958
+ } : await discordProviderRuntime.probeDiscordApplicationId(token, 4e3, discordRestFetch);
9959
+ if (applicationIdProbe.kind !== "resolved") {
9960
+ const at = Date.now();
9961
+ const terminal = applicationIdProbe.kind === "rejected";
9962
+ const probeError = formatErrorMessage$1(applicationIdProbe.error);
9963
+ const message = `Failed to resolve Discord application id: ${probeError}`;
9964
+ opts.setStatus?.({
9965
+ connected: false,
9966
+ lifecycle: terminal ? "blocked" : "recovering",
9967
+ terminalDisconnect: terminal ? true : void 0,
9968
+ lastEventAt: at,
9969
+ lastDisconnect: {
9970
+ at,
9971
+ ...applicationIdProbe.status !== null ? { status: applicationIdProbe.status } : {},
9972
+ error: probeError
9973
+ },
9974
+ lastError: message
9975
+ });
9976
+ throw new Error(message, { cause: applicationIdProbe.error });
9977
+ }
9978
+ const applicationId = applicationIdProbe.applicationId;
10871
9979
  logDiscordStartupPhase({
10872
9980
  runtime,
10873
9981
  accountId: account.accountId,
@@ -10880,9 +9988,9 @@ async function monitorDiscordProvider(opts = {}) {
10880
9988
  runtime,
10881
9989
  nativeEnabled,
10882
9990
  nativeSkillsEnabled,
9991
+ voiceEnabled,
10883
9992
  listSkillCommandsForAgents: discordProviderRuntime.listSkillCommandsForAgents,
10884
- listNativeCommandSpecsForConfig: discordProviderRuntime.listNativeCommandSpecsForConfig,
10885
- getPluginCommandSpecs: discordProviderRuntime.getPluginCommandSpecs
9993
+ listNativeCommandSpecsForConfig: discordProviderRuntime.listNativeCommandSpecsForConfig
10886
9994
  });
10887
9995
  const voiceManagerRef = { current: null };
10888
9996
  const threadBindings = threadBindingsEnabled ? discordProviderSessionRuntime.createThreadBindingManager({
@@ -10921,6 +10029,7 @@ async function monitorDiscordProvider(opts = {}) {
10921
10029
  let earlyGatewayEmitter = gatewaySupervisor?.emitter;
10922
10030
  let onEarlyGatewayDebug;
10923
10031
  try {
10032
+ const pluginChannelRuntime = opts.channelRuntime;
10924
10033
  const { commands, components, modals } = createDiscordProviderInteractionSurface({
10925
10034
  cfg,
10926
10035
  discordConfig: discordCfg,
@@ -10940,7 +10049,7 @@ async function monitorDiscordProvider(opts = {}) {
10940
10049
  allowFrom,
10941
10050
  dmPolicy,
10942
10051
  runtime,
10943
- channelRuntime: opts.channelRuntime,
10052
+ channelRuntime: pluginChannelRuntime,
10944
10053
  abortSignal: opts.abortSignal,
10945
10054
  createNativeCommand: discordProviderRuntime.createDiscordNativeCommand
10946
10055
  });
@@ -11014,7 +10123,7 @@ async function monitorDiscordProvider(opts = {}) {
11014
10123
  runtime,
11015
10124
  botUserId
11016
10125
  });
11017
- const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-DIKGsTGT.js").then((n) => n.n);
10126
+ const { setDiscordTranscriptsVoiceManager } = await import("./transcripts-source-DeIMNbRh.js").then((n) => n.n);
11018
10127
  setDiscordTranscriptsVoiceManager({
11019
10128
  accountId: account.accountId,
11020
10129
  manager: voiceManager
@@ -11032,6 +10141,7 @@ async function monitorDiscordProvider(opts = {}) {
11032
10141
  accountId: account.accountId,
11033
10142
  token,
11034
10143
  runtime,
10144
+ buildContext: pluginChannelRuntime?.inbound.buildContext,
11035
10145
  setStatus: opts.setStatus,
11036
10146
  abortSignal: opts.abortSignal,
11037
10147
  botUserId,
@@ -11087,7 +10197,7 @@ async function monitorDiscordProvider(opts = {}) {
11087
10197
  gatewayReady: lifecycleGateway?.isConnected === true,
11088
10198
  botIdentity: botIdentity || void 0
11089
10199
  }));
11090
- if (lifecycleGateway?.isConnected) opts.setStatus?.(createConnectedChannelStatusPatch());
10200
+ if (lifecycleGateway?.isConnected) opts.setStatus?.(createDiscordReadyStatusPatch());
11091
10201
  lifecycleStarted = true;
11092
10202
  earlyGatewayEmitter?.removeListener("debug", onEarlyGatewayDebug);
11093
10203
  onEarlyGatewayDebug = void 0;
@@ -11119,4 +10229,4 @@ async function monitorDiscordProvider(opts = {}) {
11119
10229
  }
11120
10230
  }
11121
10231
  //#endregion
11122
- export { sanitizeDiscordThreadName as A, handleDiscordDmCommandDecision as C, resolveDiscordAutoThreadReplyPlan as D, threading_exports as E, resolveDiscordTextCommandAccess as M, resolveDiscordReplyTarget as O, shouldIgnoreStaleDiscordRouteBinding as S, registerDiscordListener as T, buildDiscordInboundAccessContext as _, sanitizeDiscordFrontChannelReplyPayloads as a, resolveDiscordConversationRoute as b, resolveReplyContext as c, createDiscordGatewayPlugin as d, resolveDiscordGatewayIntents as f, buildDiscordGroupSystemPrompt as g, createDiscordNativeCommand as h, formatDiscordReplySkip as i, resolveDiscordDmCommandAccess as j, resolveDiscordThreadStarter as k, resolveDiscordVoiceAccess as l, resolveDiscordVoiceEnabled as m, deliverDiscordReply as n, buildDirectLabel as o, waitForDiscordGatewayPluginRegistration as p, formatDiscordReplyDeliveryFailure as r, buildGuildLabel as s, monitorDiscordProvider as t, authorizeDiscordVoiceIngress as u, createDiscordSupplementalContextAccessChecker as v, mapGatewayDispatchData as w, resolveDiscordEffectiveRoute as x, buildDiscordRoutePeer as y };
10232
+ export { mapGatewayDispatchData as _, sanitizeDiscordFrontChannelReplyPayloads as a, resolveDiscordDmCommandAccess as b, resolveReplyContext as c, waitForDiscordGatewayPluginRegistration as d, createDiscordNativeCommand as f, handleDiscordDmCommandDecision as g, createDiscordSupplementalContextAccessChecker as h, formatDiscordReplySkip as i, createDiscordGatewayPlugin as l, buildDiscordInboundAccessContext as m, deliverDiscordReply as n, buildDirectLabel as o, buildDiscordGroupSystemPrompt as p, formatDiscordReplyDeliveryFailure as r, buildGuildLabel as s, monitorDiscordProvider as t, resolveDiscordGatewayIntents as u, registerDiscordListener as v, resolveDiscordTextCommandAccess as x, resolveFetchedDiscordThreadLikeChannelContext as y };