@kodelyth/discord 2026.5.39 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/account-inspect-Dqw-enky.js +81 -0
  2. package/dist/account-inspect-api.js +10 -0
  3. package/dist/accounts-B7OBFePq.js +224 -0
  4. package/dist/action-runtime-api.js +2 -0
  5. package/dist/agent-components.runtime-DVY_1VB4.js +4 -0
  6. package/dist/allow-list-B0s7evD7.js +354 -0
  7. package/dist/api-CXAcv9nZ.js +130 -0
  8. package/dist/api.js +23 -0
  9. package/dist/approval-handler.runtime-B9xUAF3n.js +426 -0
  10. package/dist/audit-DoiK49WO.js +24 -0
  11. package/dist/audit-core-BGrq3G7r.js +105 -0
  12. package/dist/channel-U_aeoFwW.js +795 -0
  13. package/dist/channel-actions-BxEBnEuv.js +173 -0
  14. package/dist/channel-actions.runtime-CPtpH-yl.js +263 -0
  15. package/dist/channel-api-BfjklLby.js +21 -0
  16. package/dist/channel-config-api.js +2 -0
  17. package/dist/channel-plugin-api.js +2 -0
  18. package/dist/channel.setup-BUSC0apv.js +337 -0
  19. package/dist/components-luonoe13.js +909 -0
  20. package/dist/config-api-DSYGqaLQ.js +2 -0
  21. package/dist/config-schema-DIqJBGwC.js +357 -0
  22. package/dist/configured-state.js +6 -0
  23. package/dist/contract-api.js +8 -0
  24. package/dist/conversation-identity-DXAm0_Mk.js +270 -0
  25. package/dist/directory-config-CYbuMmPS.js +49 -0
  26. package/dist/directory-contract-api.js +2 -0
  27. package/dist/directory-live-DX4dLRpJ.js +159 -0
  28. package/dist/doctor-bbKSvGVD.js +244 -0
  29. package/dist/doctor-contract-Btjt6NJD.js +383 -0
  30. package/dist/doctor-contract-api.js +2 -0
  31. package/dist/gateway-registry-BKSpa4GB.js +74 -0
  32. package/dist/handle-action.guild-admin-B5BArS2n.js +286 -0
  33. package/dist/inbound-context-WAOqhGlT.js +48 -0
  34. package/dist/inbound-event-delivery-C-1Ji3WP.js +65 -0
  35. package/dist/index.js +26 -0
  36. package/dist/manager.runtime-DXHynKE4.js +2356 -0
  37. package/dist/message-handler-mXzc3tA_.js +381 -0
  38. package/dist/message-handler.preflight-BPD1a347.js +1113 -0
  39. package/dist/message-handler.process-GUa3aV8z.js +1438 -0
  40. package/dist/message-utils-dUbem16p.js +549 -0
  41. package/dist/outbound-adapter-C18OAc1y.js +536 -0
  42. package/dist/pluralkit-D1Q2x0w5.js +22 -0
  43. package/dist/preflight-audio-CZtpWcIm.js +72 -0
  44. package/dist/preflight-audio.runtime-Brx_0_xW.js +7 -0
  45. package/dist/preview-streaming-D_slNIiO.js +8 -0
  46. package/dist/probe-D--Ca4JF.js +139 -0
  47. package/dist/probe.runtime-DQBchZzv.js +2 -0
  48. package/dist/provider-B2-31CIT.js +9565 -0
  49. package/dist/provider-session.runtime-BwzzSsrH.js +6 -0
  50. package/dist/provider.runtime-CP3oHLls.js +2 -0
  51. package/dist/resolve-allowlist-common-CqxPLcJO.js +34 -0
  52. package/dist/resolve-channels-0LX4pUbB.js +265 -0
  53. package/dist/resolve-users-CztOv0Qs.js +120 -0
  54. package/dist/runtime-DUaw66V_.js +1073 -0
  55. package/dist/runtime-api.actions.js +3 -0
  56. package/dist/runtime-api.js +30 -0
  57. package/dist/runtime-api.lookup.js +7 -0
  58. package/dist/runtime-api.monitor-CvVKvEXW.js +5 -0
  59. package/dist/runtime-api.monitor.js +8 -0
  60. package/dist/runtime-api.send.js +6 -0
  61. package/dist/runtime-api.threads.js +6 -0
  62. package/dist/runtime-fC6f4UF2.js +8 -0
  63. package/dist/runtime-setter-api.js +2 -0
  64. package/dist/secret-config-contract-B6WW5V88.js +115 -0
  65. package/dist/secret-contract-api.js +2 -0
  66. package/dist/security-audit-CnyIQKz6.js +120 -0
  67. package/dist/security-audit-contract-api.js +2 -0
  68. package/dist/security-audit.runtime-CQSkjNLu.js +2 -0
  69. package/dist/security-contract-DLvYOgLM.js +26 -0
  70. package/dist/security-contract-api.js +2 -0
  71. package/dist/security-doctor-DepqtNCI.js +18 -0
  72. package/dist/send-DCtPCHGk.js +881 -0
  73. package/dist/send.components-Bcgxvm52.js +474 -0
  74. package/dist/send.outbound-S9t0UuHc.js +330 -0
  75. package/dist/send.receipt-CDn3GBWC.js +3119 -0
  76. package/dist/send.shared-D4iBnAmn.js +669 -0
  77. package/dist/sender-identity-CxCe3_1a.js +43 -0
  78. package/dist/session-contract-Dwhw3RTY.js +6 -0
  79. package/dist/session-key-api.js +2 -0
  80. package/dist/session-key-normalization-CP8dPUid.js +23 -0
  81. package/dist/setup-entry.js +11 -0
  82. package/dist/setup-plugin-api.js +2 -0
  83. package/dist/shared-AIlvuZXt.js +171 -0
  84. package/dist/subagent-hooks-8bK-mgiU.js +120 -0
  85. package/dist/subagent-hooks-api.js +22 -0
  86. package/dist/system-events-Ba1TklaL.js +34 -0
  87. package/dist/target-resolver-BrtFQtoK.js +82 -0
  88. package/dist/targets-DWLLZE2l.js +3 -0
  89. package/dist/test-api.js +45 -0
  90. package/dist/thread-binding-api.js +4 -0
  91. package/dist/thread-bindings-9aKRmZv0.js +255 -0
  92. package/dist/thread-bindings.discord-api-ssGH5wc2.js +244 -0
  93. package/dist/thread-bindings.manager-0YBHGemk.js +534 -0
  94. package/dist/thread-bindings.session-updates-DJZGIwaU.js +54 -0
  95. package/dist/thread-bindings.state-eTFl-PqJ.js +318 -0
  96. package/dist/timeouts-CEwuGaWT.js +52 -0
  97. package/dist/timeouts.js +2 -0
  98. package/dist/typing-BmJKRpCS.js +14 -0
  99. package/package.json +19 -7
  100. package/account-inspect-api.js +0 -7
  101. package/action-runtime-api.js +0 -7
  102. package/api.js +0 -7
  103. package/channel-config-api.js +0 -7
  104. package/channel-plugin-api.js +0 -7
  105. package/configured-state.js +0 -7
  106. package/contract-api.js +0 -7
  107. package/directory-contract-api.js +0 -7
  108. package/doctor-contract-api.js +0 -7
  109. package/index.js +0 -7
  110. package/runtime-api.actions.js +0 -7
  111. package/runtime-api.js +0 -7
  112. package/runtime-api.lookup.js +0 -7
  113. package/runtime-api.monitor.js +0 -7
  114. package/runtime-api.send.js +0 -7
  115. package/runtime-api.threads.js +0 -7
  116. package/runtime-setter-api.js +0 -7
  117. package/secret-contract-api.js +0 -7
  118. package/security-audit-contract-api.js +0 -7
  119. package/security-contract-api.js +0 -7
  120. package/session-key-api.js +0 -7
  121. package/setup-entry.js +0 -7
  122. package/setup-plugin-api.js +0 -7
  123. package/subagent-hooks-api.js +0 -7
  124. package/test-api.js +0 -7
  125. package/thread-binding-api.js +0 -7
  126. package/timeouts.js +0 -7
@@ -0,0 +1,1438 @@
1
+ import { Ut as resolveDiscordChannelId, c as discord_exports, ft as editChannelMessage, s as chunkDiscordTextWithMode, st as createChannelMessage, ut as deleteChannelMessage } from "./send.receipt-CDn3GBWC.js";
2
+ import { f as resolveDiscordMaxLinesPerMessage } from "./accounts-B7OBFePq.js";
3
+ import { N as createDiscordRestClient, P as createDiscordRuntimeAccountContext, _ as resolveDiscordMessageFlags, d as resolveDiscordTargetChannelId } from "./send.shared-D4iBnAmn.js";
4
+ import { S as resolveTimestampMs, a as normalizeDiscordSlug, r as normalizeDiscordAllowList } from "./allow-list-B0s7evD7.js";
5
+ import { a as removeReactionDiscord, f as editMessageDiscord, r as reactMessageDiscord } from "./send-DCtPCHGk.js";
6
+ import "./targets-DWLLZE2l.js";
7
+ import { t as resolveDiscordConversationIdentity } from "./conversation-identity-DXAm0_Mk.js";
8
+ import { t as beginDiscordInboundEventDeliveryCorrelation } from "./inbound-event-delivery-C-1Ji3WP.js";
9
+ import { t as DISCORD_TEXT_CHUNK_LIMIT } from "./outbound-adapter-C18OAc1y.js";
10
+ import { t as resolveDiscordPreviewStreamMode } from "./preview-streaming-D_slNIiO.js";
11
+ import { n as DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS, t as DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS } from "./timeouts-CEwuGaWT.js";
12
+ import { a as resolveReplyContext, i as buildGuildLabel, n as deliverDiscordReply, r as buildDirectLabel, x as resolveDiscordThreadStarter, y as resolveDiscordAutoThreadReplyPlan } from "./provider-B2-31CIT.js";
13
+ import { a as resolveForwardedMediaList, o as resolveMediaList, r as resolveDiscordMessageText, s as resolveReferencedReplyMediaList } from "./message-utils-dUbem16p.js";
14
+ import { t as sendTyping } from "./typing-BmJKRpCS.js";
15
+ import { n as buildDiscordInboundAccessContext, r as createDiscordSupplementalContextAccessChecker } from "./inbound-context-WAOqhGlT.js";
16
+ import { buildAgentSessionKey, normalizeAccountId, resolveAccountEntry, resolveThreadSessionKeys } from "klaw/plugin-sdk/routing";
17
+ import { MessageFlags } from "discord-api-types/v10";
18
+ import path from "node:path";
19
+ import { evaluateSupplementalContextVisibility } from "klaw/plugin-sdk/security-runtime";
20
+ import { getAgentScopedMediaLocalRoots } from "klaw/plugin-sdk/media-runtime";
21
+ import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts } from "klaw/plugin-sdk/reply-payload";
22
+ import { resolveChunkMode, resolveTextChunkLimit } from "klaw/plugin-sdk/reply-chunking";
23
+ import { danger, logVerbose, shouldLogVerbose } from "klaw/plugin-sdk/runtime-env";
24
+ import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
25
+ import { resolveMarkdownTableMode } from "klaw/plugin-sdk/markdown-table-runtime";
26
+ import { convertMarkdownTables, stripInlineDirectiveTagsForDelivery, stripReasoningTagsFromText } from "klaw/plugin-sdk/text-chunking";
27
+ import { createChannelMessageReplyPipeline, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, resolveChannelMessageSourceReplyDeliveryMode } from "klaw/plugin-sdk/channel-message";
28
+ import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelProgressDraftGate, formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, mergeChannelProgressDraftLine, normalizeChannelProgressDraftLineIdentity, resolveChannelProgressDraftMaxLines, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewChunk, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages, resolveTranscriptBackedChannelFinalText } from "klaw/plugin-sdk/channel-streaming";
29
+ import { recordInboundSession, resolvePinnedMainDmOwnerFromAllowlist } from "klaw/plugin-sdk/conversation-runtime";
30
+ import { EmbeddedBlockChunker, formatReasoningMessage, resolveAckReaction, resolveHumanDelayConfig } from "klaw/plugin-sdk/agent-runtime";
31
+ import { truncateUtf16Safe } from "klaw/plugin-sdk/text-utility-runtime";
32
+ import { isDangerousNameMatchingEnabled } from "klaw/plugin-sdk/dangerous-name-runtime";
33
+ import { loadSessionStore, readLatestAssistantTextFromSessionTranscript, readSessionUpdatedAt, resolveAndPersistSessionFile, resolveSessionStoreEntry, resolveStorePath } from "klaw/plugin-sdk/session-store-runtime";
34
+ import { buildChannelInboundEventContext, formatInboundEnvelope, resolveEnvelopeFormatOptions, toHistoryMediaEntries, toInboundMediaFacts } from "klaw/plugin-sdk/channel-inbound";
35
+ import { createFinalizableDraftLifecycle } from "klaw/plugin-sdk/channel-lifecycle";
36
+ import { hasFinalInboundReplyDispatch, recordChannelBotPairLoopAndCheckSuppression, runPreparedInboundReplyTurn } from "klaw/plugin-sdk/inbound-reply-dispatch";
37
+ import { DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, shouldAckReaction } from "klaw/plugin-sdk/channel-feedback";
38
+ import { createChannelHistoryWindow } from "klaw/plugin-sdk/reply-history";
39
+ import { resolveChannelContextVisibilityMode } from "klaw/plugin-sdk/context-visibility-runtime";
40
+ //#region extensions/discord/src/monitor/ack-reactions.ts
41
+ function createDiscordAckReactionContext(params) {
42
+ return {
43
+ rest: params.rest,
44
+ ...createDiscordRuntimeAccountContext({
45
+ cfg: params.cfg,
46
+ accountId: params.accountId
47
+ })
48
+ };
49
+ }
50
+ function createDiscordAckReactionAdapter(params) {
51
+ return {
52
+ setReaction: async (emoji) => {
53
+ await reactMessageDiscord(params.channelId, params.messageId, emoji, params.reactionContext);
54
+ },
55
+ removeReaction: async (emoji) => {
56
+ await removeReactionDiscord(params.channelId, params.messageId, emoji, params.reactionContext);
57
+ }
58
+ };
59
+ }
60
+ function queueInitialDiscordAckReaction(params) {
61
+ if (params.enabled) {
62
+ params.statusReactions.setQueued();
63
+ return;
64
+ }
65
+ if (!params.shouldSendAckReaction || !params.ackReaction) return;
66
+ params.reactionAdapter.setReaction(params.ackReaction).catch((err) => {
67
+ logAckFailure({
68
+ log: logVerbose,
69
+ channel: "discord",
70
+ target: params.target,
71
+ error: err
72
+ });
73
+ });
74
+ }
75
+ //#endregion
76
+ //#region extensions/discord/src/monitor/message-handler.context.ts
77
+ function normalizeDiscordDmOwnerEntry(entry) {
78
+ const candidate = normalizeDiscordAllowList([entry], [
79
+ "discord:",
80
+ "user:",
81
+ "pk:"
82
+ ])?.ids.values().next().value;
83
+ return typeof candidate === "string" && /^\d+$/.test(candidate) ? candidate : void 0;
84
+ }
85
+ function isContextAborted(abortSignal) {
86
+ return Boolean(abortSignal?.aborted);
87
+ }
88
+ async function buildDiscordMessageProcessContext(params) {
89
+ const { ctx, text, mediaList } = params;
90
+ const { cfg, discordConfig, accountId, runtime, mediaMaxBytes, discordRestFetch, abortSignal, guildHistories, historyLimit, replyToMode, message, author, sender, canonicalMessageId, data, client, channelInfo, channelName, messageChannelId, isGuildMessage, isDirectMessage, baseText, preflightAudioTranscript, threadChannel, threadParentId, threadParentName, threadParentType, threadName, displayChannelSlug, guildInfo, guildSlug, memberRoleIds, channelConfig, baseSessionKey, boundSessionKey, route, commandAuthorized } = ctx;
91
+ const fromLabel = isDirectMessage ? buildDirectLabel(author) : buildGuildLabel({
92
+ guild: data.guild ?? void 0,
93
+ channelName: channelName ?? messageChannelId,
94
+ channelId: messageChannelId
95
+ });
96
+ const senderLabel = sender.label;
97
+ const isForumParent = threadParentType === discord_exports.ChannelType.GuildForum || threadParentType === discord_exports.ChannelType.GuildMedia;
98
+ const forumParentSlug = isForumParent && threadParentName ? normalizeDiscordSlug(threadParentName) : "";
99
+ const threadChannelId = threadChannel?.id;
100
+ const threadParentInheritanceEnabled = discordConfig?.thread?.inheritParent ?? false;
101
+ const forumContextLine = Boolean(threadChannelId && isForumParent && forumParentSlug) && message.id === threadChannelId ? `[Forum parent: #${forumParentSlug}]` : null;
102
+ const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : void 0;
103
+ const groupSubject = isDirectMessage ? void 0 : groupChannel;
104
+ const senderName = sender.isPluralKit ? sender.name ?? author.username : data.member?.nickname ?? author.globalName ?? author.username;
105
+ const senderUsername = sender.isPluralKit ? sender.tag ?? sender.name ?? author.username : author.username;
106
+ const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
107
+ channelConfig,
108
+ guildInfo,
109
+ sender: {
110
+ id: sender.id,
111
+ name: sender.name,
112
+ tag: sender.tag
113
+ },
114
+ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
115
+ isGuild: isGuildMessage,
116
+ channelTopic: channelInfo?.topic
117
+ });
118
+ const pinnedMainDmOwner = isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({
119
+ dmScope: cfg.session?.dmScope,
120
+ allowFrom: channelConfig?.users ?? guildInfo?.users,
121
+ normalizeEntry: normalizeDiscordDmOwnerEntry
122
+ }) : null;
123
+ const contextVisibilityMode = resolveChannelContextVisibilityMode({
124
+ cfg,
125
+ channel: "discord",
126
+ accountId
127
+ });
128
+ const isSupplementalContextSenderAllowed = createDiscordSupplementalContextAccessChecker({
129
+ channelConfig,
130
+ guildInfo,
131
+ allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
132
+ isGuild: isGuildMessage
133
+ });
134
+ const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
135
+ const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
136
+ const previousTimestamp = readSessionUpdatedAt({
137
+ storePath,
138
+ sessionKey: route.sessionKey
139
+ });
140
+ const channelHistory = createChannelHistoryWindow({ historyMap: guildHistories });
141
+ const isRoomEvent = ctx.inboundEventKind === "room_event";
142
+ let combinedBody = formatInboundEnvelope({
143
+ channel: "Discord",
144
+ from: fromLabel,
145
+ timestamp: resolveTimestampMs(message.timestamp),
146
+ body: text,
147
+ chatType: isDirectMessage ? "direct" : "channel",
148
+ senderLabel,
149
+ previousTimestamp,
150
+ envelope: envelopeOptions
151
+ });
152
+ const shouldIncludeChannelHistory = !isDirectMessage && (isRoomEvent || !(isGuildMessage && channelConfig?.autoThread && !threadChannel));
153
+ if (shouldIncludeChannelHistory) combinedBody = channelHistory.buildPendingContext({
154
+ historyKey: messageChannelId,
155
+ limit: historyLimit,
156
+ currentMessage: combinedBody,
157
+ formatEntry: (entry) => formatInboundEnvelope({
158
+ channel: "Discord",
159
+ from: fromLabel,
160
+ timestamp: entry.timestamp,
161
+ body: `${entry.body} [id:${entry.messageId ?? "unknown"} channel:${messageChannelId}]`,
162
+ chatType: "channel",
163
+ senderLabel: entry.sender,
164
+ envelope: envelopeOptions
165
+ })
166
+ });
167
+ const replyContext = resolveReplyContext(message, resolveDiscordMessageText);
168
+ const replyVisibility = replyContext ? evaluateSupplementalContextVisibility({
169
+ mode: contextVisibilityMode,
170
+ kind: "quote",
171
+ senderAllowed: isSupplementalContextSenderAllowed({
172
+ id: replyContext.senderId,
173
+ name: replyContext.senderName,
174
+ tag: replyContext.senderTag,
175
+ memberRoleIds: replyContext.memberRoleIds
176
+ })
177
+ }) : null;
178
+ const filteredReplyContext = replyContext && replyVisibility?.include ? replyContext : null;
179
+ if (replyContext && !filteredReplyContext && isGuildMessage) logVerbose(`discord: drop reply context (mode=${contextVisibilityMode})`);
180
+ const mediaListForContext = [...mediaList];
181
+ if (filteredReplyContext) {
182
+ const referencedReplyMediaList = await resolveReferencedReplyMediaList(message, mediaMaxBytes, {
183
+ fetchImpl: discordRestFetch,
184
+ ssrfPolicy: cfg.browser?.ssrfPolicy,
185
+ readIdleTimeoutMs: DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
186
+ totalTimeoutMs: DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
187
+ abortSignal
188
+ });
189
+ if (!isContextAborted(abortSignal)) mediaListForContext.push(...referencedReplyMediaList);
190
+ }
191
+ if (forumContextLine) combinedBody = `${combinedBody}\n${forumContextLine}`;
192
+ let threadStarterBody;
193
+ let threadLabel;
194
+ let parentSessionKey;
195
+ let modelParentSessionKey;
196
+ if (threadChannel) {
197
+ if (channelConfig?.includeThreadStarter !== false) {
198
+ const starter = await resolveDiscordThreadStarter({
199
+ channel: threadChannel,
200
+ client,
201
+ parentId: threadParentId,
202
+ parentType: threadParentType,
203
+ resolveTimestampMs
204
+ });
205
+ if (starter?.text) if (evaluateSupplementalContextVisibility({
206
+ mode: contextVisibilityMode,
207
+ kind: "thread",
208
+ senderAllowed: isSupplementalContextSenderAllowed({
209
+ id: starter.authorId,
210
+ name: starter.authorName ?? starter.author,
211
+ tag: starter.authorTag,
212
+ memberRoleIds: starter.memberRoleIds
213
+ })
214
+ }).include) threadStarterBody = starter.text;
215
+ else logVerbose(`discord: drop thread starter context (mode=${contextVisibilityMode})`);
216
+ }
217
+ const parentName = threadParentName ?? "parent";
218
+ threadLabel = threadName ? `Discord thread #${normalizeDiscordSlug(parentName)} › ${threadName}` : `Discord thread #${normalizeDiscordSlug(parentName)}`;
219
+ if (threadParentId) {
220
+ parentSessionKey = buildAgentSessionKey({
221
+ agentId: route.agentId,
222
+ channel: route.channel,
223
+ peer: {
224
+ kind: "channel",
225
+ id: threadParentId
226
+ }
227
+ });
228
+ modelParentSessionKey = parentSessionKey;
229
+ }
230
+ if (!threadParentInheritanceEnabled) parentSessionKey = void 0;
231
+ }
232
+ const preflightAudioIndex = preflightAudioTranscript === void 0 ? -1 : mediaListForContext.findIndex((media) => media.contentType?.startsWith("audio/"));
233
+ const threadKeys = resolveThreadSessionKeys({
234
+ baseSessionKey,
235
+ threadId: threadChannel ? messageChannelId : void 0,
236
+ parentSessionKey,
237
+ useSuffix: false
238
+ });
239
+ const replyPlan = await resolveDiscordAutoThreadReplyPlan({
240
+ client,
241
+ message,
242
+ messageChannelId,
243
+ isGuildMessage,
244
+ channelConfig: isRoomEvent ? null : channelConfig,
245
+ threadChannel,
246
+ channelType: channelInfo?.type,
247
+ channelName: channelInfo?.name,
248
+ channelDescription: channelInfo?.topic,
249
+ baseText: baseText ?? "",
250
+ combinedBody,
251
+ replyToMode,
252
+ agentId: route.agentId,
253
+ channel: route.channel,
254
+ cfg,
255
+ threadParentInheritanceEnabled
256
+ });
257
+ const deliverTarget = replyPlan.deliverTarget;
258
+ const replyTarget = replyPlan.replyTarget;
259
+ const replyReference = replyPlan.replyReference;
260
+ const autoThreadContext = replyPlan.autoThreadContext;
261
+ const effectiveFrom = isDirectMessage ? `discord:${author.id}` : autoThreadContext?.From ?? `discord:channel:${messageChannelId}`;
262
+ const dmConversationTarget = isDirectMessage ? resolveDiscordConversationIdentity({
263
+ isDirectMessage,
264
+ userId: author.id
265
+ }) : void 0;
266
+ const effectiveTo = autoThreadContext?.To ?? dmConversationTarget ?? replyTarget;
267
+ if (!effectiveTo) {
268
+ runtime.error?.(danger("discord: missing reply target"));
269
+ return null;
270
+ }
271
+ const lastRouteTo = dmConversationTarget ?? effectiveTo;
272
+ const inboundHistory = shouldIncludeChannelHistory ? channelHistory.buildInboundHistory({
273
+ historyKey: messageChannelId,
274
+ limit: historyLimit
275
+ }) : void 0;
276
+ const originatingTo = autoThreadContext?.OriginatingTo ?? dmConversationTarget ?? replyTarget;
277
+ const effectiveSessionKey = boundSessionKey ?? autoThreadContext?.SessionKey ?? threadKeys.sessionKey;
278
+ const effectivePreviousTimestamp = effectiveSessionKey === route.sessionKey ? previousTimestamp : readSessionUpdatedAt({
279
+ storePath,
280
+ sessionKey: effectiveSessionKey
281
+ });
282
+ const ctxPayload = buildChannelInboundEventContext({
283
+ channel: "discord",
284
+ provider: "discord",
285
+ surface: "discord",
286
+ accountId: route.accountId,
287
+ messageId: canonicalMessageId ?? message.id,
288
+ messageIdFull: canonicalMessageId && canonicalMessageId !== message.id ? message.id : void 0,
289
+ timestamp: resolveTimestampMs(message.timestamp),
290
+ from: effectiveFrom,
291
+ sender: {
292
+ id: sender.id,
293
+ name: senderName,
294
+ username: senderUsername,
295
+ tag: sender.tag,
296
+ roles: memberRoleIds,
297
+ displayLabel: senderLabel
298
+ },
299
+ conversation: {
300
+ kind: isDirectMessage ? "direct" : "channel",
301
+ id: messageChannelId,
302
+ label: fromLabel,
303
+ spaceId: isGuildMessage ? (guildInfo?.id ?? guildSlug) || void 0 : void 0,
304
+ threadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? void 0,
305
+ routePeer: {
306
+ kind: isDirectMessage ? "direct" : "channel",
307
+ id: isDirectMessage ? author.id : messageChannelId
308
+ }
309
+ },
310
+ route: {
311
+ agentId: route.agentId,
312
+ accountId: route.accountId,
313
+ routeSessionKey: route.sessionKey,
314
+ dispatchSessionKey: effectiveSessionKey,
315
+ parentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
316
+ modelParentSessionKey: autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? void 0
317
+ },
318
+ reply: {
319
+ to: effectiveTo,
320
+ originatingTo
321
+ },
322
+ message: {
323
+ inboundEventKind: ctx.inboundEventKind,
324
+ body: combinedBody,
325
+ rawBody: preflightAudioTranscript ?? baseText,
326
+ bodyForAgent: preflightAudioTranscript ?? baseText ?? text,
327
+ commandBody: preflightAudioTranscript ?? baseText,
328
+ envelopeFrom: fromLabel,
329
+ inboundHistory
330
+ },
331
+ access: {
332
+ mentions: {
333
+ canDetectMention: ctx.canDetectMention,
334
+ wasMentioned: ctx.effectiveWasMentioned,
335
+ hasAnyMention: ctx.hasAnyMention,
336
+ requireMention: ctx.shouldRequireMention,
337
+ effectiveWasMentioned: ctx.effectiveWasMentioned
338
+ },
339
+ commands: {
340
+ authorized: commandAuthorized,
341
+ allowTextCommands: ctx.allowTextCommands,
342
+ useAccessGroups: false,
343
+ authorizers: []
344
+ }
345
+ },
346
+ commandTurn: {
347
+ kind: "text-slash",
348
+ source: "text",
349
+ authorized: commandAuthorized,
350
+ body: preflightAudioTranscript ?? baseText
351
+ },
352
+ media: toInboundMediaFacts(mediaListForContext, { transcribed: (_media, index) => index === preflightAudioIndex }),
353
+ supplemental: {
354
+ quote: filteredReplyContext ? {
355
+ id: filteredReplyContext.id,
356
+ body: filteredReplyContext.body,
357
+ sender: filteredReplyContext.sender
358
+ } : void 0,
359
+ thread: {
360
+ starterBody: !effectivePreviousTimestamp ? threadStarterBody : void 0,
361
+ label: threadLabel
362
+ },
363
+ groupSystemPrompt: isGuildMessage ? groupSystemPrompt : void 0
364
+ },
365
+ extra: {
366
+ ...preflightAudioTranscript !== void 0 ? { Transcript: preflightAudioTranscript } : {},
367
+ GroupSubject: groupSubject,
368
+ GroupChannel: groupChannel,
369
+ UntrustedStructuredContext: untrustedContext,
370
+ OwnerAllowFrom: ownerAllowFrom
371
+ }
372
+ });
373
+ const persistedSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
374
+ if (isRoomEvent && shouldIncludeChannelHistory) await channelHistory.recordWithMedia({
375
+ historyKey: messageChannelId,
376
+ limit: historyLimit,
377
+ entry: {
378
+ sender: senderName,
379
+ body: text,
380
+ timestamp: resolveTimestampMs(message.timestamp),
381
+ messageId: message.id
382
+ },
383
+ media: toHistoryMediaEntries(mediaList, { messageId: message.id }),
384
+ messageId: message.id
385
+ });
386
+ if (shouldLogVerbose()) {
387
+ const preview = truncateUtf16Safe(combinedBody, 200).replace(/\n/g, "\\n");
388
+ logVerbose(`discord inbound: channel=${messageChannelId} deliver=${deliverTarget} from=${ctxPayload.From} preview="${preview}"`);
389
+ }
390
+ return {
391
+ ctxPayload,
392
+ persistedSessionKey,
393
+ turn: {
394
+ storePath,
395
+ record: {
396
+ updateLastRoute: {
397
+ sessionKey: persistedSessionKey,
398
+ channel: "discord",
399
+ to: lastRouteTo,
400
+ accountId: route.accountId,
401
+ mainDmOwnerPin: isDirectMessage && persistedSessionKey === route.mainSessionKey && pinnedMainDmOwner ? {
402
+ ownerRecipient: pinnedMainDmOwner,
403
+ senderRecipient: author.id,
404
+ onSkip: ({ ownerRecipient, senderRecipient }) => {
405
+ logVerbose(`discord: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
406
+ }
407
+ } : void 0
408
+ },
409
+ onRecordError: (err) => {
410
+ logVerbose(`discord: failed updating session meta: ${String(err)}`);
411
+ }
412
+ }
413
+ },
414
+ replyPlan,
415
+ deliverTarget,
416
+ replyTarget,
417
+ replyReference
418
+ };
419
+ }
420
+ //#endregion
421
+ //#region extensions/discord/src/draft-chunking.ts
422
+ const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200;
423
+ const DEFAULT_DISCORD_DRAFT_STREAM_MAX = 800;
424
+ function resolveDiscordDraftStreamingChunking(cfg, accountId) {
425
+ const textLimit = resolveTextChunkLimit(cfg, "discord", accountId, { fallbackLimit: DISCORD_TEXT_CHUNK_LIMIT });
426
+ const normalizedAccountId = normalizeAccountId(accountId);
427
+ const draftCfg = resolveChannelStreamingPreviewChunk(resolveAccountEntry(cfg?.channels?.discord?.accounts, normalizedAccountId)) ?? resolveChannelStreamingPreviewChunk(cfg?.channels?.discord);
428
+ const maxRequested = Math.max(1, Math.floor(draftCfg?.maxChars ?? DEFAULT_DISCORD_DRAFT_STREAM_MAX));
429
+ const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
430
+ const minRequested = Math.max(1, Math.floor(draftCfg?.minChars ?? DEFAULT_DISCORD_DRAFT_STREAM_MIN));
431
+ return {
432
+ minChars: Math.min(minRequested, maxChars),
433
+ maxChars,
434
+ breakPreference: draftCfg?.breakPreference === "newline" || draftCfg?.breakPreference === "sentence" ? draftCfg.breakPreference : "paragraph"
435
+ };
436
+ }
437
+ //#endregion
438
+ //#region extensions/discord/src/draft-stream.ts
439
+ /** Discord messages cap at 2000 characters. */
440
+ const DISCORD_STREAM_MAX_CHARS = 2e3;
441
+ const DEFAULT_THROTTLE_MS = 1200;
442
+ const DISCORD_PREVIEW_ALLOWED_MENTIONS = { parse: [] };
443
+ function createDiscordDraftStream(params) {
444
+ const maxChars = Math.min(params.maxChars ?? DISCORD_STREAM_MAX_CHARS, DISCORD_STREAM_MAX_CHARS);
445
+ const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
446
+ const minInitialChars = params.minInitialChars;
447
+ const channelId = params.channelId;
448
+ const rest = params.rest;
449
+ const flags = resolveDiscordMessageFlags({ suppressEmbeds: params.suppressEmbeds });
450
+ const resolveReplyToMessageId = () => typeof params.replyToMessageId === "function" ? params.replyToMessageId() : params.replyToMessageId;
451
+ const streamState = {
452
+ stopped: false,
453
+ final: false
454
+ };
455
+ let streamMessageId;
456
+ let lastSentText = "";
457
+ const sendOrEditStreamMessage = async (text) => {
458
+ if (streamState.stopped && !streamState.final) return false;
459
+ const trimmed = text.trimEnd();
460
+ if (!trimmed) return false;
461
+ if (trimmed.length > maxChars) {
462
+ streamState.stopped = true;
463
+ params.warn?.(`discord stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
464
+ return false;
465
+ }
466
+ if (trimmed === lastSentText) return true;
467
+ if (streamMessageId === void 0 && minInitialChars != null && !streamState.final) {
468
+ if (trimmed.length < minInitialChars) return false;
469
+ }
470
+ lastSentText = trimmed;
471
+ try {
472
+ if (streamMessageId !== void 0) {
473
+ await editChannelMessage(rest, channelId, streamMessageId, { body: {
474
+ content: trimmed,
475
+ allowed_mentions: DISCORD_PREVIEW_ALLOWED_MENTIONS,
476
+ ...flags ? { flags } : {}
477
+ } });
478
+ return true;
479
+ }
480
+ const replyToMessageId = resolveReplyToMessageId()?.trim();
481
+ const messageReference = replyToMessageId ? {
482
+ message_id: replyToMessageId,
483
+ fail_if_not_exists: false
484
+ } : void 0;
485
+ const sentMessageId = (await createChannelMessage(rest, channelId, { body: {
486
+ content: trimmed,
487
+ allowed_mentions: DISCORD_PREVIEW_ALLOWED_MENTIONS,
488
+ ...flags ? { flags } : {},
489
+ ...messageReference ? { message_reference: messageReference } : {}
490
+ } }))?.id;
491
+ if (typeof sentMessageId !== "string" || !sentMessageId) {
492
+ streamState.stopped = true;
493
+ params.warn?.("discord stream preview stopped (missing message id from send)");
494
+ return false;
495
+ }
496
+ streamMessageId = sentMessageId;
497
+ return true;
498
+ } catch (err) {
499
+ streamState.stopped = true;
500
+ params.warn?.(`discord stream preview failed: ${formatErrorMessage(err)}`);
501
+ return false;
502
+ }
503
+ };
504
+ const readMessageId = () => streamMessageId;
505
+ const clearMessageId = () => {
506
+ streamMessageId = void 0;
507
+ };
508
+ const isValidStreamMessageId = (value) => typeof value === "string";
509
+ const deleteStreamMessage = async (messageId) => {
510
+ await deleteChannelMessage(rest, channelId, messageId);
511
+ };
512
+ const { loop, update, stop, clear, discardPending, seal } = createFinalizableDraftLifecycle({
513
+ throttleMs,
514
+ state: streamState,
515
+ sendOrEditStreamMessage,
516
+ readMessageId,
517
+ clearMessageId,
518
+ isValidMessageId: isValidStreamMessageId,
519
+ deleteMessage: deleteStreamMessage,
520
+ warn: params.warn,
521
+ warnPrefix: "discord stream preview cleanup failed"
522
+ });
523
+ const forceNewMessage = () => {
524
+ streamMessageId = void 0;
525
+ lastSentText = "";
526
+ loop.resetPending();
527
+ };
528
+ params.log?.(`discord stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
529
+ return {
530
+ update,
531
+ flush: loop.flush,
532
+ messageId: () => streamMessageId,
533
+ clear,
534
+ discardPending,
535
+ seal,
536
+ stop,
537
+ forceNewMessage
538
+ };
539
+ }
540
+ //#endregion
541
+ //#region extensions/discord/src/monitor/message-handler.draft-preview.ts
542
+ function createDiscordDraftPreviewController(params) {
543
+ const discordStreamMode = resolveDiscordPreviewStreamMode(params.discordConfig);
544
+ const draftMaxChars = Math.min(params.textLimit, 2e3);
545
+ const accountBlockStreamingEnabled = resolveChannelStreamingBlockEnabled(params.discordConfig) ?? params.cfg.agents?.defaults?.blockStreamingDefault === "on";
546
+ const canStreamProgressDraftForToolOnlySource = params.sourceRepliesAreToolOnly && discordStreamMode === "progress";
547
+ const draftStream = (!params.sourceRepliesAreToolOnly || canStreamProgressDraftForToolOnlySource) && discordStreamMode !== "off" && !accountBlockStreamingEnabled ? createDiscordDraftStream({
548
+ rest: params.deliveryRest,
549
+ channelId: params.deliverChannelId,
550
+ maxChars: draftMaxChars,
551
+ replyToMessageId: () => params.replyReference.peek(),
552
+ minInitialChars: discordStreamMode === "progress" ? 0 : 30,
553
+ suppressEmbeds: params.discordConfig?.suppressEmbeds ?? true,
554
+ throttleMs: 1200,
555
+ log: params.log,
556
+ warn: params.log
557
+ }) : void 0;
558
+ const draftChunking = draftStream && discordStreamMode === "block" ? resolveDiscordDraftStreamingChunking(params.cfg, params.accountId) : void 0;
559
+ const shouldSplitPreviewMessages = discordStreamMode === "block";
560
+ const draftChunker = draftChunking ? new EmbeddedBlockChunker(draftChunking) : void 0;
561
+ let lastPartialText = "";
562
+ let draftText = "";
563
+ let hasStreamedMessage = false;
564
+ let finalizedViaPreviewMessage = false;
565
+ let finalReplyDelivered = false;
566
+ const previewToolProgressEnabled = Boolean(draftStream) && resolveChannelStreamingPreviewToolProgress(params.discordConfig);
567
+ const suppressDefaultToolProgressMessages = Boolean(draftStream) && resolveChannelStreamingSuppressDefaultToolProgressMessages(params.discordConfig, {
568
+ draftStreamActive: true,
569
+ previewToolProgressEnabled
570
+ });
571
+ let previewToolProgressSuppressed = false;
572
+ let previewToolProgressLines = [];
573
+ let reasoningProgressRawText = "";
574
+ let lastReasoningProgressLine;
575
+ const progressSeed = `${params.accountId}:${params.deliverChannelId}`;
576
+ const renderProgressDraft = async (options) => {
577
+ if (!draftStream || discordStreamMode !== "progress") return;
578
+ const previewText = formatChannelProgressDraftText({
579
+ entry: params.discordConfig,
580
+ lines: previewToolProgressLines,
581
+ seed: progressSeed
582
+ });
583
+ if (!previewText || previewText === lastPartialText) return;
584
+ lastPartialText = previewText;
585
+ draftText = previewText;
586
+ hasStreamedMessage = true;
587
+ draftChunker?.reset();
588
+ draftStream.update(previewText);
589
+ if (options?.flush) await draftStream.flush();
590
+ };
591
+ const progressDraftGate = createChannelProgressDraftGate({ onStart: () => renderProgressDraft({ flush: true }) });
592
+ const resetProgressState = () => {
593
+ lastPartialText = "";
594
+ draftText = "";
595
+ draftChunker?.reset();
596
+ previewToolProgressSuppressed = false;
597
+ previewToolProgressLines = [];
598
+ reasoningProgressRawText = "";
599
+ lastReasoningProgressLine = void 0;
600
+ };
601
+ const forceNewMessageIfNeeded = () => {
602
+ if (shouldSplitPreviewMessages && hasStreamedMessage) {
603
+ params.log("discord: calling forceNewMessage() for draft stream");
604
+ draftStream?.forceNewMessage();
605
+ }
606
+ resetProgressState();
607
+ };
608
+ return {
609
+ draftStream,
610
+ previewToolProgressEnabled,
611
+ suppressDefaultToolProgressMessages,
612
+ get isProgressMode() {
613
+ return discordStreamMode === "progress";
614
+ },
615
+ get hasProgressDraftStarted() {
616
+ return progressDraftGate.hasStarted;
617
+ },
618
+ get finalizedViaPreviewMessage() {
619
+ return finalizedViaPreviewMessage;
620
+ },
621
+ markFinalReplyDelivered() {
622
+ finalReplyDelivered = true;
623
+ },
624
+ markPreviewFinalized() {
625
+ finalizedViaPreviewMessage = true;
626
+ },
627
+ disableBlockStreamingForDraft: draftStream ? true : void 0,
628
+ async startProgressDraft() {
629
+ if (!draftStream || discordStreamMode !== "progress") return;
630
+ await progressDraftGate.startNow();
631
+ },
632
+ async pushToolProgress(line, options) {
633
+ if (!draftStream) return;
634
+ if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
635
+ if (isEmptyDiscordProgressLine(line)) return;
636
+ const normalized = normalizeChannelProgressDraftLineIdentity(line);
637
+ if (!normalized) return;
638
+ const progressLine = typeof line === "object" && line !== void 0 ? line : normalized;
639
+ if (discordStreamMode !== "progress") {
640
+ if (!previewToolProgressEnabled || previewToolProgressSuppressed) return;
641
+ const nextLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(params.discordConfig) });
642
+ if (nextLines === previewToolProgressLines) return;
643
+ previewToolProgressLines = nextLines;
644
+ const previewText = formatChannelProgressDraftText({
645
+ entry: params.discordConfig,
646
+ lines: previewToolProgressLines,
647
+ seed: progressSeed
648
+ });
649
+ lastPartialText = previewText;
650
+ draftText = previewText;
651
+ hasStreamedMessage = true;
652
+ draftChunker?.reset();
653
+ draftStream.update(previewText);
654
+ return;
655
+ }
656
+ if (previewToolProgressEnabled && !previewToolProgressSuppressed && normalized) previewToolProgressLines = mergeChannelProgressDraftLine(previewToolProgressLines, progressLine, { maxLines: resolveChannelProgressDraftMaxLines(params.discordConfig) });
657
+ const alreadyStarted = progressDraftGate.hasStarted;
658
+ if (shouldStartDiscordProgressDraftNow(line)) await progressDraftGate.startNow();
659
+ else await progressDraftGate.noteWork();
660
+ if (alreadyStarted && progressDraftGate.hasStarted) await renderProgressDraft();
661
+ },
662
+ async pushReasoningProgress(text) {
663
+ if (!draftStream || discordStreamMode !== "progress" || !text) return;
664
+ reasoningProgressRawText = mergeReasoningProgressText(reasoningProgressRawText, text);
665
+ const normalized = normalizeReasoningProgressLine(reasoningProgressRawText);
666
+ if (!normalized) return;
667
+ if (previewToolProgressEnabled && !previewToolProgressSuppressed) {
668
+ const priorIndex = lastReasoningProgressLine === void 0 ? -1 : previewToolProgressLines.lastIndexOf(lastReasoningProgressLine);
669
+ if (priorIndex >= 0) {
670
+ previewToolProgressLines = [...previewToolProgressLines];
671
+ previewToolProgressLines[priorIndex] = normalized;
672
+ } else previewToolProgressLines = [...previewToolProgressLines, normalized].slice(-resolveChannelProgressDraftMaxLines(params.discordConfig));
673
+ lastReasoningProgressLine = normalized;
674
+ }
675
+ const alreadyStarted = progressDraftGate.hasStarted;
676
+ await progressDraftGate.noteWork();
677
+ if (alreadyStarted && progressDraftGate.hasStarted) await renderProgressDraft();
678
+ },
679
+ resolvePreviewFinalText(text) {
680
+ if (typeof text !== "string") return;
681
+ const formatted = convertMarkdownTables(stripInlineDirectiveTagsForDelivery(text).text, params.tableMode);
682
+ const chunks = chunkDiscordTextWithMode(formatted, {
683
+ maxChars: draftMaxChars,
684
+ maxLines: params.maxLinesPerMessage,
685
+ chunkMode: params.chunkMode
686
+ });
687
+ if (!chunks.length && formatted) chunks.push(formatted);
688
+ if (chunks.length !== 1) return;
689
+ const trimmed = chunks[0].trim();
690
+ if (!trimmed) return;
691
+ const currentPreviewText = discordStreamMode === "block" ? draftText : lastPartialText;
692
+ if (currentPreviewText && currentPreviewText.startsWith(trimmed) && trimmed.length < currentPreviewText.length) return;
693
+ return trimmed;
694
+ },
695
+ updateFromPartial(text) {
696
+ if (!draftStream || !text) return;
697
+ const cleaned = stripInlineDirectiveTagsForDelivery(stripReasoningTagsFromText(text, {
698
+ mode: "strict",
699
+ trim: "both"
700
+ })).text;
701
+ if (!cleaned || cleaned.startsWith("Reasoning:\n")) return;
702
+ if (cleaned === lastPartialText) return;
703
+ if (discordStreamMode === "progress") return;
704
+ previewToolProgressSuppressed = true;
705
+ previewToolProgressLines = [];
706
+ hasStreamedMessage = true;
707
+ if (discordStreamMode === "partial") {
708
+ if (lastPartialText && lastPartialText.startsWith(cleaned) && cleaned.length < lastPartialText.length) return;
709
+ lastPartialText = cleaned;
710
+ draftStream.update(cleaned);
711
+ return;
712
+ }
713
+ let delta = cleaned;
714
+ if (cleaned.startsWith(lastPartialText)) delta = cleaned.slice(lastPartialText.length);
715
+ else {
716
+ draftChunker?.reset();
717
+ draftText = "";
718
+ }
719
+ lastPartialText = cleaned;
720
+ if (!delta) return;
721
+ if (!draftChunker) {
722
+ draftText = cleaned;
723
+ draftStream.update(draftText);
724
+ return;
725
+ }
726
+ draftChunker.append(delta);
727
+ draftChunker.drain({
728
+ force: false,
729
+ emit: (chunk) => {
730
+ draftText += chunk;
731
+ draftStream.update(draftText);
732
+ }
733
+ });
734
+ },
735
+ handleAssistantMessageBoundary() {
736
+ if (discordStreamMode === "progress") return;
737
+ forceNewMessageIfNeeded();
738
+ },
739
+ async flush() {
740
+ if (!draftStream) return;
741
+ if (draftChunker?.hasBuffered()) {
742
+ draftChunker.drain({
743
+ force: true,
744
+ emit: (chunk) => {
745
+ draftText += chunk;
746
+ }
747
+ });
748
+ draftChunker.reset();
749
+ if (draftText) draftStream.update(draftText);
750
+ }
751
+ await draftStream.flush();
752
+ },
753
+ async cleanup() {
754
+ try {
755
+ progressDraftGate.cancel();
756
+ if (!finalReplyDelivered) await draftStream?.discardPending();
757
+ if (!finalReplyDelivered && !finalizedViaPreviewMessage && draftStream?.messageId()) await draftStream.clear();
758
+ } catch (err) {
759
+ params.log(`discord: draft cleanup failed: ${String(err)}`);
760
+ }
761
+ }
762
+ };
763
+ }
764
+ function normalizeReasoningProgressLine(text) {
765
+ return text.replace(/^\s*(?:>\s*)?(?:Reasoning:|Thinking\.{0,3})\s*/i, "").replace(/\s+/g, " ").trim();
766
+ }
767
+ function mergeReasoningProgressText(current, incoming) {
768
+ if (!current) return incoming;
769
+ const normalizedCurrent = normalizeReasoningProgressLine(current);
770
+ const normalizedIncoming = normalizeReasoningProgressLine(incoming);
771
+ if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) return current;
772
+ if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) return incoming;
773
+ return `${current}${incoming}`;
774
+ }
775
+ function isReasoningSnapshotText(text) {
776
+ return /^\s*(?:>\s*)?(?:Reasoning:|Thinking\.{0,3})\s*/i.test(text);
777
+ }
778
+ function isEmptyDiscordProgressLine(line) {
779
+ if (!line || typeof line === "string") return false;
780
+ return line.toolName === "apply_patch" && !line.detail && !line.status;
781
+ }
782
+ function shouldStartDiscordProgressDraftNow(line) {
783
+ return typeof line === "object" && line?.kind === "patch" && Boolean(line.detail);
784
+ }
785
+ //#endregion
786
+ //#region extensions/discord/src/monitor/message-handler.process.ts
787
+ function sleep(ms) {
788
+ return new Promise((resolve) => {
789
+ setTimeout(resolve, ms);
790
+ });
791
+ }
792
+ const DISCORD_TYPING_MAX_DURATION_MS = 20 * 6e4;
793
+ let replyRuntimePromise;
794
+ async function loadReplyRuntime() {
795
+ replyRuntimePromise ??= import("klaw/plugin-sdk/reply-runtime");
796
+ return await replyRuntimePromise;
797
+ }
798
+ function isProcessAborted(abortSignal) {
799
+ return Boolean(abortSignal?.aborted);
800
+ }
801
+ function formatDiscordReplyDeliveryFailure(params) {
802
+ const context = [`target=${params.target}`, params.sessionKey ? `session=${params.sessionKey}` : void 0].filter(Boolean).join(" ");
803
+ return `discord ${params.kind} reply failed (${context}): ${String(params.err)}`;
804
+ }
805
+ function readToolStringArg(args, key) {
806
+ const value = args[key];
807
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
808
+ }
809
+ function readToolBooleanArg(args, key) {
810
+ return args[key] === true;
811
+ }
812
+ async function processDiscordMessage(ctx, observer) {
813
+ const dispatchStartedAt = Date.now();
814
+ const { cfg, discordConfig, accountId, token, runtime, guildHistories, historyLimit, mediaMaxBytes, textLimit, replyToMode, ackReactionScope, message, messageChannelId, isGuildMessage, isDirectMessage, isGroupDm, messageText, shouldRequireMention, canDetectMention, effectiveWasMentioned, shouldBypassMention, channelConfig, threadBindings, route, discordRestFetch, abortSignal, botLoopProtection } = ctx;
815
+ if (isProcessAborted(abortSignal)) return;
816
+ if (botLoopProtection) {
817
+ const botLoopResult = recordChannelBotPairLoopAndCheckSuppression(botLoopProtection);
818
+ if (botLoopResult.suppressed) {
819
+ logVerbose(`discord: bot-to-bot loop detected before dispatch setup, suppressing for ${Math.max(0, Math.ceil((botLoopResult.cooldownUntilMs - Date.now()) / 1e3))}s`);
820
+ return;
821
+ }
822
+ }
823
+ const mediaResolveOptions = {
824
+ fetchImpl: discordRestFetch,
825
+ ssrfPolicy: cfg.browser?.ssrfPolicy,
826
+ readIdleTimeoutMs: DISCORD_ATTACHMENT_IDLE_TIMEOUT_MS,
827
+ totalTimeoutMs: DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS,
828
+ abortSignal
829
+ };
830
+ const mediaList = await resolveMediaList(message, mediaMaxBytes, mediaResolveOptions);
831
+ if (isProcessAborted(abortSignal)) return;
832
+ const forwardedMediaList = await resolveForwardedMediaList(message, mediaMaxBytes, mediaResolveOptions);
833
+ if (isProcessAborted(abortSignal)) return;
834
+ mediaList.push(...forwardedMediaList);
835
+ const text = messageText;
836
+ if (!text) {
837
+ logVerbose("discord: drop message " + message.id + " (empty content)");
838
+ return;
839
+ }
840
+ const boundThreadId = ctx.threadBinding?.conversation?.conversationId?.trim();
841
+ if (boundThreadId && typeof threadBindings.touchThread === "function") threadBindings.touchThread({ threadId: boundThreadId });
842
+ const { createReplyDispatcherWithTyping, dispatchInboundMessage, settleReplyDispatcher } = await loadReplyRuntime();
843
+ const sourceReplyDeliveryMode = resolveChannelMessageSourceReplyDeliveryMode({
844
+ cfg,
845
+ ctx: {
846
+ ChatType: isDirectMessage ? "direct" : isGroupDm ? "group" : isGuildMessage ? "channel" : void 0,
847
+ InboundEventKind: ctx.inboundEventKind
848
+ }
849
+ });
850
+ const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
851
+ const ackReaction = resolveAckReaction(cfg, route.agentId, {
852
+ channel: "discord",
853
+ accountId
854
+ });
855
+ const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
856
+ const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
857
+ const isRoomEvent = ctx.inboundEventKind === "room_event";
858
+ const shouldAckReaction$1 = () => Boolean(!isRoomEvent && ackReaction && shouldAckReaction({
859
+ scope: ackReactionScope,
860
+ isDirect: isDirectMessage,
861
+ isGroup: isGuildMessage || isGroupDm,
862
+ isMentionableGroup: isGuildMessage,
863
+ requireMention: shouldRequireMention,
864
+ canDetectMention,
865
+ effectiveWasMentioned,
866
+ shouldBypassMention
867
+ }));
868
+ const shouldSendAckReaction = shouldAckReaction$1();
869
+ const statusReactionsExplicitlyEnabled = cfg.messages?.statusReactions?.enabled === true;
870
+ const statusReactionsEnabled = !isRoomEvent && shouldSendAckReaction && cfg.messages?.statusReactions?.enabled !== false && (!sourceRepliesAreToolOnly || statusReactionsExplicitlyEnabled);
871
+ const feedbackRest = createDiscordRestClient({
872
+ cfg,
873
+ token,
874
+ accountId
875
+ }).rest;
876
+ const deliveryRest = createDiscordRestClient({
877
+ cfg,
878
+ token,
879
+ accountId
880
+ }).rest;
881
+ const ackReactionContext = createDiscordAckReactionContext({
882
+ rest: feedbackRest,
883
+ cfg,
884
+ accountId
885
+ });
886
+ const discordAdapter = createDiscordAckReactionAdapter({
887
+ channelId: messageChannelId,
888
+ messageId: message.id,
889
+ reactionContext: ackReactionContext
890
+ });
891
+ let statusReactionTarget = `${messageChannelId}/${message.id}`;
892
+ let statusReactionsActive = statusReactionsEnabled;
893
+ let statusReactions = createStatusReactionController({
894
+ enabled: statusReactionsEnabled,
895
+ adapter: discordAdapter,
896
+ initialEmoji: ackReaction,
897
+ emojis: cfg.messages?.statusReactions?.emojis,
898
+ timing: cfg.messages?.statusReactions?.timing,
899
+ onError: (err) => {
900
+ logAckFailure({
901
+ log: logVerbose,
902
+ channel: "discord",
903
+ target: statusReactionTarget,
904
+ error: err
905
+ });
906
+ }
907
+ });
908
+ const resolveTrackedReactionChannelId = async (args) => {
909
+ const target = readToolStringArg(args, "channelId") ?? readToolStringArg(args, "channel_id") ?? readToolStringArg(args, "to");
910
+ if (!target) return messageChannelId;
911
+ try {
912
+ return resolveDiscordChannelId(target);
913
+ } catch {
914
+ return (await resolveDiscordTargetChannelId(target, {
915
+ cfg,
916
+ token,
917
+ accountId
918
+ })).channelId;
919
+ }
920
+ };
921
+ const maybeBindStatusReactionsToToolReaction = async (payload) => {
922
+ if (sourceRepliesAreToolOnly || cfg.messages?.statusReactions?.enabled === false || payload.phase !== "start" || payload.name !== "message" || !payload.args) return;
923
+ const args = payload.args;
924
+ if (readToolStringArg(args, "action")?.toLowerCase() !== "react") return;
925
+ if (!(readToolBooleanArg(args, "trackToolCalls") || readToolBooleanArg(args, "track_tool_calls"))) return;
926
+ const emoji = readToolStringArg(args, "emoji");
927
+ const remove = readToolBooleanArg(args, "remove");
928
+ if (!emoji || remove) return;
929
+ const trackedMessageId = readToolStringArg(args, "messageId") ?? readToolStringArg(args, "message_id") ?? message.id;
930
+ let trackedChannelId;
931
+ try {
932
+ trackedChannelId = await resolveTrackedReactionChannelId(args);
933
+ } catch (err) {
934
+ logAckFailure({
935
+ log: logVerbose,
936
+ channel: "discord",
937
+ target: `${readToolStringArg(args, "to") ?? readToolStringArg(args, "channelId") ?? messageChannelId}/${trackedMessageId}`,
938
+ error: err
939
+ });
940
+ return;
941
+ }
942
+ statusReactionTarget = `${trackedChannelId}/${trackedMessageId}`;
943
+ if (statusReactionsActive) statusReactions.clear();
944
+ statusReactions = createStatusReactionController({
945
+ enabled: true,
946
+ adapter: createDiscordAckReactionAdapter({
947
+ channelId: trackedChannelId,
948
+ messageId: trackedMessageId,
949
+ reactionContext: ackReactionContext
950
+ }),
951
+ initialEmoji: emoji,
952
+ emojis: cfg.messages?.statusReactions?.emojis,
953
+ timing: cfg.messages?.statusReactions?.timing,
954
+ onError: (err) => {
955
+ logAckFailure({
956
+ log: logVerbose,
957
+ channel: "discord",
958
+ target: statusReactionTarget,
959
+ error: err
960
+ });
961
+ }
962
+ });
963
+ statusReactionsActive = true;
964
+ statusReactions.setQueued();
965
+ };
966
+ queueInitialDiscordAckReaction({
967
+ enabled: statusReactionsEnabled,
968
+ shouldSendAckReaction,
969
+ ackReaction,
970
+ statusReactions,
971
+ reactionAdapter: discordAdapter,
972
+ target: `${messageChannelId}/${message.id}`
973
+ });
974
+ const processContext = await buildDiscordMessageProcessContext({
975
+ ctx,
976
+ text,
977
+ mediaList
978
+ });
979
+ if (!processContext) return;
980
+ const { ctxPayload, persistedSessionKey, turn, replyPlan, deliverTarget, replyTarget, replyReference } = processContext;
981
+ observer?.onReplyPlanResolved?.({
982
+ createdThreadId: replyPlan.createdThreadId,
983
+ sessionKey: persistedSessionKey
984
+ });
985
+ const typingChannelId = deliverTarget.startsWith("channel:") ? deliverTarget.slice(8) : messageChannelId;
986
+ const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
987
+ cfg,
988
+ agentId: route.agentId,
989
+ channel: "discord",
990
+ accountId: route.accountId,
991
+ typing: {
992
+ start: () => sendTyping({
993
+ rest: feedbackRest,
994
+ channelId: typingChannelId
995
+ }),
996
+ onStartError: (err) => {
997
+ logTypingFailure({
998
+ log: logVerbose,
999
+ channel: "discord",
1000
+ target: typingChannelId,
1001
+ error: err
1002
+ });
1003
+ },
1004
+ maxDurationMs: DISCORD_TYPING_MAX_DURATION_MS
1005
+ }
1006
+ });
1007
+ const tableMode = resolveMarkdownTableMode({
1008
+ cfg,
1009
+ channel: "discord",
1010
+ accountId
1011
+ });
1012
+ const maxLinesPerMessage = resolveDiscordMaxLinesPerMessage({
1013
+ cfg,
1014
+ discordConfig,
1015
+ accountId
1016
+ });
1017
+ const chunkMode = resolveChunkMode(cfg, "discord", accountId);
1018
+ const clearGroupHistory = () => {
1019
+ if (isDirectMessage) return;
1020
+ createChannelHistoryWindow({ historyMap: guildHistories }).clear({
1021
+ historyKey: messageChannelId,
1022
+ limit: historyLimit
1023
+ });
1024
+ };
1025
+ const beginDeliveryCorrelation = () => isRoomEvent ? beginDiscordInboundEventDeliveryCorrelation(ctxPayload.SessionKey, {
1026
+ outboundTo: messageChannelId,
1027
+ outboundAccountId: route.accountId,
1028
+ markInboundEventDelivered: clearGroupHistory
1029
+ }, { inboundEventKind: ctxPayload.InboundEventKind }) : () => {};
1030
+ const endDiscordInboundEventDeliveryCorrelation = beginDeliveryCorrelation();
1031
+ const resolveCurrentTurnTranscriptFinalText = async () => {
1032
+ const sessionKey = ctxPayload.SessionKey;
1033
+ if (!sessionKey) return;
1034
+ try {
1035
+ const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
1036
+ const store = loadSessionStore(storePath, { clone: false });
1037
+ const sessionEntry = resolveSessionStoreEntry({
1038
+ store,
1039
+ sessionKey
1040
+ }).existing;
1041
+ if (!sessionEntry?.sessionId) return;
1042
+ const { sessionFile } = await resolveAndPersistSessionFile({
1043
+ sessionId: sessionEntry.sessionId,
1044
+ sessionKey,
1045
+ sessionStore: store,
1046
+ storePath,
1047
+ sessionEntry,
1048
+ agentId: route.agentId,
1049
+ sessionsDir: path.dirname(storePath)
1050
+ });
1051
+ const latest = await readLatestAssistantTextFromSessionTranscript(sessionFile);
1052
+ if (!latest?.timestamp || latest.timestamp < dispatchStartedAt) return;
1053
+ return latest.text;
1054
+ } catch (err) {
1055
+ logVerbose(`discord transcript final candidate lookup failed: ${String(err)}`);
1056
+ return;
1057
+ }
1058
+ };
1059
+ const deliverChannelId = deliverTarget.startsWith("channel:") ? deliverTarget.slice(8) : messageChannelId;
1060
+ const draftPreview = createDiscordDraftPreviewController({
1061
+ cfg,
1062
+ discordConfig,
1063
+ accountId,
1064
+ sourceRepliesAreToolOnly,
1065
+ textLimit,
1066
+ deliveryRest,
1067
+ deliverChannelId,
1068
+ replyReference,
1069
+ tableMode,
1070
+ maxLinesPerMessage,
1071
+ chunkMode,
1072
+ log: logVerbose
1073
+ });
1074
+ const finalPreviewFlags = discordConfig?.suppressEmbeds ?? true ? MessageFlags.SuppressEmbeds : void 0;
1075
+ let finalReplyStartNotified = false;
1076
+ const notifyFinalReplyStart = () => {
1077
+ if (finalReplyStartNotified) return;
1078
+ finalReplyStartNotified = true;
1079
+ observer?.onFinalReplyStart?.();
1080
+ };
1081
+ const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = createReplyDispatcherWithTyping({
1082
+ ...replyPipeline,
1083
+ humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
1084
+ deliver: async (payload, info) => {
1085
+ if (isProcessAborted(abortSignal)) return;
1086
+ const isFinal = info.kind === "final";
1087
+ if (payload.isReasoning) return;
1088
+ const finalText = isFinal && typeof payload.text === "string" ? await resolveTranscriptBackedChannelFinalText({
1089
+ finalText: payload.text,
1090
+ resolveCandidateText: resolveCurrentTurnTranscriptFinalText
1091
+ }) : payload.text;
1092
+ const effectivePayload = finalText !== payload.text ? {
1093
+ ...payload,
1094
+ text: finalText
1095
+ } : payload;
1096
+ const draftStream = draftPreview.draftStream;
1097
+ if (draftStream && draftPreview.isProgressMode && info.kind === "block") {
1098
+ if (!resolveSendableOutboundReplyParts(effectivePayload).hasMedia && !payload.isError) return;
1099
+ }
1100
+ if (draftStream && isFinal && (!draftPreview.isProgressMode || draftPreview.hasProgressDraftStarted)) {
1101
+ const hasMedia = resolveSendableOutboundReplyParts(effectivePayload).hasMedia;
1102
+ const ttsSupplement = getReplyPayloadTtsSupplement(effectivePayload);
1103
+ const previewSourceText = finalText ?? ttsSupplement?.spokenText;
1104
+ const previewFinalText = draftPreview.resolvePreviewFinalText(previewSourceText);
1105
+ const previewReplyToId = replyReference.peek();
1106
+ const hasExplicitReplyDirective = Boolean(effectivePayload.replyToTag || effectivePayload.replyToCurrent) || typeof previewSourceText === "string" && /\[\[\s*reply_to(?:_current|\s*:)/i.test(previewSourceText);
1107
+ if ((await deliverWithFinalizableLivePreviewAdapter({
1108
+ kind: info.kind,
1109
+ payload: effectivePayload,
1110
+ adapter: defineFinalizableLivePreviewAdapter({
1111
+ draft: {
1112
+ flush: () => draftPreview.flush(),
1113
+ clear: () => draftStream.clear(),
1114
+ discardPending: () => draftStream.discardPending(),
1115
+ seal: () => draftStream.seal(),
1116
+ id: draftStream.messageId
1117
+ },
1118
+ buildFinalEdit: () => {
1119
+ if (draftPreview.finalizedViaPreviewMessage || hasMedia && !ttsSupplement || typeof previewFinalText !== "string" || hasExplicitReplyDirective || payload.isError) return;
1120
+ return {
1121
+ content: previewFinalText,
1122
+ ...finalPreviewFlags ? { flags: finalPreviewFlags } : {}
1123
+ };
1124
+ },
1125
+ editFinal: async (previewMessageId, edit) => {
1126
+ if (isProcessAborted(abortSignal)) throw new Error("process aborted");
1127
+ notifyFinalReplyStart();
1128
+ await editMessageDiscord(deliverChannelId, previewMessageId, edit, {
1129
+ cfg,
1130
+ accountId,
1131
+ rest: deliveryRest
1132
+ });
1133
+ },
1134
+ onPreviewFinalized: () => {
1135
+ draftPreview.markFinalReplyDelivered();
1136
+ draftPreview.markPreviewFinalized();
1137
+ replyReference.markSent();
1138
+ observer?.onFinalReplyDelivered?.();
1139
+ },
1140
+ buildSupplementalPayload: () => ttsSupplement ? buildTtsSupplementMediaPayload(effectivePayload) : void 0,
1141
+ deliverSupplemental: async (supplementalPayload) => {
1142
+ if (isProcessAborted(abortSignal)) return false;
1143
+ const supplementalReplyToId = previewReplyToId ?? replyReference.peek() ?? (replyToMode === "all" ? typeof message.id === "string" && message.id ? message.id : ctxPayload.MessageSid : void 0);
1144
+ await deliverDiscordReply({
1145
+ cfg,
1146
+ replies: [supplementalPayload],
1147
+ target: deliverTarget,
1148
+ token,
1149
+ accountId,
1150
+ rest: deliveryRest,
1151
+ runtime,
1152
+ replyToId: supplementalReplyToId,
1153
+ replyToMode,
1154
+ textLimit,
1155
+ maxLinesPerMessage,
1156
+ tableMode,
1157
+ chunkMode,
1158
+ sessionKey: ctxPayload.SessionKey,
1159
+ threadBindings,
1160
+ mediaLocalRoots,
1161
+ kind: info.kind
1162
+ });
1163
+ return true;
1164
+ },
1165
+ logPreviewEditFailure: (err) => {
1166
+ logVerbose(`discord: preview final edit failed; falling back to standard send (${String(err)})`);
1167
+ }
1168
+ }),
1169
+ deliverNormally: async () => {
1170
+ if (isProcessAborted(abortSignal)) return false;
1171
+ const fallbackPayload = ttsSupplement && ttsSupplement.visibleTextAlreadyDelivered !== true && !effectivePayload.text?.trim() ? {
1172
+ ...effectivePayload,
1173
+ text: ttsSupplement.spokenText
1174
+ } : effectivePayload;
1175
+ const replyToId = replyReference.use();
1176
+ notifyFinalReplyStart();
1177
+ await deliverDiscordReply({
1178
+ cfg,
1179
+ replies: [fallbackPayload],
1180
+ target: deliverTarget,
1181
+ token,
1182
+ accountId,
1183
+ rest: deliveryRest,
1184
+ runtime,
1185
+ replyToId,
1186
+ replyToMode,
1187
+ textLimit,
1188
+ maxLinesPerMessage,
1189
+ tableMode,
1190
+ chunkMode,
1191
+ sessionKey: ctxPayload.SessionKey,
1192
+ threadBindings,
1193
+ mediaLocalRoots,
1194
+ kind: info.kind
1195
+ });
1196
+ return true;
1197
+ },
1198
+ onNormalDelivered: () => {
1199
+ draftPreview.markFinalReplyDelivered();
1200
+ replyReference.markSent();
1201
+ observer?.onFinalReplyDelivered?.();
1202
+ }
1203
+ })).kind !== "normal-skipped") return;
1204
+ }
1205
+ if (isProcessAborted(abortSignal)) return;
1206
+ const replyToId = replyReference.use();
1207
+ if (isFinal) notifyFinalReplyStart();
1208
+ await deliverDiscordReply({
1209
+ cfg,
1210
+ replies: [effectivePayload],
1211
+ target: deliverTarget,
1212
+ token,
1213
+ accountId,
1214
+ rest: deliveryRest,
1215
+ runtime,
1216
+ replyToId,
1217
+ replyToMode,
1218
+ textLimit,
1219
+ maxLinesPerMessage,
1220
+ tableMode,
1221
+ chunkMode,
1222
+ sessionKey: ctxPayload.SessionKey,
1223
+ threadBindings,
1224
+ mediaLocalRoots,
1225
+ kind: info.kind
1226
+ });
1227
+ replyReference.markSent();
1228
+ if (isFinal) observer?.onFinalReplyDelivered?.();
1229
+ },
1230
+ onError: (err, info) => {
1231
+ runtime.error?.(danger(formatDiscordReplyDeliveryFailure({
1232
+ kind: info.kind,
1233
+ err,
1234
+ target: deliverTarget,
1235
+ sessionKey: ctxPayload.SessionKey
1236
+ })));
1237
+ },
1238
+ onReplyStart: async () => {
1239
+ if (isProcessAborted(abortSignal)) return;
1240
+ await replyPipeline.typingCallbacks?.onReplyStart();
1241
+ await statusReactions.setThinking();
1242
+ }
1243
+ });
1244
+ const resolvedBlockStreamingEnabled = resolveChannelStreamingBlockEnabled(discordConfig);
1245
+ let dispatchResult = null;
1246
+ let dispatchError = false;
1247
+ let dispatchAborted = false;
1248
+ let dispatchSettledBeforeStart = false;
1249
+ const settleDispatchBeforeStart = async () => {
1250
+ dispatchSettledBeforeStart = true;
1251
+ await settleReplyDispatcher({
1252
+ dispatcher,
1253
+ onSettled: () => {
1254
+ markRunComplete();
1255
+ markDispatchIdle();
1256
+ }
1257
+ });
1258
+ };
1259
+ try {
1260
+ if (isProcessAborted(abortSignal)) {
1261
+ dispatchAborted = true;
1262
+ await settleDispatchBeforeStart();
1263
+ return;
1264
+ }
1265
+ const preparedResult = await runPreparedInboundReplyTurn({
1266
+ channel: "discord",
1267
+ accountId: route.accountId,
1268
+ routeSessionKey: persistedSessionKey,
1269
+ storePath: turn.storePath,
1270
+ ctxPayload,
1271
+ recordInboundSession,
1272
+ record: turn.record,
1273
+ history: isRoomEvent ? void 0 : {
1274
+ isGroup: isGuildMessage,
1275
+ historyKey: messageChannelId,
1276
+ historyMap: guildHistories,
1277
+ limit: historyLimit
1278
+ },
1279
+ onPreDispatchFailure: settleDispatchBeforeStart,
1280
+ runDispatch: async () => await dispatchInboundMessage({
1281
+ ctx: ctxPayload,
1282
+ cfg,
1283
+ dispatcher,
1284
+ replyOptions: {
1285
+ ...replyOptions,
1286
+ abortSignal,
1287
+ skillFilter: channelConfig?.skills,
1288
+ sourceReplyDeliveryMode,
1289
+ queuedDeliveryCorrelations: isRoomEvent ? [{ begin: beginDeliveryCorrelation }] : void 0,
1290
+ suppressTyping: isRoomEvent ? true : void 0,
1291
+ allowProgressCallbacksWhenSourceDeliverySuppressed: sourceRepliesAreToolOnly && draftPreview.draftStream && draftPreview.isProgressMode ? true : void 0,
1292
+ disableBlockStreaming: sourceRepliesAreToolOnly ? true : draftPreview.disableBlockStreamingForDraft ?? (typeof resolvedBlockStreamingEnabled === "boolean" ? !resolvedBlockStreamingEnabled : void 0),
1293
+ onPartialReply: draftPreview.draftStream && !draftPreview.isProgressMode ? (payload) => draftPreview.updateFromPartial(payload.text) : void 0,
1294
+ onAssistantMessageStart: draftPreview.draftStream ? () => draftPreview.handleAssistantMessageBoundary() : void 0,
1295
+ onReasoningEnd: draftPreview.draftStream ? () => draftPreview.handleAssistantMessageBoundary() : void 0,
1296
+ onModelSelected,
1297
+ suppressDefaultToolProgressMessages: draftPreview.suppressDefaultToolProgressMessages ? true : void 0,
1298
+ onReasoningStream: async (payload) => {
1299
+ await statusReactions.setThinking();
1300
+ const formattedText = payload?.text ? formatReasoningMessage(payload.text) : void 0;
1301
+ await draftPreview.pushReasoningProgress(formattedText);
1302
+ },
1303
+ onToolStart: async (payload) => {
1304
+ if (isProcessAborted(abortSignal)) return;
1305
+ await maybeBindStatusReactionsToToolReaction(payload);
1306
+ await statusReactions.setTool(payload.name);
1307
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLineForEntry(discordConfig, {
1308
+ event: "tool",
1309
+ name: payload.name,
1310
+ phase: payload.phase,
1311
+ args: payload.args
1312
+ }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName: payload.name });
1313
+ },
1314
+ onItemEvent: async (payload) => {
1315
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLineForEntry(discordConfig, {
1316
+ event: "item",
1317
+ itemId: payload.itemId,
1318
+ itemKind: payload.kind,
1319
+ title: payload.title,
1320
+ name: payload.name,
1321
+ phase: payload.phase,
1322
+ status: payload.status,
1323
+ summary: payload.summary,
1324
+ progressText: payload.progressText,
1325
+ meta: payload.meta
1326
+ }));
1327
+ },
1328
+ onPlanUpdate: async (payload) => {
1329
+ if (payload.phase !== "update") return;
1330
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLine({
1331
+ event: "plan",
1332
+ phase: payload.phase,
1333
+ title: payload.title,
1334
+ explanation: payload.explanation,
1335
+ steps: payload.steps
1336
+ }));
1337
+ },
1338
+ onApprovalEvent: async (payload) => {
1339
+ if (payload.phase !== "requested") return;
1340
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLine({
1341
+ event: "approval",
1342
+ phase: payload.phase,
1343
+ title: payload.title,
1344
+ command: payload.command,
1345
+ reason: payload.reason,
1346
+ message: payload.message
1347
+ }));
1348
+ },
1349
+ onCommandOutput: async (payload) => {
1350
+ if (payload.phase !== "end") return;
1351
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLine({
1352
+ event: "command-output",
1353
+ phase: payload.phase,
1354
+ title: payload.title,
1355
+ name: payload.name,
1356
+ status: payload.status,
1357
+ exitCode: payload.exitCode
1358
+ }));
1359
+ },
1360
+ onPatchSummary: async (payload) => {
1361
+ if (payload.phase !== "end") return;
1362
+ await draftPreview.pushToolProgress(buildChannelProgressDraftLine({
1363
+ event: "patch",
1364
+ phase: payload.phase,
1365
+ title: payload.title,
1366
+ name: payload.name,
1367
+ added: payload.added,
1368
+ modified: payload.modified,
1369
+ deleted: payload.deleted,
1370
+ summary: payload.summary
1371
+ }));
1372
+ },
1373
+ onCompactionStart: async () => {
1374
+ if (isProcessAborted(abortSignal)) return;
1375
+ await statusReactions.setCompacting();
1376
+ },
1377
+ onCompactionEnd: async () => {
1378
+ if (isProcessAborted(abortSignal)) return;
1379
+ statusReactions.cancelPending();
1380
+ await statusReactions.setThinking();
1381
+ }
1382
+ }
1383
+ })
1384
+ });
1385
+ if (!preparedResult.dispatched) return;
1386
+ dispatchResult = preparedResult.dispatchResult;
1387
+ if (isProcessAborted(abortSignal)) {
1388
+ dispatchAborted = true;
1389
+ return;
1390
+ }
1391
+ } catch (err) {
1392
+ if (isProcessAborted(abortSignal)) {
1393
+ dispatchAborted = true;
1394
+ return;
1395
+ }
1396
+ dispatchError = true;
1397
+ throw err;
1398
+ } finally {
1399
+ endDiscordInboundEventDeliveryCorrelation();
1400
+ try {
1401
+ await draftPreview.cleanup();
1402
+ } finally {
1403
+ if (!dispatchSettledBeforeStart) {
1404
+ markRunComplete();
1405
+ markDispatchIdle();
1406
+ }
1407
+ }
1408
+ const finalDeliveryFailed = (dispatchResult?.failedCounts?.final ?? 0) > 0;
1409
+ if (statusReactionsActive) if (dispatchAborted) if (removeAckAfterReply) statusReactions.clear();
1410
+ else statusReactions.restoreInitial();
1411
+ else {
1412
+ if (dispatchError || finalDeliveryFailed) await statusReactions.setError();
1413
+ else await statusReactions.setDone();
1414
+ if (removeAckAfterReply) (async () => {
1415
+ await sleep(dispatchError || finalDeliveryFailed ? DEFAULT_TIMING.errorHoldMs : DEFAULT_TIMING.doneHoldMs);
1416
+ await statusReactions.clear();
1417
+ })();
1418
+ else statusReactions.restoreInitial();
1419
+ }
1420
+ else if (shouldSendAckReaction && ackReaction && removeAckAfterReply) removeReactionDiscord(messageChannelId, message.id, ackReaction, ackReactionContext).catch((err) => {
1421
+ logAckFailure({
1422
+ log: logVerbose,
1423
+ channel: "discord",
1424
+ target: `${messageChannelId}/${message.id}`,
1425
+ error: err
1426
+ });
1427
+ });
1428
+ }
1429
+ if (dispatchAborted) return;
1430
+ const finalDispatchResult = dispatchResult;
1431
+ if (!finalDispatchResult || !hasFinalInboundReplyDispatch(finalDispatchResult)) return;
1432
+ if (shouldLogVerbose()) {
1433
+ const finalCount = finalDispatchResult.counts.final;
1434
+ logVerbose(`discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`);
1435
+ }
1436
+ }
1437
+ //#endregion
1438
+ export { processDiscordMessage };