@openclaw/slack 2026.5.27 → 2026.5.28-beta.2

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 (94) hide show
  1. package/dist/{account-inspect-vVg3pT03.js → account-inspect-CdGk6R7l.js} +1 -1
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/{accounts-BnLQ3fe2.js → accounts-f6Xcv9Vi.js} +42 -1
  4. package/dist/accounts.runtime-BJt1IxOS.js +2 -0
  5. package/dist/{action-runtime-JVb7KyQs.js → action-runtime-BOEgcnv6.js} +7 -8
  6. package/dist/action-runtime.runtime-BXQYV0yA.js +2 -0
  7. package/dist/{actions-1o9nMIY8.js → actions-zfVWcIY6.js} +4 -4
  8. package/dist/{actions.runtime-qQtdNww-.js → actions.runtime-CoijPN8g.js} +1 -1
  9. package/dist/api.js +14 -17
  10. package/dist/{approval-handler.runtime-Ba8nwnYi.js → approval-handler.runtime-CWz3XLfN.js} +12 -69
  11. package/dist/{channel-Bd7eept6.js → channel-D8p_1twn.js} +25 -28
  12. package/dist/channel-config-api.js +1 -1
  13. package/dist/channel-plugin-api.js +1 -1
  14. package/dist/{channel.setup-WNNVmCm0.js → channel.setup-oGp4gSTP.js} +4 -4
  15. package/dist/{client-Dc2Ji2YN.js → client-DowBk5k0.js} +6 -24
  16. package/dist/{config-schema-CUiDK8HV.js → config-schema-C0RewpJQ.js} +4 -0
  17. package/dist/contract-api.js +1 -1
  18. package/dist/{directory-config-9_djLTOK.js → directory-config-8UPAEyNg.js} +1 -1
  19. package/dist/directory-contract-api.js +1 -1
  20. package/dist/{directory-live-Cl83cGLZ.js → directory-live-BFB1pSax.js} +2 -2
  21. package/dist/http-routes-api.js +1 -1
  22. package/dist/index.js +3 -7
  23. package/dist/{interactive-replies-DtYu4Sf5.js → interactive-replies-DrBq4Mld.js} +1 -1
  24. package/dist/interactive-replies-api.js +1 -1
  25. package/dist/{message-tool-api-BieXX5lk.js → message-tool-api-B9M0zzlQ.js} +2 -2
  26. package/dist/message-tool-api.js +1 -1
  27. package/dist/{monitor-Cn7LUDFL.js → monitor-D7jGKmQk.js} +3 -4
  28. package/dist/{outbound-adapter-ChuR4_0v.js → outbound-adapter-BHZMgblN.js} +4 -7
  29. package/dist/pipeline.runtime-xM6ppqQZ.js +3492 -0
  30. package/dist/plugin-routes-B9PvcDQJ.js +22 -0
  31. package/dist/{policy-pu8tsF8_.js → policy-BBDU-PQK.js} +1 -1
  32. package/dist/{probe-cBVPhCKZ.js → probe-Djes9Fy6.js} +1 -1
  33. package/dist/{provider-Bccng2Wp.js → provider-CxMP_s2o.js} +828 -28
  34. package/dist/{replies-jiEDV6lH.js → replies-DkmWK7JW.js} +2 -4
  35. package/dist/{resolve-channels-CT4oiz_9.js → resolve-channels-zXt5f47h.js} +1 -1
  36. package/dist/{resolve-users-o5S-Ijks.js → resolve-users-BLfGAz1v.js} +1 -1
  37. package/dist/{runtime-api-CyPE-EvY.js → runtime-api-DvpUD2hw.js} +2 -2
  38. package/dist/runtime-api.js +12 -12
  39. package/dist/{scopes-CnyhLIPT.js → scopes-DiiHsqh1.js} +1 -1
  40. package/dist/{send-DvfE8LVm.js → send-BURYyCXI.js} +4 -4
  41. package/dist/send.runtime-CKaMG3s-.js +2 -0
  42. package/dist/{setup-core-DKe7Ug3V.js → setup-core-POfI_bgP.js} +2 -2
  43. package/dist/setup-entry.js +8 -1
  44. package/dist/setup-plugin-api.js +1 -1
  45. package/dist/{setup-surface-gjRthuZA.js → setup-surface-DJTHAguz.js} +5 -5
  46. package/dist/{shared-D8A7iVVH.js → shared-D9WMYymo.js} +5 -5
  47. package/dist/{slash-dispatch.runtime-BBhdJHb8.js → slash-dispatch.runtime-lsyTm_q5.js} +1 -1
  48. package/dist/thread-ts-NSVqWybn.js +646 -0
  49. package/node_modules/semver/classes/range.js +7 -0
  50. package/node_modules/semver/package.json +1 -1
  51. package/node_modules/semver/ranges/subset.js +2 -2
  52. package/npm-shrinkwrap.json +6 -29
  53. package/openclaw.plugin.json +10 -0
  54. package/package.json +8 -7
  55. package/dist/accounts.runtime-BVdtQeuq.js +0 -2
  56. package/dist/action-runtime.runtime-BZa5VDjs.js +0 -2
  57. package/dist/allow-list-B1lkGjwl.js +0 -82
  58. package/dist/blocks-render-CNC4vQnd.js +0 -232
  59. package/dist/inbound-contract-test-api.js +0 -3
  60. package/dist/magic-string.es-BPXBBMwL.js +0 -1011
  61. package/dist/outbound-payload-test-api.js +0 -2
  62. package/dist/outbound-payload.test-harness-8MnHFgR1.js +0 -13551
  63. package/dist/pipeline.runtime-BVK8yXw_.js +0 -1473
  64. package/dist/plugin-routes-DRR3ijKM.js +0 -20
  65. package/dist/prepare-Bl5WcC3f.js +0 -1713
  66. package/dist/prepare.test-helpers-BcAo4KMw.js +0 -49
  67. package/dist/reply-blocks-BlOURkUm.js +0 -290
  68. package/dist/room-context-BmNTBiw5.js +0 -816
  69. package/dist/send.runtime-Dv6ajTGK.js +0 -2
  70. package/dist/send.runtime-v3TSw9xY.js +0 -2
  71. package/dist/test-api.js +0 -8
  72. package/dist/thread-ts-ks-O8cEG.js +0 -52
  73. package/node_modules/agent-base/LICENSE +0 -22
  74. package/node_modules/agent-base/README.md +0 -69
  75. package/node_modules/agent-base/dist/helpers.d.ts +0 -10
  76. package/node_modules/agent-base/dist/helpers.d.ts.map +0 -1
  77. package/node_modules/agent-base/dist/helpers.js +0 -37
  78. package/node_modules/agent-base/dist/helpers.js.map +0 -1
  79. package/node_modules/agent-base/dist/index.d.ts +0 -37
  80. package/node_modules/agent-base/dist/index.d.ts.map +0 -1
  81. package/node_modules/agent-base/dist/index.js +0 -146
  82. package/node_modules/agent-base/dist/index.js.map +0 -1
  83. package/node_modules/agent-base/package.json +0 -46
  84. package/node_modules/https-proxy-agent/LICENSE +0 -22
  85. package/node_modules/https-proxy-agent/README.md +0 -70
  86. package/node_modules/https-proxy-agent/dist/index.d.ts +0 -43
  87. package/node_modules/https-proxy-agent/dist/index.d.ts.map +0 -1
  88. package/node_modules/https-proxy-agent/dist/index.js +0 -150
  89. package/node_modules/https-proxy-agent/dist/index.js.map +0 -1
  90. package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +0 -12
  91. package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts.map +0 -1
  92. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +0 -94
  93. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +0 -1
  94. package/node_modules/https-proxy-agent/package.json +0 -50
@@ -0,0 +1,3492 @@
1
+ import { l as resolveSlackReplyToMode } from "./accounts-f6Xcv9Vi.js";
2
+ import { r as parseSlackTarget } from "./target-parsing-C7eeWg7M.js";
3
+ import "./targets-nUqxHGgg.js";
4
+ import { b as truncateSlackText, f as normalizeAllowListLower, h as resolveSlackAllowListMatch, p as normalizeSlackAllowOwnerEntry, s as SLACK_TEXT_LIMIT } from "./thread-ts-NSVqWybn.js";
5
+ import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-DrBq4Mld.js";
6
+ import { n as resolveSlackNativeStreaming, r as resolveSlackStreamingMode, t as mapStreamingModeToSlackLegacyDraftStreamMode } from "./streaming-compat-DjlgH-Be.js";
7
+ import { a as recordSlackThreadParticipation, i as hasSlackThreadParticipationWithPersistence, s as normalizeSlackOutboundText, t as sendMessageSlack } from "./send-BURYyCXI.js";
8
+ import { _ as resolveSlackThreadStarter, b as buildSlackEditTextPayload, f as removeSlackReaction, g as resolveSlackThreadHistory, l as reactSlackMessage, r as editSlackMessage, t as deleteSlackMessage, y as formatSlackFileReference } from "./actions-zfVWcIY6.js";
9
+ import { t as formatSlackError } from "./errors-CZtmv-h0.js";
10
+ import { _ as resolveStorePath, a as escapeSlackMrkdwn, b as stripSlackMentionsForCommandDetection, c as authorizeSlackBotRoomMessage, d as buildSlackAssistantThreadMetadata, f as parseSlackAssistantThreadMetadata, g as resolveChannelContextVisibilityMode, h as readSessionUpdatedAt, i as authorizeSlackDirectMessage, l as resolveSlackCommandIngress, m as resolveSlackChatType, o as recordInboundSession, p as normalizeSlackChannelType, r as resolveSlackRoomContextHints, s as resolveConversationLabel$1, u as resolveSlackEffectiveAllowFrom, v as updateLastRoute } from "./provider-CxMP_s2o.js";
11
+ import { n as resolveSlackChannelConfig } from "./policy-BBDU-PQK.js";
12
+ import { a as resolveDeliveredSlackReplyThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as resolveSlackThreadTs, t as createSlackReplyDeliveryPlan } from "./replies-DkmWK7JW.js";
13
+ import { asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
14
+ import { resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
15
+ import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelMessageReplyPipeline, createChannelProgressDraftGate, createDraftStreamLoop, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, isChannelProgressDraftWorkToolName, mergeChannelProgressDraftLine, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelProgressDraftMaxLines, resolveChannelProgressDraftRender, resolveChannelStreamingBlockEnabled, resolveChannelStreamingNativeTransport, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
16
+ import { createHash } from "node:crypto";
17
+ import { danger, logVerbose, shouldLogVerbose, sleep } from "openclaw/plugin-sdk/runtime-env";
18
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
19
+ import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
20
+ import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-chunking";
21
+ import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
22
+ import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
23
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
24
+ import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
25
+ import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
26
+ import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundReply, formatInboundEnvelope, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, logInboundDrop, matchesMentionWithExplicit, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveUnmentionedGroupInboundPolicy, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
27
+ import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist, shouldIncludeSupplementalContext } from "openclaw/plugin-sdk/security-runtime";
28
+ import { DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, removeAckReactionAfterReply, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
29
+ import { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime";
30
+ import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
31
+ import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
32
+ import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-surface";
33
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
34
+ import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
35
+ //#region extensions/slack/src/draft-stream.ts
36
+ const DEFAULT_THROTTLE_MS = 1e3;
37
+ function createSlackDraftStream(params) {
38
+ const maxChars = Math.min(params.maxChars ?? 8e3, SLACK_TEXT_LIMIT);
39
+ const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
40
+ const send = params.send ?? sendMessageSlack;
41
+ const edit = params.edit ?? editSlackMessage;
42
+ const remove = params.remove ?? deleteSlackMessage;
43
+ let streamMessageId;
44
+ let streamChannelId;
45
+ let lastSentKey = "";
46
+ let pendingUpdate;
47
+ let stopped = false;
48
+ const normalizeUpdate = (update) => typeof update === "string" ? { text: update } : update;
49
+ const sendOrEditStreamMessage = async (text) => {
50
+ if (stopped) return;
51
+ const trimmed = text.trimEnd();
52
+ if (!trimmed) return;
53
+ if (trimmed.length > maxChars) {
54
+ stopped = true;
55
+ params.warn?.(`slack stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
56
+ return;
57
+ }
58
+ const update = normalizeUpdate(pendingUpdate ?? text);
59
+ const blocks = update.text === text ? update.blocks : void 0;
60
+ const sentKey = `${trimmed}\n${blocks ? JSON.stringify(blocks) : ""}`;
61
+ if (sentKey === lastSentKey) return;
62
+ lastSentKey = sentKey;
63
+ try {
64
+ if (streamChannelId && streamMessageId) {
65
+ await edit(streamChannelId, streamMessageId, trimmed, {
66
+ cfg: params.cfg,
67
+ token: params.token,
68
+ accountId: params.accountId,
69
+ ...blocks ? { blocks } : {}
70
+ });
71
+ return;
72
+ }
73
+ const sent = await send(params.target, trimmed, {
74
+ cfg: params.cfg,
75
+ token: params.token,
76
+ accountId: params.accountId,
77
+ threadTs: params.resolveThreadTs?.(),
78
+ identity: params.identity,
79
+ ...params.metadata ? { metadata: params.metadata } : {},
80
+ ...blocks ? { blocks } : {}
81
+ });
82
+ streamChannelId = sent.channelId || streamChannelId;
83
+ streamMessageId = sent.messageId || streamMessageId;
84
+ if (!streamChannelId || !streamMessageId) {
85
+ stopped = true;
86
+ params.warn?.("slack stream preview stopped (missing identifiers from sendMessage)");
87
+ return;
88
+ }
89
+ params.onMessageSent?.();
90
+ } catch (err) {
91
+ stopped = true;
92
+ params.warn?.(`slack stream preview failed: ${formatSlackError(err)}`);
93
+ }
94
+ };
95
+ const loop = createDraftStreamLoop({
96
+ throttleMs,
97
+ isStopped: () => stopped,
98
+ sendOrEditStreamMessage
99
+ });
100
+ const stop = () => {
101
+ stopped = true;
102
+ loop.stop();
103
+ };
104
+ const discardPending = async () => {
105
+ stop();
106
+ await loop.waitForInFlight();
107
+ };
108
+ const clear = async () => {
109
+ await discardPending();
110
+ const channelId = streamChannelId;
111
+ const messageId = streamMessageId;
112
+ streamChannelId = void 0;
113
+ streamMessageId = void 0;
114
+ lastSentKey = "";
115
+ pendingUpdate = void 0;
116
+ if (!channelId || !messageId) return;
117
+ try {
118
+ await remove(channelId, messageId, {
119
+ token: params.token,
120
+ accountId: params.accountId
121
+ });
122
+ } catch (err) {
123
+ params.warn?.(`slack stream preview cleanup failed: ${formatSlackError(err)}`);
124
+ }
125
+ };
126
+ const forceNewMessage = () => {
127
+ streamMessageId = void 0;
128
+ streamChannelId = void 0;
129
+ lastSentKey = "";
130
+ pendingUpdate = void 0;
131
+ loop.resetPending();
132
+ };
133
+ params.log?.(`slack stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
134
+ return {
135
+ update: (update) => {
136
+ const normalized = normalizeUpdate(update);
137
+ pendingUpdate = update;
138
+ loop.update(normalized.text);
139
+ },
140
+ flush: loop.flush,
141
+ clear,
142
+ discardPending,
143
+ seal: discardPending,
144
+ stop,
145
+ forceNewMessage,
146
+ messageId: () => streamMessageId,
147
+ channelId: () => streamChannelId
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region extensions/slack/src/progress-blocks.ts
152
+ const SLACK_PROGRESS_FIELD_MAX = 1800;
153
+ const DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS = 120;
154
+ const DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS = 48;
155
+ const SLACK_PROGRESS_CHUNK_TEXT_MAX = 256;
156
+ const SLACK_PROGRESS_TASK_TITLE_MAX = 120;
157
+ const SLACK_PROGRESS_PLAN_FALLBACK_TITLE = "Thinking";
158
+ function field(text) {
159
+ return {
160
+ type: "mrkdwn",
161
+ text: truncateSlackText(text, SLACK_PROGRESS_FIELD_MAX)
162
+ };
163
+ }
164
+ function resolveMaxLineChars(value, fallback) {
165
+ return value && value > 0 ? Math.floor(value) : fallback;
166
+ }
167
+ function compactDetail(value, maxChars) {
168
+ const normalized = value.replace(/\s+/g, " ").trim();
169
+ const chars = Array.from(normalized);
170
+ if (chars.length <= maxChars) return normalized;
171
+ if (maxChars <= 1) return "…";
172
+ const keepStart = Math.max(1, Math.ceil((maxChars - 1) * .45));
173
+ const keepEnd = Math.max(1, maxChars - keepStart - 1);
174
+ return `${chars.slice(0, keepStart).join("").trimEnd()}…${chars.slice(-keepEnd).join("").trimStart()}`;
175
+ }
176
+ function compactTitle(value) {
177
+ return truncateSlackText(value.replace(/\s+/g, " ").trim(), SLACK_PROGRESS_TASK_TITLE_MAX);
178
+ }
179
+ function compactChunkText(value) {
180
+ return truncateSlackText(value.replace(/\s+/g, " ").trim(), SLACK_PROGRESS_CHUNK_TEXT_MAX);
181
+ }
182
+ function lineDetailParts(line) {
183
+ return [line.detail, line.status && !line.detail?.includes(line.status) ? line.status : void 0].map((part) => part?.trim()).filter((part) => Boolean(part));
184
+ }
185
+ function legacyLineTitle(line) {
186
+ return `${line.icon ?? "•"} *${escapeSlackMrkdwn(line.label)}*`;
187
+ }
188
+ function legacyLineDetail(line, maxChars) {
189
+ const detail = lineDetailParts(line).join(" · ");
190
+ return detail ? escapeSlackMrkdwn(compactDetail(detail, maxChars)) : "—";
191
+ }
192
+ function lineTaskTitle(line, maxLineChars) {
193
+ const label = line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
194
+ const detail = lineDetailParts(line).join(" · ");
195
+ const fallback = line.text.replace(/\s+/g, " ").trim();
196
+ if (detail) return compactTitle(`${label} — ${compactDetail(detail, maxLineChars)}`);
197
+ if (fallback && fallback !== label) return compactTitle(fallback);
198
+ return compactTitle(label);
199
+ }
200
+ function lineTaskStatus(line) {
201
+ const normalized = line.status?.replace(/\s+/g, " ").trim().toLowerCase();
202
+ if (!normalized) return "in_progress";
203
+ if (normalized === "complete" || normalized === "completed" || normalized === "done" || normalized === "ok" || normalized === "success" || normalized === "succeeded" || normalized === "successful" || normalized === "exit 0") return "complete";
204
+ if (normalized === "error" || normalized === "failed" || normalized === "failure" || normalized.startsWith("exit ")) return normalized === "exit 0" ? "complete" : "error";
205
+ return "in_progress";
206
+ }
207
+ function slugTaskIdPart(value) {
208
+ return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "task";
209
+ }
210
+ function stableTaskIdPart(value) {
211
+ const suffix = createHash("sha256").update(value).digest("hex").slice(0, 8);
212
+ return `${slugTaskIdPart(value)}_${suffix}`;
213
+ }
214
+ function buildPlanTasks(params) {
215
+ const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS);
216
+ return params.lines.slice(-50).map((line, index) => ({
217
+ id: line.id ? stableTaskIdPart(line.id) : `${slugTaskIdPart(line.toolName ?? line.kind ?? line.label)}_${index + 1}`,
218
+ title: lineTaskTitle(line, maxLineChars),
219
+ status: lineTaskStatus(line)
220
+ }));
221
+ }
222
+ function resolvePlanTitle(params) {
223
+ return compactChunkText(params.title?.trim() || params.label?.trim() || params.tasks.at(-1)?.title || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
224
+ }
225
+ function buildSlackProgressStreamChunks(params) {
226
+ const tasks = buildPlanTasks({
227
+ lines: params.lines,
228
+ maxLineChars: params.maxLineChars
229
+ });
230
+ if (tasks.length === 0) return;
231
+ return [{
232
+ type: "plan_update",
233
+ title: resolvePlanTitle({
234
+ label: params.label,
235
+ title: params.title,
236
+ tasks
237
+ })
238
+ }, ...tasks.map((task) => ({
239
+ type: "task_update",
240
+ id: task.id,
241
+ title: task.title,
242
+ status: task.status === "in_progress" ? params.finalInProgressStatus ?? (params.completeInProgress ? "complete" : task.status) : task.status
243
+ }))];
244
+ }
245
+ function buildSlackProgressDraftBlocks(params) {
246
+ const label = params.label?.trim() || params.title?.trim();
247
+ const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS);
248
+ const renderedBlocks = [...label ? [{
249
+ type: "section",
250
+ text: field(`*${escapeSlackMrkdwn(label)}*`)
251
+ }] : [], ...params.lines.map((line) => ({
252
+ type: "section",
253
+ fields: [field(legacyLineTitle(line)), field(legacyLineDetail(line, maxLineChars))]
254
+ }))].slice(-50);
255
+ return renderedBlocks.length ? renderedBlocks : void 0;
256
+ }
257
+ function buildSlackProgressStreamStartChunks(params) {
258
+ return buildSlackProgressStreamChunks(params);
259
+ }
260
+ function buildSlackProgressStreamUpdateChunks(params) {
261
+ return buildSlackProgressStreamChunks(params);
262
+ }
263
+ function buildSlackProgressStreamCompletionChunks(params) {
264
+ return buildSlackProgressStreamChunks({
265
+ ...params,
266
+ completeInProgress: true
267
+ });
268
+ }
269
+ //#endregion
270
+ //#region extensions/slack/src/stream-mode.ts
271
+ function resolveSlackStreamingConfig(params) {
272
+ const mode = resolveSlackStreamingMode(params);
273
+ return {
274
+ mode,
275
+ nativeStreaming: resolveSlackNativeStreaming(params),
276
+ draftMode: mapStreamingModeToSlackLegacyDraftStreamMode(mode)
277
+ };
278
+ }
279
+ function applyAppendOnlyStreamUpdate(params) {
280
+ const incoming = params.incoming.trimEnd();
281
+ if (!incoming) return {
282
+ rendered: params.rendered,
283
+ source: params.source,
284
+ changed: false
285
+ };
286
+ if (!params.rendered) return {
287
+ rendered: incoming,
288
+ source: incoming,
289
+ changed: true
290
+ };
291
+ if (incoming === params.source) return {
292
+ rendered: params.rendered,
293
+ source: params.source,
294
+ changed: false
295
+ };
296
+ if (incoming.startsWith(params.source) || incoming.startsWith(params.rendered)) return {
297
+ rendered: incoming,
298
+ source: incoming,
299
+ changed: incoming !== params.rendered
300
+ };
301
+ if (params.source.startsWith(incoming)) return {
302
+ rendered: params.rendered,
303
+ source: params.source,
304
+ changed: false
305
+ };
306
+ const separator = params.rendered.endsWith("\n") ? "" : "\n";
307
+ return {
308
+ rendered: `${params.rendered}${separator}${incoming}`,
309
+ source: incoming,
310
+ changed: true
311
+ };
312
+ }
313
+ //#endregion
314
+ //#region extensions/slack/src/streaming.ts
315
+ /**
316
+ * Thrown when Slack rejects a stream flush/finalize with a recipient-resolution
317
+ * error (see {@link BENIGN_SLACK_FINALIZE_ERROR_CODES}) while text is still
318
+ * only buffered locally by the Slack SDK. Carries the pending text so the
319
+ * caller can deliver it via the normal Slack reply path.
320
+ */
321
+ var SlackStreamNotDeliveredError = class extends Error {
322
+ constructor(pendingText, slackCode) {
323
+ super(`slack-stream: finalize failed with ${slackCode} before any text reached Slack (${pendingText.length} chars pending)`);
324
+ this.name = "SlackStreamNotDeliveredError";
325
+ this.pendingText = pendingText;
326
+ this.slackCode = slackCode;
327
+ }
328
+ };
329
+ /**
330
+ * Start a new Slack text stream.
331
+ *
332
+ * Returns a {@link SlackStreamSession} that should be passed to
333
+ * {@link appendSlackStream} and {@link stopSlackStream}.
334
+ *
335
+ * The first chunk of text can optionally be included via `text`.
336
+ */
337
+ async function startSlackStream(params) {
338
+ const { client, channel, threadTs, text, chunks, taskDisplayMode, teamId, userId } = params;
339
+ logVerbose(`slack-stream: starting stream in ${channel} thread=${threadTs}${teamId ? ` team=${teamId}` : ""}${userId ? ` user=${userId}` : ""}`);
340
+ const streamer = client.chatStream({
341
+ channel,
342
+ thread_ts: threadTs,
343
+ ...taskDisplayMode ? { task_display_mode: taskDisplayMode } : {},
344
+ ...teamId ? { recipient_team_id: teamId } : {},
345
+ ...userId ? { recipient_user_id: userId } : {}
346
+ });
347
+ const session = {
348
+ streamer,
349
+ channel,
350
+ threadTs,
351
+ stopped: false,
352
+ delivered: false,
353
+ pendingText: ""
354
+ };
355
+ if (text || chunks?.length) {
356
+ if (text) session.pendingText += text;
357
+ try {
358
+ const result = await streamer.append({
359
+ ...text ? { markdown_text: text } : {},
360
+ ...chunks?.length ? { chunks } : {}
361
+ });
362
+ if (result) {
363
+ session.delivered = true;
364
+ session.pendingText = "";
365
+ }
366
+ logVerbose(`slack-stream: appended initial payload (${text?.length ?? 0} chars, ${chunks?.length ?? 0} chunks, ${result ? "flushed" : "buffered"})`);
367
+ } catch (err) {
368
+ if (isBenignSlackFinalizeError(err) && session.pendingText) throw new SlackStreamNotDeliveredError(session.pendingText, extractSlackErrorCode(err) ?? "unknown");
369
+ throw err;
370
+ }
371
+ }
372
+ return session;
373
+ }
374
+ /**
375
+ * Append markdown text to an active Slack stream.
376
+ */
377
+ async function appendSlackStream(params) {
378
+ const { session, text, chunks } = params;
379
+ if (session.stopped) {
380
+ logVerbose("slack-stream: attempted to append to a stopped stream, ignoring");
381
+ return;
382
+ }
383
+ if (!text && !chunks?.length) return;
384
+ if (text) session.pendingText += text;
385
+ try {
386
+ const result = await session.streamer.append({
387
+ ...text ? { markdown_text: text } : {},
388
+ ...chunks?.length ? { chunks } : {}
389
+ });
390
+ if (result) {
391
+ session.delivered = true;
392
+ session.pendingText = "";
393
+ }
394
+ logVerbose(`slack-stream: appended ${text?.length ?? 0} chars, ${chunks?.length ?? 0} chunks (${result ? "flushed" : "buffered"})`);
395
+ } catch (err) {
396
+ if (isBenignSlackFinalizeError(err) && session.pendingText) throw new SlackStreamNotDeliveredError(session.pendingText, extractSlackErrorCode(err) ?? "unknown");
397
+ throw err;
398
+ }
399
+ }
400
+ /**
401
+ * Stop (finalize) a Slack stream.
402
+ *
403
+ * After calling this the stream message becomes a normal Slack message.
404
+ * Optionally include final text to append before stopping.
405
+ *
406
+ * If Slack's `chat.stopStream` responds with a known benign finalize error
407
+ * (see {@link BENIGN_SLACK_FINALIZE_ERROR_CODES}) AND any prior `append`
408
+ * has already landed on Slack, the error is swallowed and the session is
409
+ * marked stopped - the already-delivered text stays visible.
410
+ *
411
+ * If the same benign error fires while text is still only buffered locally
412
+ * (e.g. short replies that never exceeded the SDK's buffer_size), this
413
+ * function throws a {@link SlackStreamNotDeliveredError} carrying that pending
414
+ * text so the caller can deliver it through the normal Slack reply path.
415
+ *
416
+ * All other errors propagate unchanged.
417
+ */
418
+ async function stopSlackStream(params) {
419
+ const { session, text, chunks, metadata } = params;
420
+ if (session.stopped) {
421
+ logVerbose("slack-stream: stream already stopped, ignoring duplicate stop");
422
+ return;
423
+ }
424
+ session.stopped = true;
425
+ if (text) session.pendingText += text;
426
+ logVerbose(`slack-stream: stopping stream in ${session.channel} thread=${session.threadTs}${text ? ` (final text: ${text.length} chars)` : ""}`);
427
+ try {
428
+ await session.streamer.stop(text || chunks?.length || metadata ? {
429
+ ...text ? { markdown_text: text } : {},
430
+ ...chunks?.length ? { chunks } : {},
431
+ ...metadata ? { metadata } : {}
432
+ } : void 0);
433
+ session.delivered = true;
434
+ session.pendingText = "";
435
+ } catch (err) {
436
+ if (isBenignSlackFinalizeError(err)) {
437
+ const code = extractSlackErrorCode(err) ?? "unknown";
438
+ if (session.pendingText) throw new SlackStreamNotDeliveredError(session.pendingText, code);
439
+ if (session.delivered) {
440
+ logVerbose(`slack-stream: finalize rejected by Slack (${code}); prior appends delivered, treating stream as stopped`);
441
+ return;
442
+ }
443
+ }
444
+ throw err;
445
+ }
446
+ logVerbose("slack-stream: stream stopped");
447
+ }
448
+ /**
449
+ * Slack API error codes that indicate `chat.stopStream` (or the
450
+ * `chat.startStream` call the SDK issues inside `stop()` when the buffer
451
+ * never flushed) cannot finalize the stream for the current recipient or
452
+ * team. Either the caller falls back to a normal message (see
453
+ * {@link SlackStreamNotDeliveredError}) or, if prior appends already
454
+ * delivered text, the error is logged verbosely and swallowed.
455
+ */
456
+ const BENIGN_SLACK_FINALIZE_ERROR_CODES = new Set([
457
+ "user_not_found",
458
+ "team_not_found",
459
+ "missing_recipient_user_id"
460
+ ]);
461
+ function isBenignSlackFinalizeError(err) {
462
+ const code = extractSlackErrorCode(err);
463
+ return code !== void 0 && BENIGN_SLACK_FINALIZE_ERROR_CODES.has(code);
464
+ }
465
+ function extractSlackErrorCode(err) {
466
+ if (!err || typeof err !== "object") return;
467
+ const record = err;
468
+ if (record.data && typeof record.data === "object") {
469
+ const inner = record.data.error;
470
+ if (typeof inner === "string") return inner;
471
+ }
472
+ return (typeof record.message === "string" ? record.message : "").match(/An API error occurred:\s*([a-z_][a-z0-9_]*)/i)?.[1];
473
+ }
474
+ function markSlackStreamFallbackDelivered(session) {
475
+ const hadNativeDelivery = session.delivered;
476
+ session.pendingText = "";
477
+ session.delivered = true;
478
+ if (!hadNativeDelivery) session.stopped = true;
479
+ }
480
+ //#endregion
481
+ //#region extensions/slack/src/threading.ts
482
+ function resolveSlackThreadContext(params) {
483
+ const incomingThreadTs = params.message.thread_ts;
484
+ const eventTs = params.message.event_ts;
485
+ const messageTs = params.message.ts ?? eventTs;
486
+ const isThreadReply = typeof incomingThreadTs === "string" && incomingThreadTs.length > 0 && (incomingThreadTs !== messageTs || Boolean(params.message.parent_user_id));
487
+ return {
488
+ incomingThreadTs,
489
+ messageTs,
490
+ isThreadReply,
491
+ replyToId: incomingThreadTs ?? messageTs,
492
+ messageThreadId: isThreadReply ? incomingThreadTs : params.replyToMode === "all" ? messageTs : void 0
493
+ };
494
+ }
495
+ /**
496
+ * Resolves Slack thread targeting for replies and status indicators.
497
+ *
498
+ * @returns replyThreadTs - Thread timestamp for reply messages
499
+ * @returns statusThreadTs - Thread timestamp for status indicators (typing, etc.)
500
+ * @returns isThreadReply - true if this is a genuine user reply in a thread,
501
+ * false if thread_ts comes from a bot status message (e.g. typing indicator)
502
+ */
503
+ function resolveSlackThreadTargets(params) {
504
+ const { incomingThreadTs, messageTs, isThreadReply } = resolveSlackThreadContext(params);
505
+ const replyThreadTs = isThreadReply ? incomingThreadTs : params.replyToMode === "all" ? messageTs : void 0;
506
+ return {
507
+ replyThreadTs,
508
+ statusThreadTs: replyThreadTs,
509
+ isThreadReply
510
+ };
511
+ }
512
+ //#endregion
513
+ //#region extensions/slack/src/monitor/message-handler/preview-finalize.ts
514
+ function buildExpectedSlackEditText(params) {
515
+ return buildSlackEditTextPayload(params.text, params.blocks);
516
+ }
517
+ function blocksMatch(expected, actual) {
518
+ if (!expected?.length) return !actual?.length;
519
+ if (!actual?.length) return false;
520
+ return JSON.stringify(expected) === JSON.stringify(actual);
521
+ }
522
+ async function readSlackMessageAfterEditError(params) {
523
+ if (params.threadTs) return ((await params.client.conversations.replies({
524
+ token: params.token,
525
+ channel: params.channelId,
526
+ ts: params.threadTs,
527
+ latest: params.messageId,
528
+ inclusive: true,
529
+ limit: 100
530
+ })).messages ?? []).find((message) => message?.ts === params.messageId) ?? null;
531
+ const message = (await params.client.conversations.history({
532
+ token: params.token,
533
+ channel: params.channelId,
534
+ latest: params.messageId,
535
+ oldest: params.messageId,
536
+ inclusive: true,
537
+ limit: 1
538
+ })).messages?.[0];
539
+ if (!message?.ts || message.ts !== params.messageId) return null;
540
+ return message;
541
+ }
542
+ async function didSlackPreviewEditApplyAfterError(params) {
543
+ const readback = await readSlackMessageAfterEditError(params);
544
+ if (!readback) return false;
545
+ const expectedText = buildExpectedSlackEditText({
546
+ text: params.text,
547
+ blocks: params.blocks
548
+ });
549
+ const actualText = normalizeSlackOutboundText((readback.text ?? "").trim());
550
+ if (params.blocks?.length) return actualText === expectedText && blocksMatch(params.blocks, readback.blocks);
551
+ return actualText === expectedText;
552
+ }
553
+ async function finalizeSlackPreviewEdit(params) {
554
+ try {
555
+ await editSlackMessage(params.channelId, params.messageId, params.text, {
556
+ token: params.token,
557
+ accountId: params.accountId,
558
+ client: params.client,
559
+ ...params.blocks?.length ? { blocks: params.blocks } : {}
560
+ });
561
+ return;
562
+ } catch (err) {
563
+ try {
564
+ if (await didSlackPreviewEditApplyAfterError({
565
+ client: params.client,
566
+ token: params.token,
567
+ channelId: params.channelId,
568
+ messageId: params.messageId,
569
+ text: params.text,
570
+ blocks: params.blocks,
571
+ threadTs: params.threadTs
572
+ })) {
573
+ logVerbose(`slack: preview final edit response failed but readback matched message ${params.channelId}/${params.messageId}; suppressing duplicate fallback send`);
574
+ return;
575
+ }
576
+ } catch (readbackErr) {
577
+ logVerbose(`slack: preview final edit readback failed (${String(readbackErr)})`);
578
+ }
579
+ throw err;
580
+ }
581
+ }
582
+ //#endregion
583
+ //#region extensions/slack/src/monitor/message-handler/dispatch.ts
584
+ const UNICODE_TO_SLACK = {
585
+ "👀": "eyes",
586
+ "🤔": "thinking_face",
587
+ "🔥": "fire",
588
+ "👨‍💻": "male-technologist",
589
+ "👨💻": "male-technologist",
590
+ "👩‍💻": "female-technologist",
591
+ "⚡": "zap",
592
+ "🌐": "globe_with_meridians",
593
+ "✅": "white_check_mark",
594
+ "👍": "thumbsup",
595
+ "❌": "x",
596
+ "😱": "scream",
597
+ "🥱": "yawning_face",
598
+ "😨": "fearful",
599
+ "⏳": "hourglass_flowing_sand",
600
+ "⚠️": "warning",
601
+ "✍": "writing_hand",
602
+ "🗜️": "compression",
603
+ "🗜": "compression",
604
+ "🧠": "brain",
605
+ "🛠️": "hammer_and_wrench",
606
+ "💻": "computer"
607
+ };
608
+ const SLACK_REASONING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
609
+ const SLACK_REASONING_LABEL_PREFIX_RE = /^\s*(?:>\s*)?Reasoning:\s*/iu;
610
+ const SLACK_THINKING_LABEL_PREFIX_RE = /^\s*(?:>\s*)?Thinking\.{0,3}(?=\s*(?:\n|_))/iu;
611
+ function resolveSlackMessageTimestampMs(message) {
612
+ const ts = message.event_ts ?? message.ts;
613
+ if (!ts) return;
614
+ const parsed = Number(ts);
615
+ return Number.isFinite(parsed) ? Math.trunc(parsed * 1e3) : void 0;
616
+ }
617
+ function resolveSlackBotLoopProtection(prepared) {
618
+ const senderBotId = prepared.message.bot_id;
619
+ if (!senderBotId) return;
620
+ const receiverBotId = prepared.ctx.botId || prepared.ctx.botUserId;
621
+ if (!receiverBotId || senderBotId === prepared.ctx.botId || prepared.message.user === prepared.ctx.botUserId) return;
622
+ return {
623
+ scopeId: prepared.route.accountId,
624
+ conversationId: prepared.message.channel,
625
+ senderId: senderBotId,
626
+ receiverId: receiverBotId,
627
+ config: mergePairLoopGuardConfig(prepared.account.config.botLoopProtection, prepared.channelConfig?.botLoopProtection),
628
+ defaultsConfig: prepared.ctx.cfg.channels?.defaults?.botLoopProtection,
629
+ defaultEnabled: true,
630
+ nowMs: resolveSlackMessageTimestampMs(prepared.message)
631
+ };
632
+ }
633
+ function toSlackEmojiName(emoji) {
634
+ let trimmed = emoji.trim();
635
+ while (trimmed.startsWith(":")) trimmed = trimmed.slice(1);
636
+ while (trimmed.endsWith(":")) trimmed = trimmed.slice(0, -1);
637
+ return UNICODE_TO_SLACK[trimmed] ?? trimmed;
638
+ }
639
+ function isSlackStreamingEnabled(params) {
640
+ if (params.mode === "partial") return params.nativeStreaming;
641
+ if (params.mode === "progress") return params.nativeStreaming && params.nativeProgressTaskCards === true;
642
+ return false;
643
+ }
644
+ function shouldEnableSlackPreviewStreaming(params) {
645
+ return params.mode !== "off";
646
+ }
647
+ function shouldInitializeSlackDraftStream(params) {
648
+ return params.previewStreamingEnabled && !params.useStreaming;
649
+ }
650
+ function resolveSlackDisableBlockStreaming(params) {
651
+ if (params.useStreaming || params.shouldUseDraftStream) return true;
652
+ return typeof params.blockStreamingEnabled === "boolean" ? !params.blockStreamingEnabled : void 0;
653
+ }
654
+ function resolveExplicitSlackProgressTitle(entry) {
655
+ const label = resolveChannelProgressDraftConfig(entry).label;
656
+ if (typeof label !== "string") return;
657
+ const trimmed = label.trim();
658
+ return trimmed && trimmed.toLowerCase() !== "auto" ? trimmed : void 0;
659
+ }
660
+ function resolveSlackNativeProgressTaskCards(entry) {
661
+ const streaming = entry?.streaming;
662
+ if (!streaming || typeof streaming !== "object" || Array.isArray(streaming)) return false;
663
+ const progressConfig = streaming.progress;
664
+ return Boolean(progressConfig) && typeof progressConfig === "object" && !Array.isArray(progressConfig) && progressConfig.nativeTaskCards === true;
665
+ }
666
+ function resolveSlackStreamingThreadHint(params) {
667
+ return resolveSlackThreadTs({
668
+ replyToMode: params.replyToMode,
669
+ incomingThreadTs: params.incomingThreadTs,
670
+ messageTs: params.messageTs,
671
+ hasReplied: false,
672
+ isThreadReply: params.isThreadReply
673
+ });
674
+ }
675
+ const SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX = 2e3;
676
+ const slackStreamRecipientTeamCache = /* @__PURE__ */ new Map();
677
+ function buildSlackEventDeliveryKey(params) {
678
+ const reply = resolveSendableOutboundReplyParts(params.payload, { text: params.textOverride });
679
+ const slackBlocks = readSlackReplyBlocks(params.payload);
680
+ if (!reply.hasContent && !slackBlocks?.length) return null;
681
+ return JSON.stringify({
682
+ kind: params.kind,
683
+ threadTs: params.threadTs ?? "",
684
+ replyToId: params.payload.replyToId ?? null,
685
+ text: reply.trimmedText,
686
+ mediaUrls: reply.mediaUrls,
687
+ blocks: slackBlocks ?? null
688
+ });
689
+ }
690
+ function readSlackStreamRecipientTeamCache(params) {
691
+ if (!params.fallbackTeamId || !params.userId) return;
692
+ const cacheKey = `${params.fallbackTeamId}:${params.userId}`;
693
+ const cached = slackStreamRecipientTeamCache.get(cacheKey);
694
+ if (!cached) return;
695
+ slackStreamRecipientTeamCache.delete(cacheKey);
696
+ slackStreamRecipientTeamCache.set(cacheKey, cached);
697
+ return cached;
698
+ }
699
+ function rememberSlackStreamRecipientTeam(params) {
700
+ if (!params.fallbackTeamId || !params.userId) return;
701
+ const cacheKey = `${params.fallbackTeamId}:${params.userId}`;
702
+ if (slackStreamRecipientTeamCache.has(cacheKey)) slackStreamRecipientTeamCache.delete(cacheKey);
703
+ slackStreamRecipientTeamCache.set(cacheKey, params.teamId);
704
+ if (slackStreamRecipientTeamCache.size > SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX) {
705
+ const oldest = slackStreamRecipientTeamCache.keys().next().value;
706
+ if (oldest) slackStreamRecipientTeamCache.delete(oldest);
707
+ }
708
+ }
709
+ function normalizeSlackReasoningProgressLine(text) {
710
+ return (extractSlackReasoningTagText(text) || stripReasoningTagsFromText(text, {
711
+ mode: "strict",
712
+ trim: "both"
713
+ })).replace(SLACK_REASONING_LABEL_PREFIX_RE, "").replace(SLACK_THINKING_LABEL_PREFIX_RE, "").split(/\r?\n/u).map((line) => stripSimpleItalicMarkers(line)).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
714
+ }
715
+ function extractSlackReasoningTagText(text) {
716
+ if (!text) return "";
717
+ let result = "";
718
+ let lastIndex = 0;
719
+ let inReasoning = false;
720
+ SLACK_REASONING_TAG_RE.lastIndex = 0;
721
+ for (const match of text.matchAll(SLACK_REASONING_TAG_RE)) {
722
+ const index = match.index ?? 0;
723
+ if (inReasoning) result += text.slice(lastIndex, index);
724
+ inReasoning = match[1] !== "/";
725
+ lastIndex = index + match[0].length;
726
+ }
727
+ if (inReasoning) result += text.slice(lastIndex);
728
+ return result.trim();
729
+ }
730
+ function stripSimpleItalicMarkers(line) {
731
+ const trimmed = line.trim();
732
+ if (trimmed.length >= 2 && trimmed.startsWith("_") && trimmed.endsWith("_")) return trimmed.slice(1, -1).trim();
733
+ return trimmed;
734
+ }
735
+ function mergeSlackReasoningProgressText(current, incoming, options) {
736
+ if (!current) return incoming;
737
+ const normalizedCurrent = normalizeSlackReasoningProgressLine(current);
738
+ const normalizedIncoming = normalizeSlackReasoningProgressLine(incoming);
739
+ if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) return current;
740
+ if (options?.snapshot === true || isSlackReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) return incoming;
741
+ return `${current}${incoming}`;
742
+ }
743
+ function isSlackReasoningSnapshotText(text) {
744
+ return SLACK_REASONING_LABEL_PREFIX_RE.test(text) || SLACK_THINKING_LABEL_PREFIX_RE.test(text);
745
+ }
746
+ function createSlackEventDeliveryTracker() {
747
+ const deliveredKeys = /* @__PURE__ */ new Set();
748
+ return {
749
+ hasDelivered(params) {
750
+ const key = buildSlackEventDeliveryKey(params);
751
+ return key ? deliveredKeys.has(key) : false;
752
+ },
753
+ markDelivered(params) {
754
+ const key = buildSlackEventDeliveryKey(params);
755
+ if (key) deliveredKeys.add(key);
756
+ }
757
+ };
758
+ }
759
+ function shouldUseStreaming(params) {
760
+ if (!params.streamingEnabled) return false;
761
+ if (!params.threadTs) {
762
+ logVerbose("slack-stream: streaming disabled — no reply thread target available");
763
+ return false;
764
+ }
765
+ return true;
766
+ }
767
+ async function resolveSlackStreamRecipientTeamId(params) {
768
+ const cachedTeamId = readSlackStreamRecipientTeamCache(params);
769
+ if (cachedTeamId) return cachedTeamId;
770
+ if (params.userId) try {
771
+ const info = await params.client.users.info({
772
+ token: params.token,
773
+ user: params.userId
774
+ });
775
+ const teamId = info.user?.team_id ?? info.user?.profile?.team;
776
+ if (teamId) {
777
+ rememberSlackStreamRecipientTeam({
778
+ ...params,
779
+ teamId
780
+ });
781
+ return teamId;
782
+ }
783
+ } catch (err) {
784
+ logVerbose(`slack-stream: users.info team lookup failed (${formatErrorMessage(err)})`);
785
+ }
786
+ return params.fallbackTeamId;
787
+ }
788
+ async function dispatchPreparedSlackMessage(prepared) {
789
+ const { ctx, account, message, route } = prepared;
790
+ const cfg = ctx.cfg;
791
+ const runtime = ctx.runtime;
792
+ const outboundIdentity = resolveAgentOutboundIdentity(cfg, route.agentId);
793
+ const slackIdentity = outboundIdentity ? {
794
+ username: outboundIdentity.name,
795
+ iconUrl: outboundIdentity.avatarUrl,
796
+ iconEmoji: outboundIdentity.emoji
797
+ } : void 0;
798
+ if (prepared.isDirectMessage) {
799
+ const sessionCfg = cfg.session;
800
+ const storePath = resolveStorePath(sessionCfg?.store, { agentId: route.agentId });
801
+ const pinnedMainDmOwner = resolvePinnedMainDmOwnerFromAllowlist({
802
+ dmScope: cfg.session?.dmScope,
803
+ allowFrom: ctx.allowFrom,
804
+ normalizeEntry: normalizeSlackAllowOwnerEntry
805
+ });
806
+ const senderRecipient = normalizeOptionalLowercaseString(message.user);
807
+ const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
808
+ route,
809
+ sessionKey: prepared.ctxPayload.SessionKey ?? route.sessionKey
810
+ });
811
+ if (inboundLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderRecipient && normalizeOptionalLowercaseString(pinnedMainDmOwner) !== senderRecipient) logVerbose(`slack: skip main-session last route for ${senderRecipient} (pinned owner ${pinnedMainDmOwner})`);
812
+ else await updateLastRoute({
813
+ storePath,
814
+ sessionKey: inboundLastRouteSessionKey,
815
+ deliveryContext: {
816
+ channel: "slack",
817
+ to: `user:${message.user}`,
818
+ accountId: route.accountId,
819
+ threadId: prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId
820
+ },
821
+ ctx: prepared.ctxPayload
822
+ });
823
+ }
824
+ const threadTargets = resolveSlackThreadTargets({
825
+ message,
826
+ replyToMode: prepared.replyToMode
827
+ });
828
+ const forcedReplyThreadTs = prepared.forcedReplyThreadTs;
829
+ const slackMessageMetadata = prepared.slackMessageMetadata;
830
+ const statusThreadTs = forcedReplyThreadTs ?? threadTargets.statusThreadTs;
831
+ const isThreadReply = threadTargets.isThreadReply;
832
+ const replyDeliveryMode = forcedReplyThreadTs ? "off" : prepared.replyToMode;
833
+ const sourceReplyDeliveryMode = resolveChannelMessageSourceReplyDeliveryMode({
834
+ cfg,
835
+ ctx: prepared.ctxPayload
836
+ });
837
+ const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
838
+ const reactionMessageTs = prepared.ackReactionMessageTs;
839
+ const messageTs = message.ts ?? message.event_ts;
840
+ const incomingThreadTs = message.thread_ts;
841
+ let didSetStatus = false;
842
+ const statusReactionsEnabled = Boolean(prepared.ackReactionPromise) && Boolean(reactionMessageTs) && cfg.messages?.statusReactions?.enabled !== false;
843
+ const slackStatusAdapter = {
844
+ setReaction: async (emoji) => {
845
+ await reactSlackMessage(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), {
846
+ token: ctx.botToken,
847
+ client: ctx.app.client
848
+ }).catch((err) => {
849
+ if (formatErrorMessage(err).includes("already_reacted")) return;
850
+ throw err;
851
+ });
852
+ },
853
+ removeReaction: async (emoji) => {
854
+ await removeSlackReaction(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), {
855
+ token: ctx.botToken,
856
+ client: ctx.app.client
857
+ }).catch((err) => {
858
+ if (formatErrorMessage(err).includes("no_reaction")) return;
859
+ throw err;
860
+ });
861
+ }
862
+ };
863
+ const statusReactionTiming = {
864
+ ...DEFAULT_TIMING,
865
+ ...cfg.messages?.statusReactions?.timing
866
+ };
867
+ const statusReactions = createStatusReactionController({
868
+ enabled: statusReactionsEnabled,
869
+ adapter: slackStatusAdapter,
870
+ initialEmoji: prepared.ackReactionValue || "eyes",
871
+ emojis: cfg.messages?.statusReactions?.emojis,
872
+ timing: cfg.messages?.statusReactions?.timing,
873
+ onError: (err) => {
874
+ logAckFailure({
875
+ log: logVerbose,
876
+ channel: "slack",
877
+ target: `${message.channel}/${message.ts}`,
878
+ error: err
879
+ });
880
+ }
881
+ });
882
+ if (statusReactionsEnabled) statusReactions.setQueued();
883
+ const hasRepliedRef = { value: false };
884
+ const replyPlan = createSlackReplyDeliveryPlan({
885
+ replyToMode: replyDeliveryMode,
886
+ incomingThreadTs: forcedReplyThreadTs ?? incomingThreadTs,
887
+ messageTs,
888
+ hasRepliedRef,
889
+ isThreadReply: Boolean(forcedReplyThreadTs) || isThreadReply
890
+ });
891
+ const typingTarget = statusThreadTs ? `${message.channel}/${statusThreadTs}` : message.channel;
892
+ const typingReaction = ctx.typingReaction;
893
+ const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
894
+ cfg,
895
+ agentId: route.agentId,
896
+ channel: "slack",
897
+ accountId: route.accountId,
898
+ transformReplyPayload: (payload) => {
899
+ if (payload.isReasoning === true) return null;
900
+ return isSlackInteractiveRepliesEnabled({
901
+ cfg,
902
+ accountId: route.accountId
903
+ }) ? compileSlackInteractiveReplies(payload) : payload;
904
+ },
905
+ typing: {
906
+ start: async () => {
907
+ didSetStatus = true;
908
+ await ctx.setSlackThreadStatus({
909
+ channelId: message.channel,
910
+ threadTs: statusThreadTs,
911
+ status: "is typing..."
912
+ });
913
+ if (typingReaction && message.ts) await reactSlackMessage(message.channel, message.ts, typingReaction, {
914
+ token: ctx.botToken,
915
+ client: ctx.app.client
916
+ }).catch(() => {});
917
+ },
918
+ stop: async () => {
919
+ if (!didSetStatus) return;
920
+ didSetStatus = false;
921
+ await ctx.setSlackThreadStatus({
922
+ channelId: message.channel,
923
+ threadTs: statusThreadTs,
924
+ status: ""
925
+ });
926
+ if (typingReaction && message.ts) await removeSlackReaction(message.channel, message.ts, typingReaction, {
927
+ token: ctx.botToken,
928
+ client: ctx.app.client
929
+ }).catch(() => {});
930
+ },
931
+ onStartError: (err) => {
932
+ logTypingFailure({
933
+ log: (message) => runtime.error?.(danger(message)),
934
+ channel: "slack",
935
+ action: "start",
936
+ target: typingTarget,
937
+ error: err
938
+ });
939
+ },
940
+ onStopError: (err) => {
941
+ logTypingFailure({
942
+ log: (message) => runtime.error?.(danger(message)),
943
+ channel: "slack",
944
+ action: "stop",
945
+ target: typingTarget,
946
+ error: err
947
+ });
948
+ }
949
+ }
950
+ });
951
+ const slackStreaming = resolveSlackStreamingConfig({
952
+ streaming: account.config.streaming,
953
+ nativeStreaming: resolveChannelStreamingNativeTransport(account.config)
954
+ });
955
+ const streamThreadHint = forcedReplyThreadTs ?? resolveSlackStreamingThreadHint({
956
+ replyToMode: replyDeliveryMode,
957
+ incomingThreadTs,
958
+ messageTs,
959
+ isThreadReply
960
+ });
961
+ const previewStreamingEnabled = !sourceRepliesAreToolOnly && shouldEnableSlackPreviewStreaming({ mode: slackStreaming.mode });
962
+ const useStreaming = shouldUseStreaming({
963
+ streamingEnabled: !sourceRepliesAreToolOnly && isSlackStreamingEnabled({
964
+ mode: slackStreaming.mode,
965
+ nativeStreaming: slackStreaming.nativeStreaming,
966
+ nativeProgressTaskCards: resolveSlackNativeProgressTaskCards(account.config)
967
+ }),
968
+ threadTs: streamThreadHint
969
+ });
970
+ const shouldUseDraftStream = shouldInitializeSlackDraftStream({
971
+ previewStreamingEnabled,
972
+ useStreaming
973
+ });
974
+ const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
975
+ const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
976
+ useStreaming,
977
+ shouldUseDraftStream,
978
+ blockStreamingEnabled
979
+ });
980
+ let streamSession = null;
981
+ let nativeProgressStreamStartPromise = null;
982
+ let nativeProgressStreamThreadTs;
983
+ let streamFailed = false;
984
+ let usedReplyThreadTs;
985
+ let usedBlockReplyThreadTs;
986
+ let observedReplyDelivery = false;
987
+ let observedFinalReplyDelivery = false;
988
+ const deliveryTracker = createSlackEventDeliveryTracker();
989
+ const resolveDeliveryThreadTs = (params) => {
990
+ const plannedThreadTs = params.forcedThreadTs ? void 0 : replyPlan.nextThreadTs();
991
+ return params.forcedThreadTs ?? plannedThreadTs ?? (params.kind === "block" ? usedBlockReplyThreadTs : void 0);
992
+ };
993
+ const rememberDeliveredThreadTs = (kind, deliveredThreadTs) => {
994
+ if (!deliveredThreadTs) return;
995
+ usedReplyThreadTs ??= deliveredThreadTs;
996
+ if (kind === "block") usedBlockReplyThreadTs = deliveredThreadTs;
997
+ };
998
+ const deliverPendingStreamFallback = async (session, err) => {
999
+ const fallbackText = err.pendingText.trim();
1000
+ if (!fallbackText) return false;
1001
+ try {
1002
+ await deliverReplies({
1003
+ cfg: ctx.cfg,
1004
+ replies: [{ text: fallbackText }],
1005
+ target: prepared.replyTarget,
1006
+ token: ctx.botToken,
1007
+ accountId: account.accountId,
1008
+ runtime,
1009
+ textLimit: ctx.textLimit,
1010
+ replyThreadTs: session.threadTs,
1011
+ replyToMode: replyDeliveryMode,
1012
+ ...slackIdentity ? { identity: slackIdentity } : {},
1013
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1014
+ });
1015
+ markSlackStreamFallbackDelivered(session);
1016
+ observedReplyDelivery = true;
1017
+ usedReplyThreadTs ??= session.threadTs;
1018
+ logVerbose(`slack-stream: streamed delivery failed (${err.slackCode}); delivered ${fallbackText.length} chars via deliverReplies fallback`);
1019
+ return true;
1020
+ } catch (postErr) {
1021
+ runtime.error?.(danger(`slack-stream: fallback deliverReplies failed after ${err.slackCode}: ${formatErrorMessage(postErr)}`));
1022
+ return false;
1023
+ }
1024
+ };
1025
+ const deliverNormally = async (params) => {
1026
+ if (params.payload.isReasoning === true) return;
1027
+ const replyThreadTs = resolveDeliveryThreadTs(params);
1028
+ if (deliveryTracker.hasDelivered({
1029
+ kind: params.kind,
1030
+ payload: params.payload,
1031
+ threadTs: replyThreadTs
1032
+ })) {
1033
+ logVerbose("slack: suppressed duplicate normal delivery within the same turn");
1034
+ return;
1035
+ }
1036
+ await deliverReplies({
1037
+ cfg: ctx.cfg,
1038
+ replies: [params.payload],
1039
+ target: prepared.replyTarget,
1040
+ token: ctx.botToken,
1041
+ accountId: account.accountId,
1042
+ runtime,
1043
+ textLimit: ctx.textLimit,
1044
+ replyThreadTs,
1045
+ replyToMode: replyDeliveryMode,
1046
+ ...slackIdentity ? { identity: slackIdentity } : {},
1047
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1048
+ });
1049
+ observedReplyDelivery = true;
1050
+ if (params.kind === "final") observedFinalReplyDelivery = true;
1051
+ const deliveredThreadTs = resolveDeliveredSlackReplyThreadTs({
1052
+ replyToMode: replyDeliveryMode,
1053
+ payloadReplyToId: params.payload.replyToId,
1054
+ replyThreadTs
1055
+ });
1056
+ rememberDeliveredThreadTs(params.kind, deliveredThreadTs);
1057
+ replyPlan.markSent();
1058
+ deliveryTracker.markDelivered({
1059
+ kind: params.kind,
1060
+ payload: params.payload,
1061
+ threadTs: replyThreadTs
1062
+ });
1063
+ };
1064
+ const deliverBufferedStreamFallback = async (params) => {
1065
+ if (!await deliverPendingStreamFallback(params.session, params.err)) return false;
1066
+ replyPlan.markSent();
1067
+ if (params.kind === "final") observedFinalReplyDelivery = true;
1068
+ deliveryTracker.markDelivered({
1069
+ kind: params.kind,
1070
+ payload: params.payload,
1071
+ threadTs: params.session.threadTs,
1072
+ textOverride: params.textOverride
1073
+ });
1074
+ rememberDeliveredThreadTs(params.kind, params.session.threadTs);
1075
+ return true;
1076
+ };
1077
+ const deliverWithStreaming = async (params) => {
1078
+ if (params.payload.isReasoning === true) return;
1079
+ const reply = resolveSendableOutboundReplyParts(params.payload);
1080
+ if (streamFailed || reply.hasMedia || readSlackReplyBlocks(params.payload)?.length || !reply.hasText) {
1081
+ await deliverNormally({
1082
+ payload: params.payload,
1083
+ kind: params.kind,
1084
+ forcedThreadTs: streamSession?.threadTs ?? nativeProgressStreamThreadTs
1085
+ });
1086
+ return;
1087
+ }
1088
+ const text = reply.trimmedText;
1089
+ let plannedThreadTs;
1090
+ try {
1091
+ if (!streamSession && nativeProgressStreamStartPromise) await nativeProgressStreamStartPromise;
1092
+ if (streamFailed) {
1093
+ await deliverNormally({
1094
+ payload: params.payload,
1095
+ kind: params.kind,
1096
+ forcedThreadTs: streamSession?.threadTs ?? nativeProgressStreamThreadTs
1097
+ });
1098
+ return;
1099
+ }
1100
+ if (useNativeProgressStreaming && !streamSession) {
1101
+ await deliverNormally({
1102
+ payload: params.payload,
1103
+ kind: params.kind
1104
+ });
1105
+ return;
1106
+ }
1107
+ if (!streamSession) {
1108
+ const streamThreadTs = replyPlan.nextThreadTs();
1109
+ plannedThreadTs = streamThreadTs;
1110
+ if (!streamThreadTs) {
1111
+ logVerbose("slack-stream: no reply thread target for stream start, falling back to normal delivery");
1112
+ streamFailed = true;
1113
+ await deliverNormally({
1114
+ payload: params.payload,
1115
+ kind: params.kind
1116
+ });
1117
+ return;
1118
+ }
1119
+ if (deliveryTracker.hasDelivered({
1120
+ kind: params.kind,
1121
+ payload: params.payload,
1122
+ threadTs: streamThreadTs,
1123
+ textOverride: text
1124
+ })) {
1125
+ logVerbose("slack-stream: suppressed duplicate stream start payload");
1126
+ return;
1127
+ }
1128
+ streamSession = await startSlackStream({
1129
+ client: ctx.app.client,
1130
+ channel: message.channel,
1131
+ threadTs: streamThreadTs,
1132
+ text,
1133
+ teamId: await resolveSlackStreamRecipientTeamId({
1134
+ client: ctx.app.client,
1135
+ token: ctx.botToken,
1136
+ userId: message.user,
1137
+ fallbackTeamId: ctx.teamId
1138
+ }),
1139
+ userId: message.user
1140
+ });
1141
+ if (streamSession.delivered) {
1142
+ observedReplyDelivery = true;
1143
+ if (params.kind === "final") observedFinalReplyDelivery = true;
1144
+ }
1145
+ rememberDeliveredThreadTs(params.kind, streamThreadTs);
1146
+ replyPlan.markSent();
1147
+ deliveryTracker.markDelivered({
1148
+ kind: params.kind,
1149
+ payload: params.payload,
1150
+ threadTs: streamThreadTs,
1151
+ textOverride: text
1152
+ });
1153
+ return;
1154
+ }
1155
+ if (deliveryTracker.hasDelivered({
1156
+ kind: params.kind,
1157
+ payload: params.payload,
1158
+ threadTs: streamSession.threadTs,
1159
+ textOverride: text
1160
+ })) {
1161
+ logVerbose("slack-stream: suppressed duplicate append payload");
1162
+ return;
1163
+ }
1164
+ const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent && previewToolProgressLines.length > 0 ? buildSlackProgressStreamCompletionChunks({
1165
+ title: explicitProgressTitle,
1166
+ lines: previewToolProgressLines,
1167
+ maxLineChars: progressDraftMaxLineChars,
1168
+ finalInProgressStatus: params.payload.isError ? "error" : "complete"
1169
+ }) : void 0;
1170
+ if (useNativeProgressStreaming) {
1171
+ if (completionChunks?.length) {
1172
+ await appendSlackStream({
1173
+ session: streamSession,
1174
+ chunks: completionChunks
1175
+ });
1176
+ nativeProgressCompletionSent = true;
1177
+ if (streamSession.delivered) observedReplyDelivery = true;
1178
+ }
1179
+ await deliverNormally({
1180
+ payload: params.payload,
1181
+ kind: params.kind,
1182
+ forcedThreadTs: streamSession.threadTs
1183
+ });
1184
+ return;
1185
+ }
1186
+ await appendSlackStream({
1187
+ session: streamSession,
1188
+ text: "\n" + text,
1189
+ chunks: completionChunks
1190
+ });
1191
+ if (completionChunks?.length) nativeProgressCompletionSent = true;
1192
+ if (streamSession.delivered) {
1193
+ observedReplyDelivery = true;
1194
+ if (params.kind === "final") observedFinalReplyDelivery = true;
1195
+ }
1196
+ deliveryTracker.markDelivered({
1197
+ kind: params.kind,
1198
+ payload: params.payload,
1199
+ threadTs: streamSession.threadTs,
1200
+ textOverride: text
1201
+ });
1202
+ } catch (err) {
1203
+ if (err instanceof SlackStreamNotDeliveredError) {
1204
+ streamFailed = true;
1205
+ if (streamSession) {
1206
+ if (await deliverBufferedStreamFallback({
1207
+ session: streamSession,
1208
+ err,
1209
+ payload: params.payload,
1210
+ kind: params.kind,
1211
+ textOverride: text
1212
+ })) return;
1213
+ throw err;
1214
+ }
1215
+ await deliverNormally({
1216
+ payload: params.payload,
1217
+ kind: params.kind,
1218
+ forcedThreadTs: plannedThreadTs
1219
+ });
1220
+ return;
1221
+ }
1222
+ runtime.error?.(danger(`slack-stream: streaming API call failed: ${formatSlackError(err)}, falling back`));
1223
+ streamFailed = true;
1224
+ if (streamSession && streamSession.pendingText) {
1225
+ const bufferedFallbackErr = new SlackStreamNotDeliveredError(streamSession.pendingText, "unknown");
1226
+ if (await deliverBufferedStreamFallback({
1227
+ session: streamSession,
1228
+ err: bufferedFallbackErr,
1229
+ payload: params.payload,
1230
+ kind: params.kind,
1231
+ textOverride: text
1232
+ })) return;
1233
+ }
1234
+ await deliverNormally({
1235
+ payload: params.payload,
1236
+ kind: params.kind,
1237
+ forcedThreadTs: streamSession?.threadTs ?? plannedThreadTs
1238
+ });
1239
+ }
1240
+ };
1241
+ let draftPreviewCommitted = false;
1242
+ const deliverSlackPayload = async (payload, info) => {
1243
+ if (payload.isReasoning === true) return { visibleReplySent: false };
1244
+ if (useStreaming) {
1245
+ await deliverWithStreaming({
1246
+ payload,
1247
+ kind: info.kind
1248
+ });
1249
+ return;
1250
+ }
1251
+ const reply = resolveSendableOutboundReplyParts(payload);
1252
+ const slackBlocks = readSlackReplyBlocks(payload);
1253
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
1254
+ const trimmedFinalText = (payload.text ?? ttsSupplement?.spokenText ?? "").trim();
1255
+ const shouldRestoreTtsSupplementTextForPreviewFallback = Boolean(ttsSupplement) && ttsSupplement?.visibleTextAlreadyDelivered !== true && Boolean(draftStream) && !draftPreviewCommitted && !observedFinalReplyDelivery && previewStreamingEnabled && !payload.text?.trim();
1256
+ if (info.kind === "final" && ttsSupplement && draftStream && !draftPreviewCommitted && !observedFinalReplyDelivery && previewStreamingEnabled && !payload.isError && trimmedFinalText.length > 0) {
1257
+ const channelId = draftStream.channelId();
1258
+ const messageId = draftStream.messageId();
1259
+ if (channelId && messageId) {
1260
+ const finalThreadTs = usedReplyThreadTs ?? statusThreadTs;
1261
+ await draftStream.flush();
1262
+ await draftStream.seal();
1263
+ try {
1264
+ await finalizeSlackPreviewEdit({
1265
+ client: ctx.app.client,
1266
+ token: ctx.botToken,
1267
+ accountId: account.accountId,
1268
+ channelId,
1269
+ messageId,
1270
+ text: normalizeSlackOutboundText(trimmedFinalText),
1271
+ ...slackBlocks?.length ? { blocks: slackBlocks } : {},
1272
+ threadTs: finalThreadTs
1273
+ });
1274
+ } catch (err) {
1275
+ logVerbose(`slack: preview final edit failed; falling back to standard send (${formatSlackError(err)})`);
1276
+ await draftStream.discardPending();
1277
+ let delivered = false;
1278
+ try {
1279
+ await deliverNormally({
1280
+ payload: payload.text?.trim() ? payload : {
1281
+ ...payload,
1282
+ text: trimmedFinalText
1283
+ },
1284
+ kind: info.kind,
1285
+ forcedThreadTs: finalThreadTs
1286
+ });
1287
+ delivered = true;
1288
+ } finally {
1289
+ if (delivered) await draftStream.clear();
1290
+ }
1291
+ return;
1292
+ }
1293
+ draftPreviewCommitted = true;
1294
+ observedFinalReplyDelivery = true;
1295
+ observedReplyDelivery = true;
1296
+ replyPlan.markSent();
1297
+ await deliverNormally({
1298
+ payload: buildTtsSupplementMediaPayload(payload),
1299
+ kind: info.kind,
1300
+ forcedThreadTs: finalThreadTs
1301
+ });
1302
+ deliveryTracker.markDelivered({
1303
+ kind: info.kind,
1304
+ payload,
1305
+ threadTs: finalThreadTs
1306
+ });
1307
+ return;
1308
+ }
1309
+ }
1310
+ if ((await deliverWithFinalizableLivePreviewAdapter({
1311
+ kind: info.kind,
1312
+ payload,
1313
+ adapter: defineFinalizableLivePreviewAdapter({
1314
+ draft: draftStream && !draftPreviewCommitted && !observedFinalReplyDelivery ? {
1315
+ flush: draftStream.flush,
1316
+ clear: draftStream.clear,
1317
+ discardPending: draftStream.discardPending,
1318
+ seal: draftStream.seal,
1319
+ id: () => {
1320
+ const channelId = draftStream.channelId();
1321
+ const messageId = draftStream.messageId();
1322
+ return channelId && messageId ? {
1323
+ channelId,
1324
+ messageId
1325
+ } : void 0;
1326
+ }
1327
+ } : void 0,
1328
+ buildFinalEdit: () => {
1329
+ if (!previewStreamingEnabled || reply.hasMedia && !ttsSupplement || payload.isError || trimmedFinalText.length === 0 && !slackBlocks?.length) return;
1330
+ return {
1331
+ text: normalizeSlackOutboundText(trimmedFinalText),
1332
+ blocks: slackBlocks,
1333
+ threadTs: usedReplyThreadTs ?? statusThreadTs
1334
+ };
1335
+ },
1336
+ editFinal: async (preview, edit) => {
1337
+ if (deliveryTracker.hasDelivered({
1338
+ kind: info.kind,
1339
+ payload,
1340
+ threadTs: edit.threadTs
1341
+ })) return;
1342
+ await finalizeSlackPreviewEdit({
1343
+ client: ctx.app.client,
1344
+ token: ctx.botToken,
1345
+ accountId: account.accountId,
1346
+ channelId: preview.channelId,
1347
+ messageId: preview.messageId,
1348
+ text: edit.text,
1349
+ ...edit.blocks?.length ? { blocks: edit.blocks } : {},
1350
+ threadTs: edit.threadTs
1351
+ });
1352
+ draftPreviewCommitted = true;
1353
+ observedFinalReplyDelivery = true;
1354
+ },
1355
+ onPreviewFinalized: (_preview) => {
1356
+ draftPreviewCommitted = true;
1357
+ observedFinalReplyDelivery = true;
1358
+ const finalThreadTs = usedReplyThreadTs ?? statusThreadTs;
1359
+ observedReplyDelivery = true;
1360
+ replyPlan.markSent();
1361
+ deliveryTracker.markDelivered({
1362
+ kind: info.kind,
1363
+ payload,
1364
+ threadTs: finalThreadTs
1365
+ });
1366
+ },
1367
+ buildSupplementalPayload: () => ttsSupplement ? buildTtsSupplementMediaPayload(payload) : void 0,
1368
+ deliverSupplemental: async (supplementalPayload) => {
1369
+ await deliverNormally({
1370
+ payload: supplementalPayload,
1371
+ kind: info.kind
1372
+ });
1373
+ },
1374
+ logPreviewEditFailure: (err) => {
1375
+ logVerbose(`slack: preview final edit failed; falling back to standard send (${formatSlackError(err)})`);
1376
+ }
1377
+ }),
1378
+ deliverNormally: async () => {
1379
+ await deliverNormally({
1380
+ payload: shouldRestoreTtsSupplementTextForPreviewFallback ? {
1381
+ ...payload,
1382
+ text: ttsSupplement?.spokenText
1383
+ } : payload,
1384
+ kind: info.kind
1385
+ });
1386
+ }
1387
+ })).kind === "preview-finalized") return;
1388
+ };
1389
+ const onSlackDeliveryError = (err, info) => {
1390
+ runtime.error?.(danger(`slack ${info.kind} reply failed: ${formatSlackError(err)}`));
1391
+ replyPipeline.typingCallbacks?.onIdle?.();
1392
+ };
1393
+ const draftStream = shouldUseDraftStream ? createSlackDraftStream({
1394
+ target: prepared.replyTarget,
1395
+ cfg,
1396
+ token: ctx.botToken,
1397
+ accountId: account.accountId,
1398
+ identity: slackIdentity,
1399
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1400
+ maxChars: Math.min(ctx.textLimit, SLACK_TEXT_LIMIT),
1401
+ resolveThreadTs: () => {
1402
+ const ts = replyPlan.peekThreadTs();
1403
+ if (ts) usedReplyThreadTs ??= ts;
1404
+ return ts;
1405
+ },
1406
+ log: logVerbose,
1407
+ warn: logVerbose
1408
+ }) : void 0;
1409
+ let hasStreamedMessage = false;
1410
+ const streamMode = slackStreaming.draftMode;
1411
+ const useNativeProgressStreaming = useStreaming && slackStreaming.mode === "progress";
1412
+ const previewToolProgressEnabled = (Boolean(draftStream) || useNativeProgressStreaming) && resolveChannelStreamingPreviewToolProgress(account.config);
1413
+ const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, {
1414
+ draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming,
1415
+ previewToolProgressEnabled,
1416
+ previewStreamingEnabled
1417
+ });
1418
+ let previewToolProgressSuppressed = false;
1419
+ let previewToolProgressLines = [];
1420
+ let lastNonEmptyPreviewToolProgressLines = [];
1421
+ let appendRenderedText = "";
1422
+ let appendSourceText = "";
1423
+ let reasoningProgressRawText = "";
1424
+ let statusUpdateCount = 0;
1425
+ let nativeProgressCompletionSent = false;
1426
+ let nativeProgressChunkKey;
1427
+ const progressSeed = `${account.accountId}:${message.channel}`;
1428
+ const useRichProgressDraft = streamMode === "status_final" && resolveChannelProgressDraftRender(account.config) === "rich";
1429
+ const explicitProgressTitle = resolveExplicitSlackProgressTitle(account.config);
1430
+ const progressDraftMaxLineChars = resolveChannelProgressDraftMaxLineChars(account.config);
1431
+ const renderProgressDraft = () => {
1432
+ if (!draftStream || streamMode !== "status_final") return;
1433
+ const progressLines = useRichProgressDraft && previewToolProgressLines.length === 0 ? lastNonEmptyPreviewToolProgressLines : previewToolProgressLines;
1434
+ const previewText = formatChannelProgressDraftText({
1435
+ entry: account.config,
1436
+ lines: progressLines,
1437
+ seed: progressSeed,
1438
+ formatLine: escapeSlackMrkdwn
1439
+ });
1440
+ if (!previewText) return;
1441
+ const richProgressBlocks = useRichProgressDraft ? buildSlackProgressDraftBlocks({
1442
+ title: explicitProgressTitle,
1443
+ lines: progressLines,
1444
+ maxLineChars: resolveChannelProgressDraftMaxLineChars(account.config)
1445
+ }) : void 0;
1446
+ draftStream.update(useRichProgressDraft && richProgressBlocks ? {
1447
+ text: previewText,
1448
+ blocks: richProgressBlocks
1449
+ } : previewText);
1450
+ hasStreamedMessage = true;
1451
+ };
1452
+ const waitForNativeProgressStreamStart = async () => {
1453
+ if (streamSession || !nativeProgressStreamStartPromise) return true;
1454
+ try {
1455
+ await nativeProgressStreamStartPromise;
1456
+ } catch {
1457
+ streamFailed = true;
1458
+ return false;
1459
+ }
1460
+ return !streamFailed;
1461
+ };
1462
+ const buildNativeProgressChunks = () => streamSession ? buildSlackProgressStreamUpdateChunks({
1463
+ title: explicitProgressTitle,
1464
+ lines: previewToolProgressLines,
1465
+ maxLineChars: progressDraftMaxLineChars
1466
+ }) : buildSlackProgressStreamStartChunks({
1467
+ title: explicitProgressTitle,
1468
+ lines: previewToolProgressLines,
1469
+ maxLineChars: progressDraftMaxLineChars
1470
+ });
1471
+ const markNativeProgressDelivered = (session, threadTs) => {
1472
+ if (session.delivered) observedReplyDelivery = true;
1473
+ if (threadTs) {
1474
+ usedReplyThreadTs ??= threadTs;
1475
+ rememberDeliveredThreadTs("block", threadTs);
1476
+ }
1477
+ };
1478
+ const startNativeProgressStream = async (chunks, chunkKey) => {
1479
+ const streamThreadTs = replyPlan.nextThreadTs();
1480
+ if (!streamThreadTs) {
1481
+ logVerbose("slack-stream: no reply thread target for native progress stream start, falling back");
1482
+ streamFailed = true;
1483
+ return;
1484
+ }
1485
+ nativeProgressStreamThreadTs = streamThreadTs;
1486
+ const startPromise = (async () => {
1487
+ const session = await startSlackStream({
1488
+ client: ctx.app.client,
1489
+ channel: message.channel,
1490
+ threadTs: streamThreadTs,
1491
+ chunks,
1492
+ taskDisplayMode: "plan",
1493
+ teamId: await resolveSlackStreamRecipientTeamId({
1494
+ client: ctx.app.client,
1495
+ token: ctx.botToken,
1496
+ userId: message.user,
1497
+ fallbackTeamId: ctx.teamId
1498
+ }),
1499
+ userId: message.user
1500
+ });
1501
+ streamSession = session;
1502
+ return session;
1503
+ })();
1504
+ nativeProgressStreamStartPromise = startPromise;
1505
+ let startedSession = null;
1506
+ try {
1507
+ startedSession = await startPromise;
1508
+ } finally {
1509
+ if (nativeProgressStreamStartPromise === startPromise) nativeProgressStreamStartPromise = null;
1510
+ }
1511
+ if (startedSession) markNativeProgressDelivered(startedSession, streamThreadTs);
1512
+ nativeProgressChunkKey = chunkKey;
1513
+ replyPlan.markSent();
1514
+ };
1515
+ const appendNativeProgressStream = async (chunks, chunkKey) => {
1516
+ if (!streamSession) return;
1517
+ await appendSlackStream({
1518
+ session: streamSession,
1519
+ chunks
1520
+ });
1521
+ markNativeProgressDelivered(streamSession);
1522
+ nativeProgressChunkKey = chunkKey;
1523
+ };
1524
+ const updateNativeProgressStream = async () => {
1525
+ if (!useNativeProgressStreaming || streamFailed || previewToolProgressLines.length === 0) return;
1526
+ if (!await waitForNativeProgressStreamStart()) return;
1527
+ const chunks = buildNativeProgressChunks();
1528
+ if (!chunks?.length) return;
1529
+ const chunkKey = JSON.stringify(chunks);
1530
+ if (chunkKey === nativeProgressChunkKey) return;
1531
+ try {
1532
+ if (!streamSession) {
1533
+ await startNativeProgressStream(chunks, chunkKey);
1534
+ return;
1535
+ }
1536
+ await appendNativeProgressStream(chunks, chunkKey);
1537
+ } catch (err) {
1538
+ runtime.error?.(danger(`slack-stream: native progress stream failed: ${formatSlackError(err)}, falling back`));
1539
+ streamFailed = true;
1540
+ }
1541
+ };
1542
+ const progressDraftGate = createChannelProgressDraftGate({ onStart: useNativeProgressStreaming ? updateNativeProgressStream : renderProgressDraft });
1543
+ const refreshStartedProgressDraft = async () => {
1544
+ if (useNativeProgressStreaming) await updateNativeProgressStream();
1545
+ else renderProgressDraft();
1546
+ };
1547
+ const pushPreviewToolProgress = async (line, options) => {
1548
+ if (!draftStream && !useNativeProgressStreaming) return;
1549
+ if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
1550
+ const normalized = line?.text.replace(/\s+/g, " ").trim();
1551
+ if (!line || !normalized) {
1552
+ if (streamMode !== "status_final") return;
1553
+ const alreadyStarted = progressDraftGate.hasStarted;
1554
+ await progressDraftGate.noteWork();
1555
+ if (alreadyStarted && progressDraftGate.hasStarted) await refreshStartedProgressDraft();
1556
+ return;
1557
+ }
1558
+ if (streamMode !== "status_final") {
1559
+ if (!previewToolProgressEnabled || previewToolProgressSuppressed) return;
1560
+ const nextLines = mergeChannelProgressDraftLine(previewToolProgressLines, line, { maxLines: resolveChannelProgressDraftMaxLines(account.config) });
1561
+ if (nextLines === previewToolProgressLines) return;
1562
+ previewToolProgressLines = nextLines;
1563
+ draftStream?.update(formatChannelProgressDraftText({
1564
+ entry: account.config,
1565
+ lines: previewToolProgressLines,
1566
+ seed: progressSeed,
1567
+ formatLine: escapeSlackMrkdwn
1568
+ }));
1569
+ hasStreamedMessage = true;
1570
+ return;
1571
+ }
1572
+ if (previewToolProgressEnabled && !previewToolProgressSuppressed) {
1573
+ previewToolProgressLines = mergeChannelProgressDraftLine(previewToolProgressLines, line, { maxLines: resolveChannelProgressDraftMaxLines(account.config) });
1574
+ if (previewToolProgressLines.length > 0) lastNonEmptyPreviewToolProgressLines = previewToolProgressLines;
1575
+ }
1576
+ if (useNativeProgressStreaming) {
1577
+ if (progressDraftGate.hasStarted) await updateNativeProgressStream();
1578
+ else await progressDraftGate.startNow();
1579
+ return;
1580
+ }
1581
+ const alreadyStarted = progressDraftGate.hasStarted;
1582
+ await progressDraftGate.noteWork();
1583
+ if (alreadyStarted && progressDraftGate.hasStarted) await refreshStartedProgressDraft();
1584
+ };
1585
+ const updateDraftFromPartial = (text) => {
1586
+ const trimmed = text?.trimEnd();
1587
+ if (!trimmed) return;
1588
+ if (streamMode === "append") {
1589
+ previewToolProgressSuppressed = true;
1590
+ previewToolProgressLines = [];
1591
+ const next = applyAppendOnlyStreamUpdate({
1592
+ incoming: trimmed,
1593
+ rendered: appendRenderedText,
1594
+ source: appendSourceText
1595
+ });
1596
+ appendRenderedText = next.rendered;
1597
+ appendSourceText = next.source;
1598
+ if (!next.changed) return;
1599
+ draftStream?.update(next.rendered);
1600
+ hasStreamedMessage = true;
1601
+ return;
1602
+ }
1603
+ if (streamMode === "status_final") {
1604
+ if (!progressDraftGate.hasStarted) return;
1605
+ statusUpdateCount += 1;
1606
+ if (statusUpdateCount > 1 && statusUpdateCount % 4 !== 0) return;
1607
+ renderProgressDraft();
1608
+ return;
1609
+ }
1610
+ previewToolProgressSuppressed = true;
1611
+ previewToolProgressLines = [];
1612
+ draftStream?.update(trimmed);
1613
+ hasStreamedMessage = true;
1614
+ };
1615
+ const pushReasoningProgress = async (payload) => {
1616
+ if (!payload?.text) return;
1617
+ reasoningProgressRawText = mergeSlackReasoningProgressText(reasoningProgressRawText, payload.text, { snapshot: payload.isReasoningSnapshot === true });
1618
+ const normalized = normalizeSlackReasoningProgressLine(reasoningProgressRawText);
1619
+ if (!normalized) return;
1620
+ await pushPreviewToolProgress({
1621
+ id: "reasoning",
1622
+ kind: "item",
1623
+ text: normalized,
1624
+ label: "Reasoning"
1625
+ });
1626
+ };
1627
+ const onDraftBoundary = !shouldUseDraftStream ? void 0 : async () => {
1628
+ if (hasStreamedMessage) {
1629
+ draftStream?.forceNewMessage();
1630
+ hasStreamedMessage = false;
1631
+ appendRenderedText = "";
1632
+ appendSourceText = "";
1633
+ statusUpdateCount = 0;
1634
+ }
1635
+ reasoningProgressRawText = "";
1636
+ previewToolProgressSuppressed = false;
1637
+ previewToolProgressLines = [];
1638
+ };
1639
+ let dispatchError;
1640
+ let queuedFinal = false;
1641
+ let counts = {};
1642
+ try {
1643
+ const turnResult = await dispatchChannelInboundReply({
1644
+ cfg,
1645
+ channel: "slack",
1646
+ accountId: route.accountId,
1647
+ agentId: route.agentId,
1648
+ routeSessionKey: route.sessionKey,
1649
+ storePath: prepared.turn.storePath,
1650
+ ctxPayload: prepared.ctxPayload,
1651
+ recordInboundSession,
1652
+ dispatchReplyWithBufferedBlockDispatcher,
1653
+ dispatcherOptions: {
1654
+ ...replyPipeline,
1655
+ humanDelay: resolveHumanDelayConfig(cfg, route.agentId)
1656
+ },
1657
+ delivery: {
1658
+ deliver: deliverSlackPayload,
1659
+ onError: onSlackDeliveryError
1660
+ },
1661
+ record: prepared.turn.record,
1662
+ history: prepared.turn.history,
1663
+ botLoopProtection: resolveSlackBotLoopProtection(prepared),
1664
+ replyOptions: {
1665
+ skillFilter: prepared.channelConfig?.skills,
1666
+ sourceReplyDeliveryMode,
1667
+ hasRepliedRef,
1668
+ disableBlockStreaming,
1669
+ onModelSelected,
1670
+ suppressDefaultToolProgressMessages: suppressDefaultToolProgressMessages ? true : void 0,
1671
+ onPartialReply: useStreaming ? void 0 : !previewStreamingEnabled ? void 0 : async (payload) => {
1672
+ updateDraftFromPartial(payload.text);
1673
+ },
1674
+ onAssistantMessageStart: onDraftBoundary,
1675
+ onReasoningEnd: onDraftBoundary,
1676
+ onReasoningStream: statusReactionsEnabled || previewToolProgressEnabled ? async (payload) => {
1677
+ await pushReasoningProgress(payload);
1678
+ if (!statusReactionsEnabled) return;
1679
+ await statusReactions.setThinking();
1680
+ } : void 0,
1681
+ onToolStart: async (payload) => {
1682
+ if (statusReactionsEnabled) await statusReactions.setTool(payload.name);
1683
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
1684
+ event: "tool",
1685
+ itemId: payload.itemId,
1686
+ toolCallId: payload.toolCallId,
1687
+ name: payload.name,
1688
+ phase: payload.phase,
1689
+ args: payload.args
1690
+ }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName: payload.name });
1691
+ },
1692
+ onItemEvent: async (payload) => {
1693
+ await pushPreviewToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
1694
+ event: "item",
1695
+ itemId: payload.itemId,
1696
+ itemKind: payload.kind,
1697
+ title: payload.title,
1698
+ name: payload.name,
1699
+ phase: payload.phase,
1700
+ status: payload.status,
1701
+ summary: payload.summary,
1702
+ progressText: payload.progressText,
1703
+ meta: payload.meta
1704
+ }));
1705
+ },
1706
+ onPlanUpdate: async (payload) => {
1707
+ if (payload.phase !== "update") return;
1708
+ await pushPreviewToolProgress(buildChannelProgressDraftLine({
1709
+ event: "plan",
1710
+ phase: payload.phase,
1711
+ title: payload.title,
1712
+ explanation: payload.explanation,
1713
+ steps: payload.steps
1714
+ }));
1715
+ },
1716
+ onApprovalEvent: async (payload) => {
1717
+ if (payload.phase !== "requested") return;
1718
+ await pushPreviewToolProgress(buildChannelProgressDraftLine({
1719
+ event: "approval",
1720
+ phase: payload.phase,
1721
+ title: payload.title,
1722
+ command: payload.command,
1723
+ reason: payload.reason,
1724
+ message: payload.message
1725
+ }));
1726
+ },
1727
+ onCommandOutput: async (payload) => {
1728
+ if (payload.phase !== "end") return;
1729
+ await pushPreviewToolProgress(buildChannelProgressDraftLine({
1730
+ event: "command-output",
1731
+ itemId: payload.itemId,
1732
+ toolCallId: payload.toolCallId,
1733
+ phase: payload.phase,
1734
+ title: payload.title,
1735
+ name: payload.name,
1736
+ status: payload.status,
1737
+ exitCode: payload.exitCode
1738
+ }));
1739
+ },
1740
+ onPatchSummary: async (payload) => {
1741
+ if (payload.phase !== "end") return;
1742
+ await pushPreviewToolProgress(buildChannelProgressDraftLine({
1743
+ event: "patch",
1744
+ itemId: payload.itemId,
1745
+ toolCallId: payload.toolCallId,
1746
+ phase: payload.phase,
1747
+ title: payload.title,
1748
+ name: payload.name,
1749
+ added: payload.added,
1750
+ modified: payload.modified,
1751
+ deleted: payload.deleted,
1752
+ summary: payload.summary
1753
+ }));
1754
+ }
1755
+ }
1756
+ });
1757
+ if (turnResult.dispatched) {
1758
+ const result = turnResult.dispatchResult;
1759
+ queuedFinal = result.queuedFinal;
1760
+ counts = result.counts;
1761
+ }
1762
+ } catch (err) {
1763
+ dispatchError = err;
1764
+ } finally {
1765
+ progressDraftGate.cancel();
1766
+ await draftStream?.discardPending();
1767
+ }
1768
+ let streamFallbackDelivered = false;
1769
+ const finalStream = streamSession;
1770
+ if (finalStream && !finalStream.stopped) try {
1771
+ const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent && previewToolProgressLines.length > 0 ? buildSlackProgressStreamCompletionChunks({
1772
+ title: explicitProgressTitle,
1773
+ lines: previewToolProgressLines,
1774
+ maxLineChars: progressDraftMaxLineChars,
1775
+ finalInProgressStatus: dispatchError ? "error" : "complete"
1776
+ }) : void 0;
1777
+ if (completionChunks?.length) nativeProgressCompletionSent = true;
1778
+ await stopSlackStream({
1779
+ session: finalStream,
1780
+ ...completionChunks?.length ? { chunks: completionChunks } : {},
1781
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1782
+ });
1783
+ } catch (err) {
1784
+ if (err instanceof SlackStreamNotDeliveredError) streamFallbackDelivered = await deliverPendingStreamFallback(finalStream, err);
1785
+ else runtime.error?.(danger(`slack-stream: failed to stop stream: ${formatSlackError(err)}`));
1786
+ }
1787
+ const anyReplyDelivered = hasVisibleInboundReplyDispatch({
1788
+ queuedFinal,
1789
+ counts
1790
+ }, {
1791
+ observedReplyDelivery,
1792
+ fallbackDelivered: streamFallbackDelivered
1793
+ });
1794
+ if (statusReactionsEnabled) if (dispatchError) {
1795
+ await statusReactions.setError();
1796
+ if (ctx.removeAckAfterReply) (async () => {
1797
+ await sleep(statusReactionTiming.errorHoldMs);
1798
+ if (anyReplyDelivered) await statusReactions.clear();
1799
+ })();
1800
+ } else if (anyReplyDelivered) {
1801
+ await statusReactions.setDone();
1802
+ if (ctx.removeAckAfterReply) (async () => {
1803
+ await sleep(statusReactionTiming.doneHoldMs);
1804
+ await statusReactions.clear();
1805
+ })();
1806
+ else statusReactions.restoreInitial();
1807
+ } else await statusReactions.restoreInitial();
1808
+ if (dispatchError) throw dispatchError;
1809
+ const participationThreadTs = usedReplyThreadTs ?? statusThreadTs;
1810
+ if (anyReplyDelivered && participationThreadTs) recordSlackThreadParticipation(account.accountId, message.channel, participationThreadTs, { agentId: route.agentId });
1811
+ if (!anyReplyDelivered && !draftPreviewCommitted) {
1812
+ await draftStream?.clear();
1813
+ return;
1814
+ }
1815
+ if (shouldLogVerbose()) {
1816
+ const finalCount = counts.final;
1817
+ logVerbose(`slack: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${prepared.replyTarget}`);
1818
+ }
1819
+ if (!statusReactionsEnabled) removeAckReactionAfterReply({
1820
+ removeAfterReply: ctx.removeAckAfterReply && anyReplyDelivered,
1821
+ ackReactionPromise: prepared.ackReactionPromise,
1822
+ ackReactionValue: prepared.ackReactionValue,
1823
+ remove: () => removeSlackReaction(message.channel, prepared.ackReactionMessageTs ?? "", prepared.ackReactionValue, {
1824
+ token: ctx.botToken,
1825
+ client: ctx.app.client
1826
+ }),
1827
+ onError: (err) => {
1828
+ logAckFailure({
1829
+ log: logVerbose,
1830
+ channel: "slack",
1831
+ target: `${message.channel}/${message.ts}`,
1832
+ error: err
1833
+ });
1834
+ }
1835
+ });
1836
+ }
1837
+ //#endregion
1838
+ //#region extensions/slack/src/monitor/message-handler/prepare-content.ts
1839
+ const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4;
1840
+ const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20;
1841
+ const SLACK_USER_MENTION_RE$1 = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
1842
+ let slackMediaModulePromise$1;
1843
+ function loadSlackMediaModule$1() {
1844
+ slackMediaModulePromise$1 ??= import("./actions-zfVWcIY6.js").then((n) => n.h);
1845
+ return slackMediaModulePromise$1;
1846
+ }
1847
+ function collectUniqueSlackMentionIds$1(texts) {
1848
+ const seen = /* @__PURE__ */ new Set();
1849
+ const mentionIds = [];
1850
+ for (const text of texts) {
1851
+ if (!text) continue;
1852
+ SLACK_USER_MENTION_RE$1.lastIndex = 0;
1853
+ for (const match of text.matchAll(SLACK_USER_MENTION_RE$1)) {
1854
+ const userId = match[1];
1855
+ if (!userId || seen.has(userId)) continue;
1856
+ seen.add(userId);
1857
+ mentionIds.push(userId);
1858
+ }
1859
+ }
1860
+ return mentionIds;
1861
+ }
1862
+ function renderSlackUserMentions(text, renderedMentions) {
1863
+ if (!text || renderedMentions.size === 0) return text;
1864
+ SLACK_USER_MENTION_RE$1.lastIndex = 0;
1865
+ return text.replace(SLACK_USER_MENTION_RE$1, (full, userId) => {
1866
+ return renderedMentions.get(userId) ?? full;
1867
+ });
1868
+ }
1869
+ function readTextObject(value) {
1870
+ if (!value || typeof value !== "object") return;
1871
+ return normalizeOptionalString(readStringValue(value.text));
1872
+ }
1873
+ function renderSlackRichTextLeaf(element) {
1874
+ switch (element.type) {
1875
+ case "text": return readStringValue(element.text) ?? "";
1876
+ case "link": return readStringValue(element.text) ?? readStringValue(element.url) ?? "";
1877
+ case "user": {
1878
+ const userId = readStringValue(element.user_id);
1879
+ return userId ? `<@${userId}>` : "";
1880
+ }
1881
+ case "channel": {
1882
+ const channelId = readStringValue(element.channel_id);
1883
+ return channelId ? `<#${channelId}>` : "";
1884
+ }
1885
+ case "usergroup": {
1886
+ const usergroupId = readStringValue(element.usergroup_id);
1887
+ return usergroupId ? `<!subteam^${usergroupId}>` : "";
1888
+ }
1889
+ case "broadcast": {
1890
+ const range = readStringValue(element.range);
1891
+ return range ? `<!${range}>` : "";
1892
+ }
1893
+ case "emoji": {
1894
+ const name = readStringValue(element.name);
1895
+ return name ? `:${name}:` : "";
1896
+ }
1897
+ default: return "";
1898
+ }
1899
+ }
1900
+ function renderSlackRichTextElements(elements) {
1901
+ if (!Array.isArray(elements)) return "";
1902
+ const parts = [];
1903
+ for (const rawElement of elements) {
1904
+ if (!rawElement || typeof rawElement !== "object") continue;
1905
+ const element = rawElement;
1906
+ switch (element.type) {
1907
+ case "rich_text_section":
1908
+ case "rich_text_preformatted":
1909
+ case "rich_text_quote":
1910
+ parts.push(renderSlackRichTextElements(element.elements));
1911
+ break;
1912
+ case "rich_text_list": {
1913
+ const listParts = [];
1914
+ if (Array.isArray(element.elements)) for (const child of element.elements) {
1915
+ if (!child || typeof child !== "object") continue;
1916
+ const rendered = renderSlackRichTextElements(child.elements);
1917
+ if (rendered) listParts.push(rendered);
1918
+ }
1919
+ const listText = listParts.join("\n");
1920
+ parts.push(listText);
1921
+ break;
1922
+ }
1923
+ default:
1924
+ parts.push(renderSlackRichTextLeaf(element));
1925
+ break;
1926
+ }
1927
+ }
1928
+ return parts.join("");
1929
+ }
1930
+ function readSlackBlockText(block) {
1931
+ if (!block || typeof block !== "object") return;
1932
+ const blockLike = block;
1933
+ switch (blockLike.type) {
1934
+ case "rich_text": return normalizeOptionalString(renderSlackRichTextElements(blockLike.elements));
1935
+ case "section": {
1936
+ const text = readTextObject(blockLike.text);
1937
+ if (text) return text;
1938
+ if (Array.isArray(blockLike.fields)) {
1939
+ const fields = [];
1940
+ for (const field of blockLike.fields) {
1941
+ const fieldText = readTextObject(field);
1942
+ if (fieldText) fields.push(fieldText);
1943
+ }
1944
+ return fields.length > 0 ? fields.join("\n") : void 0;
1945
+ }
1946
+ return;
1947
+ }
1948
+ case "header": return readTextObject(blockLike.text);
1949
+ case "context": {
1950
+ if (!Array.isArray(blockLike.elements)) return;
1951
+ const parts = [];
1952
+ for (const element of blockLike.elements) {
1953
+ const text = readTextObject(element);
1954
+ if (text) parts.push(text);
1955
+ }
1956
+ return parts.length > 0 ? parts.join(" ") : void 0;
1957
+ }
1958
+ case "image": return normalizeOptionalString(readStringValue(blockLike.alt_text)) ?? readTextObject(blockLike.title);
1959
+ case "video": return readTextObject(blockLike.title) ?? normalizeOptionalString(readStringValue(blockLike.alt_text));
1960
+ default: return;
1961
+ }
1962
+ }
1963
+ function resolveSlackBlocksText(blocks) {
1964
+ if (!blocks?.length) return;
1965
+ const parts = [];
1966
+ let hasRichText = false;
1967
+ for (const block of blocks) {
1968
+ if (block && typeof block === "object" && block.type === "rich_text") hasRichText = true;
1969
+ const text = readSlackBlockText(block);
1970
+ if (text) parts.push(text);
1971
+ }
1972
+ return parts.length > 0 ? {
1973
+ text: parts.join("\n"),
1974
+ hasRichText
1975
+ } : void 0;
1976
+ }
1977
+ function chooseSlackPrimaryText(params) {
1978
+ const { messageText, blocksText } = params;
1979
+ if (!blocksText) return messageText;
1980
+ if (!messageText) return blocksText.text;
1981
+ if (blocksText.hasRichText && blocksText.text.length > messageText.length) return blocksText.text;
1982
+ return blocksText.text.length > messageText.length && blocksText.text.startsWith(messageText) ? blocksText.text : messageText;
1983
+ }
1984
+ function filterInheritedParentFiles(params) {
1985
+ const { files, isThreadReply, threadStarter } = params;
1986
+ if (!isThreadReply || !files?.length) return files;
1987
+ if (!threadStarter?.files?.length) return files;
1988
+ const starterFileIds = new Set(threadStarter.files.map((file) => file.id));
1989
+ const filtered = files.filter((file) => !file.id || !starterFileIds.has(file.id));
1990
+ if (filtered.length < files.length) logVerbose(`slack: filtered ${files.length - filtered.length} inherited parent file(s) from thread reply`);
1991
+ return filtered.length > 0 ? filtered : void 0;
1992
+ }
1993
+ async function resolveSlackMessageContent(params) {
1994
+ const ownFiles = filterInheritedParentFiles({
1995
+ files: params.message.files,
1996
+ isThreadReply: params.isThreadReply,
1997
+ threadStarter: params.threadStarter
1998
+ });
1999
+ const mediaPromise = ownFiles && ownFiles.length > 0 ? loadSlackMediaModule$1().then(({ resolveSlackMedia }) => resolveSlackMedia({
2000
+ files: ownFiles,
2001
+ client: params.client,
2002
+ token: params.botToken,
2003
+ maxBytes: params.mediaMaxBytes,
2004
+ readIdleTimeoutMs: params.mediaReadIdleTimeoutMs,
2005
+ totalTimeoutMs: params.mediaTotalTimeoutMs,
2006
+ abortSignal: params.abortSignal
2007
+ })) : Promise.resolve(null);
2008
+ const attachmentContentPromise = params.message.attachments && params.message.attachments.length > 0 ? loadSlackMediaModule$1().then(({ resolveSlackAttachmentContent }) => resolveSlackAttachmentContent({
2009
+ attachments: params.message.attachments,
2010
+ client: params.client,
2011
+ token: params.botToken,
2012
+ maxBytes: params.mediaMaxBytes,
2013
+ readIdleTimeoutMs: params.mediaReadIdleTimeoutMs,
2014
+ totalTimeoutMs: params.mediaTotalTimeoutMs,
2015
+ abortSignal: params.abortSignal
2016
+ })) : Promise.resolve(null);
2017
+ const [media, attachmentContent] = await Promise.all([mediaPromise, attachmentContentPromise]);
2018
+ const mergedMedia = [...media ?? [], ...attachmentContent?.media ?? []];
2019
+ const effectiveDirectMedia = mergedMedia.length > 0 ? mergedMedia : null;
2020
+ const mediaPlaceholder = effectiveDirectMedia ? effectiveDirectMedia.map((item) => item.placeholder).join(" ") : void 0;
2021
+ const fallbackFiles = ownFiles ?? [];
2022
+ const fileOnlyFallback = !mediaPlaceholder && fallbackFiles.length > 0 ? fallbackFiles.slice(0, 8).map((file) => formatSlackFileReference(file)).join(", ") : void 0;
2023
+ const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : void 0;
2024
+ let botAttachmentText;
2025
+ if (params.isBotMessage && !attachmentContent?.text) {
2026
+ const botAttachmentTextParts = [];
2027
+ for (const attachment of params.message.attachments ?? []) {
2028
+ const text = normalizeOptionalString(attachment.text) ?? normalizeOptionalString(attachment.fallback);
2029
+ if (text) botAttachmentTextParts.push(text);
2030
+ }
2031
+ botAttachmentText = botAttachmentTextParts.length > 0 ? botAttachmentTextParts.join("\n") : void 0;
2032
+ }
2033
+ const blocksText = resolveSlackBlocksText(params.message.blocks);
2034
+ const textParts = [
2035
+ chooseSlackPrimaryText({
2036
+ messageText: normalizeOptionalString(params.message.text),
2037
+ blocksText
2038
+ }),
2039
+ attachmentContent?.text,
2040
+ botAttachmentText
2041
+ ];
2042
+ const renderedMentions = /* @__PURE__ */ new Map();
2043
+ const resolveUserName = params.resolveUserName;
2044
+ if (resolveUserName) {
2045
+ const mentionIds = collectUniqueSlackMentionIds$1(textParts);
2046
+ const lookupIds = mentionIds.slice(0, SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE);
2047
+ const skippedLookups = mentionIds.length - lookupIds.length;
2048
+ if (skippedLookups > 0) logVerbose(`slack: skipping ${skippedLookups} mention lookup(s) beyond per-message cap (${SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE})`);
2049
+ const { results } = await runTasksWithConcurrency({
2050
+ tasks: lookupIds.map((userId) => async () => {
2051
+ const renderedName = normalizeOptionalString((await resolveUserName(userId))?.name);
2052
+ return {
2053
+ userId,
2054
+ rendered: renderedName ? `<@${userId}> (${renderedName})` : null
2055
+ };
2056
+ }),
2057
+ limit: SLACK_MENTION_RESOLUTION_CONCURRENCY
2058
+ });
2059
+ for (const result of results) {
2060
+ if (!result) continue;
2061
+ renderedMentions.set(result.userId, result.rendered);
2062
+ }
2063
+ }
2064
+ const rawBody = [
2065
+ renderSlackUserMentions(textParts[0], renderedMentions),
2066
+ renderSlackUserMentions(textParts[1], renderedMentions),
2067
+ renderSlackUserMentions(textParts[2], renderedMentions),
2068
+ mediaPlaceholder,
2069
+ fileOnlyPlaceholder
2070
+ ].filter(Boolean).join("\n") || "";
2071
+ if (!rawBody) return null;
2072
+ return {
2073
+ rawBody,
2074
+ effectiveDirectMedia
2075
+ };
2076
+ }
2077
+ //#endregion
2078
+ //#region extensions/slack/src/monitor/message-handler/prepare-dm-history.ts
2079
+ function resolveSlackDmHistoryLimit(params) {
2080
+ const override = params.userId && params.account.config.dms?.[params.userId]?.historyLimit !== void 0 ? params.account.config.dms[params.userId]?.historyLimit : void 0;
2081
+ return Math.max(0, override ?? params.defaultLimit);
2082
+ }
2083
+ async function resolveSlackDmHistoryContext(params) {
2084
+ const maxMessages = Math.max(0, Math.floor(params.limit));
2085
+ if (maxMessages <= 0) return {
2086
+ body: void 0,
2087
+ inboundHistory: void 0
2088
+ };
2089
+ try {
2090
+ const messages = ((await params.ctx.app.client.conversations.history({
2091
+ token: params.ctx.botToken,
2092
+ channel: params.channelId,
2093
+ ...params.currentMessageTs ? {
2094
+ latest: params.currentMessageTs,
2095
+ inclusive: true
2096
+ } : {},
2097
+ limit: maxMessages + 1
2098
+ })).messages ?? []).filter((message) => {
2099
+ if (params.currentMessageTs && message.ts === params.currentMessageTs) return false;
2100
+ return Boolean(normalizeOptionalString(message.text));
2101
+ }).slice(0, maxMessages).toReversed();
2102
+ if (messages.length === 0) return {
2103
+ body: void 0,
2104
+ inboundHistory: void 0
2105
+ };
2106
+ const userNames = /* @__PURE__ */ new Map();
2107
+ const resolveUserLabel = async (userId) => {
2108
+ const cached = userNames.get(userId);
2109
+ if (cached) return cached;
2110
+ const label = normalizeOptionalString((await params.ctx.resolveUserName(userId)).name) ?? userId;
2111
+ userNames.set(userId, label);
2112
+ return label;
2113
+ };
2114
+ const entries = [];
2115
+ const formatted = [];
2116
+ for (const message of messages) {
2117
+ const body = normalizeOptionalString(message.text);
2118
+ if (!body) continue;
2119
+ const isCurrentBot = params.ctx.botUserId && message.user === params.ctx.botUserId || params.ctx.botId && message.bot_id === params.ctx.botId;
2120
+ const role = isCurrentBot || message.bot_id ? "assistant" : "user";
2121
+ const sender = `${isCurrentBot ? "Assistant" : message.user ? await resolveUserLabel(message.user) : normalizeOptionalString(message.username) ?? (message.bot_id ? "Bot" : "Unknown")} (${role})`;
2122
+ const timestamp = message.ts ? Math.round(Number(message.ts) * 1e3) : void 0;
2123
+ entries.push({
2124
+ sender,
2125
+ body,
2126
+ timestamp
2127
+ });
2128
+ formatted.push(formatInboundEnvelope({
2129
+ channel: "Slack",
2130
+ from: sender,
2131
+ timestamp,
2132
+ body: `${body}\n[slack message id: ${message.ts ?? "unknown"} channel: ${params.channelId}]`,
2133
+ chatType: "direct",
2134
+ envelope: params.envelopeOptions
2135
+ }));
2136
+ }
2137
+ return {
2138
+ body: formatted.length > 0 ? formatted.join("\n\n") : void 0,
2139
+ inboundHistory: entries.length > 0 ? entries : void 0
2140
+ };
2141
+ } catch (err) {
2142
+ logVerbose(`slack: failed to fetch DM history for channel ${params.channelId}: ${formatErrorMessage(err)}`);
2143
+ return {
2144
+ body: void 0,
2145
+ inboundHistory: void 0
2146
+ };
2147
+ }
2148
+ }
2149
+ //#endregion
2150
+ //#region extensions/slack/src/monitor/message-handler/prepare-routing.ts
2151
+ const slackRouteBindingConfigCache = /* @__PURE__ */ new WeakMap();
2152
+ function slackTargetDefaultKindForPeer(kind) {
2153
+ return kind === "direct" ? "user" : "channel";
2154
+ }
2155
+ function slackTargetKindMatchesPeer(peerKind, targetKind) {
2156
+ if (targetKind === "user") return peerKind === "direct";
2157
+ return peerKind === "channel" || peerKind === "group";
2158
+ }
2159
+ function normalizeSlackRouteBindingPeer(peer) {
2160
+ const rawId = peer.id.trim();
2161
+ if (!rawId || rawId === "*") return peer;
2162
+ const target = (() => {
2163
+ try {
2164
+ return parseSlackTarget(rawId, { defaultKind: slackTargetDefaultKindForPeer(peer.kind) });
2165
+ } catch {
2166
+ return;
2167
+ }
2168
+ })();
2169
+ if (!target || !slackTargetKindMatchesPeer(peer.kind, target.kind) || target.id === peer.id) return peer;
2170
+ return {
2171
+ ...peer,
2172
+ id: target.id
2173
+ };
2174
+ }
2175
+ function normalizeSlackRouteBindingConfig(cfg) {
2176
+ const bindings = cfg.bindings;
2177
+ const cached = slackRouteBindingConfigCache.get(cfg);
2178
+ if (cached && cached.bindingsRef === bindings) return cached.normalizedCfg;
2179
+ if (!Array.isArray(bindings)) return cfg;
2180
+ let changed = false;
2181
+ const normalizedBindings = bindings.map((binding) => {
2182
+ if (binding.type === "acp" || binding.match.channel.trim().toLowerCase() !== "slack") return binding;
2183
+ const peer = binding.match.peer;
2184
+ if (!peer) return binding;
2185
+ const normalizedPeer = normalizeSlackRouteBindingPeer(peer);
2186
+ if (normalizedPeer === peer) return binding;
2187
+ changed = true;
2188
+ return {
2189
+ ...binding,
2190
+ match: {
2191
+ ...binding.match,
2192
+ peer: normalizedPeer
2193
+ }
2194
+ };
2195
+ });
2196
+ const normalizedCfg = changed ? {
2197
+ ...cfg,
2198
+ bindings: normalizedBindings
2199
+ } : cfg;
2200
+ slackRouteBindingConfigCache.set(cfg, {
2201
+ bindingsRef: bindings,
2202
+ normalizedCfg
2203
+ });
2204
+ return normalizedCfg;
2205
+ }
2206
+ function resolveSlackBaseConversationId(params) {
2207
+ return params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel;
2208
+ }
2209
+ function resolveSlackInitialAgentRoute(params) {
2210
+ return resolveAgentRoute({
2211
+ cfg: normalizeSlackRouteBindingConfig(params.ctx.cfg),
2212
+ channel: "slack",
2213
+ accountId: params.account.accountId,
2214
+ teamId: params.ctx.teamId || void 0,
2215
+ peer: {
2216
+ kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
2217
+ id: params.isDirectMessage ? params.message.user ?? "unknown" : params.message.channel
2218
+ }
2219
+ });
2220
+ }
2221
+ function resolveSlackRoutingContext(params) {
2222
+ const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish, seedTopLevelRoomThread, assistantThreadTs } = params;
2223
+ let route = resolveSlackInitialAgentRoute({
2224
+ ctx,
2225
+ account,
2226
+ message,
2227
+ isDirectMessage,
2228
+ isRoom
2229
+ });
2230
+ const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";
2231
+ const replyToMode = resolveSlackReplyToMode(account, chatType);
2232
+ const threadContext = resolveSlackThreadContext({
2233
+ message,
2234
+ replyToMode
2235
+ });
2236
+ const threadTs = threadContext.incomingThreadTs;
2237
+ const isThreadReply = threadContext.isThreadReply;
2238
+ const autoThreadId = !isThreadReply && replyToMode === "all" && threadContext.messageTs ? threadContext.messageTs : void 0;
2239
+ const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
2240
+ const routedThreadId = (isDirectMessage ? assistantThreadTs : isRoomish ? isThreadReply && threadTs ? threadTs : void 0 : isThreadReply ? threadTs : autoThreadId) ?? (isRoomish ? !isThreadReply && isRoom && seedTopLevelRoomThread && replyToMode !== "off" && seedCandidateThreadId ? seedCandidateThreadId : void 0 : void 0);
2241
+ const baseConversationId = resolveSlackBaseConversationId({
2242
+ message,
2243
+ isDirectMessage
2244
+ });
2245
+ const runtimeBindingThreadId = routedThreadId ?? (isDirectMessage && isThreadReply ? threadTs : void 0);
2246
+ const boundThreadRoute = runtimeBindingThreadId ? resolveRuntimeConversationBindingRoute({
2247
+ route,
2248
+ conversation: {
2249
+ channel: "slack",
2250
+ accountId: account.accountId,
2251
+ conversationId: runtimeBindingThreadId,
2252
+ parentConversationId: baseConversationId
2253
+ }
2254
+ }) : null;
2255
+ const runtimeRoute = boundThreadRoute?.boundSessionKey || boundThreadRoute?.bindingRecord ? boundThreadRoute : resolveRuntimeConversationBindingRoute({
2256
+ route,
2257
+ conversation: {
2258
+ channel: "slack",
2259
+ accountId: account.accountId,
2260
+ conversationId: baseConversationId
2261
+ }
2262
+ });
2263
+ let configuredBinding = null;
2264
+ let configuredBindingSessionKey = "";
2265
+ if (runtimeRoute.boundSessionKey || runtimeRoute.bindingRecord) route = runtimeRoute.route;
2266
+ else {
2267
+ const configuredRoute = resolveConfiguredBindingRoute({
2268
+ cfg: ctx.cfg,
2269
+ route,
2270
+ conversation: {
2271
+ channel: "slack",
2272
+ accountId: account.accountId,
2273
+ conversationId: baseConversationId
2274
+ }
2275
+ });
2276
+ configuredBinding = configuredRoute.bindingResolution;
2277
+ configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
2278
+ route = configuredRoute.route;
2279
+ }
2280
+ const threadKeys = runtimeRoute.boundSessionKey || configuredBindingSessionKey ? {
2281
+ sessionKey: route.sessionKey,
2282
+ parentSessionKey: void 0
2283
+ } : resolveThreadSessionKeys({
2284
+ baseSessionKey: route.sessionKey,
2285
+ threadId: routedThreadId,
2286
+ parentSessionKey: routedThreadId && ctx.threadInheritParent ? route.sessionKey : void 0
2287
+ });
2288
+ const sessionKey = threadKeys.sessionKey;
2289
+ const historyKey = isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
2290
+ return {
2291
+ route,
2292
+ runtimeBinding: runtimeRoute.bindingRecord,
2293
+ runtimeBoundSessionKey: runtimeRoute.boundSessionKey,
2294
+ configuredBinding,
2295
+ configuredBindingSessionKey,
2296
+ chatType,
2297
+ replyToMode,
2298
+ threadContext,
2299
+ threadTs,
2300
+ isThreadReply,
2301
+ threadKeys,
2302
+ sessionKey,
2303
+ historyKey
2304
+ };
2305
+ }
2306
+ //#endregion
2307
+ //#region extensions/slack/src/monitor/message-handler/prepare-thread-context-root.ts
2308
+ function isSlackThreadAuthorCurrentBot(params) {
2309
+ const { identity, author } = params;
2310
+ if (identity.botUserId && author.userId && author.userId === identity.botUserId) return true;
2311
+ if (identity.botId && author.botId && author.botId === identity.botId) return true;
2312
+ return false;
2313
+ }
2314
+ function resolveSlackThreadHistoryFilterPolicy(params) {
2315
+ if (!params.includeBotStarterAsRootContext || !params.starterTs) return {};
2316
+ return { retainCurrentBotRootTs: params.starterTs };
2317
+ }
2318
+ function applySlackThreadHistoryFilterPolicy(params) {
2319
+ const kept = [];
2320
+ let omittedCurrentBot = 0;
2321
+ for (const entry of params.history) {
2322
+ if (!isSlackThreadAuthorCurrentBot({
2323
+ identity: params.identity,
2324
+ author: entry
2325
+ })) {
2326
+ kept.push(entry);
2327
+ continue;
2328
+ }
2329
+ if (params.policy.retainCurrentBotRootTs && entry.ts === params.policy.retainCurrentBotRootTs) kept.push(entry);
2330
+ else omittedCurrentBot += 1;
2331
+ }
2332
+ return {
2333
+ kept,
2334
+ omittedCurrentBot
2335
+ };
2336
+ }
2337
+ function shouldIncludeBotThreadStarterContext(params) {
2338
+ if (!params.hasStarterText) return false;
2339
+ return params.starterIsCurrentBot && params.isNewThreadSession;
2340
+ }
2341
+ function ensureSlackThreadHistoryHasBotRoot(params) {
2342
+ if (!params.includeBotStarterAsRootContext || !params.threadStarter?.text) return params.history;
2343
+ if (params.history.some((entry) => entry.ts === params.threadStarter?.ts)) return params.history;
2344
+ return [params.threadStarter, ...params.history];
2345
+ }
2346
+ function formatSlackBotStarterThreadLabel(params) {
2347
+ const base = `Slack thread ${params.roomLabel}`;
2348
+ if (!params.starterText) return base;
2349
+ const snippet = params.starterText.replace(/\s+/g, " ").slice(0, 80).trim();
2350
+ if (!snippet) return base;
2351
+ return `${base} (assistant root): ${snippet}`;
2352
+ }
2353
+ //#endregion
2354
+ //#region extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
2355
+ let slackMediaModulePromise;
2356
+ function loadSlackMediaModule() {
2357
+ slackMediaModulePromise ??= import("./actions-zfVWcIY6.js").then((n) => n.h);
2358
+ return slackMediaModulePromise;
2359
+ }
2360
+ const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
2361
+ function isSlackThreadContextSenderAllowed(params) {
2362
+ if (params.allowFromLower.length === 0 || params.botId) return true;
2363
+ if (!params.userId) return false;
2364
+ return resolveSlackAllowListMatch({
2365
+ allowList: params.allowFromLower,
2366
+ id: params.userId,
2367
+ name: params.userName,
2368
+ allowNameMatching: params.allowNameMatching
2369
+ }).allowed;
2370
+ }
2371
+ async function resolveSlackThreadUserMap(params) {
2372
+ const uniqueUserIds = [];
2373
+ const seen = /* @__PURE__ */ new Set();
2374
+ for (const item of params.messages) {
2375
+ if (!item.userId || seen.has(item.userId)) continue;
2376
+ seen.add(item.userId);
2377
+ uniqueUserIds.push(item.userId);
2378
+ }
2379
+ const userMap = /* @__PURE__ */ new Map();
2380
+ if (uniqueUserIds.length === 0) return userMap;
2381
+ const { results } = await runTasksWithConcurrency({
2382
+ tasks: uniqueUserIds.map((id) => async () => {
2383
+ const user = await params.ctx.resolveUserName(id);
2384
+ return user ? {
2385
+ id,
2386
+ user
2387
+ } : null;
2388
+ }),
2389
+ limit: SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY
2390
+ });
2391
+ for (const result of results) if (result) userMap.set(result.id, result.user);
2392
+ return userMap;
2393
+ }
2394
+ async function resolveSlackThreadContextData(params) {
2395
+ const botIdentity = {
2396
+ botUserId: params.ctx.botUserId,
2397
+ botId: params.ctx.botId
2398
+ };
2399
+ const isCurrentBotAuthor = (author) => isSlackThreadAuthorCurrentBot({
2400
+ identity: botIdentity,
2401
+ author
2402
+ });
2403
+ let threadStarterBody;
2404
+ let threadHistoryBody;
2405
+ let threadSessionPreviousTimestamp;
2406
+ let threadLabel;
2407
+ let threadStarterMedia = null;
2408
+ if (!params.isThreadReply || !params.threadTs) return {
2409
+ threadStarterBody,
2410
+ threadHistoryBody,
2411
+ threadSessionPreviousTimestamp,
2412
+ threadLabel,
2413
+ threadStarterMedia
2414
+ };
2415
+ const starter = params.threadStarter;
2416
+ const starterSenderName = params.allowNameMatching && params.allowFromLower.length > 0 && starter?.userId ? (await params.ctx.resolveUserName(starter.userId))?.name : void 0;
2417
+ const starterIsCurrentBot = Boolean(starter && isCurrentBotAuthor({
2418
+ userId: starter.userId,
2419
+ botId: starter.botId
2420
+ }));
2421
+ const starterAllowed = !starter || !starterIsCurrentBot && isSlackThreadContextSenderAllowed({
2422
+ allowFromLower: params.allowFromLower,
2423
+ allowNameMatching: params.allowNameMatching,
2424
+ userId: starter.userId,
2425
+ userName: starterSenderName,
2426
+ botId: starter.botId
2427
+ });
2428
+ const includeStarterContext = !starter || !starterIsCurrentBot && shouldIncludeSupplementalContext({
2429
+ mode: params.contextVisibilityMode,
2430
+ kind: "thread",
2431
+ senderAllowed: starterAllowed
2432
+ });
2433
+ if (starter?.text && includeStarterContext) {
2434
+ threadStarterBody = starter.text;
2435
+ const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
2436
+ threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`;
2437
+ if (!params.effectiveDirectMedia && starter.files && starter.files.length > 0) {
2438
+ const { resolveSlackMedia } = await loadSlackMediaModule();
2439
+ threadStarterMedia = await resolveSlackMedia({
2440
+ files: starter.files,
2441
+ client: params.ctx.app.client,
2442
+ token: params.ctx.botToken,
2443
+ maxBytes: params.ctx.mediaMaxBytes
2444
+ });
2445
+ if (threadStarterMedia) logVerbose(`slack: hydrated thread starter file ${threadStarterMedia.map((item) => item.placeholder).join(", ")} from root message`);
2446
+ }
2447
+ } else threadLabel = `Slack thread ${params.roomLabel}`;
2448
+ threadSessionPreviousTimestamp = readSessionUpdatedAt({
2449
+ storePath: params.storePath,
2450
+ sessionKey: params.sessionKey
2451
+ });
2452
+ const includeBotStarterAsRootContext = shouldIncludeBotThreadStarterContext({
2453
+ starterIsCurrentBot,
2454
+ isNewThreadSession: !threadSessionPreviousTimestamp,
2455
+ hasStarterText: Boolean(starter?.text)
2456
+ });
2457
+ if (starter?.text && starterIsCurrentBot && !includeBotStarterAsRootContext) logVerbose("slack: omitted current-bot thread starter from context");
2458
+ else if (starter?.text && !includeStarterContext && !starterIsCurrentBot) logVerbose(`slack: omitted thread starter from context (mode=${params.contextVisibilityMode}, sender_allowed=${starterAllowed ? "yes" : "no"})`);
2459
+ else if (includeBotStarterAsRootContext) {
2460
+ threadLabel = formatSlackBotStarterThreadLabel({
2461
+ roomLabel: params.roomLabel,
2462
+ starterText: starter?.text
2463
+ });
2464
+ logVerbose("slack: retained current-bot thread starter as assistant root context");
2465
+ }
2466
+ const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20;
2467
+ if (threadInitialHistoryLimit > 0 && (!threadSessionPreviousTimestamp || params.forceInitialHistory)) {
2468
+ const currentBotRootTs = starter?.ts ?? params.threadTs;
2469
+ const threadHistoryWithBotRoot = ensureSlackThreadHistoryHasBotRoot({
2470
+ history: await resolveSlackThreadHistory({
2471
+ channelId: params.message.channel,
2472
+ threadTs: params.threadTs,
2473
+ client: params.ctx.app.client,
2474
+ currentMessageTs: params.message.ts,
2475
+ limit: threadInitialHistoryLimit
2476
+ }),
2477
+ includeBotStarterAsRootContext,
2478
+ threadStarter: starter ? {
2479
+ ...starter,
2480
+ ts: currentBotRootTs
2481
+ } : null
2482
+ });
2483
+ if (threadHistoryWithBotRoot.length > 0) {
2484
+ const { kept: threadHistoryWithoutCurrentBot, omittedCurrentBot: omittedCurrentBotHistoryCount } = applySlackThreadHistoryFilterPolicy({
2485
+ history: threadHistoryWithBotRoot,
2486
+ policy: resolveSlackThreadHistoryFilterPolicy({
2487
+ includeBotStarterAsRootContext,
2488
+ starterTs: currentBotRootTs
2489
+ }),
2490
+ identity: botIdentity
2491
+ });
2492
+ const userMapForFilter = params.contextVisibilityMode !== "all" && params.allowNameMatching && params.allowFromLower.length > 0 ? await resolveSlackThreadUserMap({
2493
+ ctx: params.ctx,
2494
+ messages: threadHistoryWithoutCurrentBot
2495
+ }) : /* @__PURE__ */ new Map();
2496
+ const { items: filteredThreadHistory, omitted: omittedHistoryCount } = params.contextVisibilityMode === "all" ? {
2497
+ items: threadHistoryWithoutCurrentBot,
2498
+ omitted: 0
2499
+ } : filterSupplementalContextItems({
2500
+ items: threadHistoryWithoutCurrentBot,
2501
+ mode: params.contextVisibilityMode,
2502
+ kind: "thread",
2503
+ isSenderAllowed: (historyMsg) => {
2504
+ if (isCurrentBotAuthor({
2505
+ userId: historyMsg.userId,
2506
+ botId: historyMsg.botId
2507
+ })) return true;
2508
+ const msgUser = historyMsg.userId ? userMapForFilter.get(historyMsg.userId) : null;
2509
+ return isSlackThreadContextSenderAllowed({
2510
+ allowFromLower: params.allowFromLower,
2511
+ allowNameMatching: params.allowNameMatching,
2512
+ userId: historyMsg.userId,
2513
+ userName: msgUser?.name,
2514
+ botId: historyMsg.botId
2515
+ });
2516
+ }
2517
+ });
2518
+ const userMap = await resolveSlackThreadUserMap({
2519
+ ctx: params.ctx,
2520
+ messages: filteredThreadHistory
2521
+ });
2522
+ if (omittedHistoryCount > 0 || omittedCurrentBotHistoryCount > 0) logVerbose(`slack: omitted ${omittedHistoryCount + omittedCurrentBotHistoryCount} thread message(s) from context (mode=${params.contextVisibilityMode})`);
2523
+ const historyParts = [];
2524
+ for (const historyMsg of filteredThreadHistory) {
2525
+ const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
2526
+ const isOtherBot = Boolean(historyMsg.botId) && historyMsg.botId !== params.ctx.botId;
2527
+ const isCurrentBot = isCurrentBotAuthor({
2528
+ userId: historyMsg.userId,
2529
+ botId: historyMsg.botId
2530
+ });
2531
+ const role = isCurrentBot || isOtherBot || Boolean(historyMsg.botId) ? "assistant" : "user";
2532
+ const msgSenderName = isCurrentBot ? "Bot (this assistant)" : msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown");
2533
+ const msgWithId = `${historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${params.message.channel}]`;
2534
+ historyParts.push(formatInboundEnvelope({
2535
+ channel: "Slack",
2536
+ from: `${msgSenderName} (${role})`,
2537
+ timestamp: historyMsg.ts ? Math.round(Number(historyMsg.ts) * 1e3) : void 0,
2538
+ body: msgWithId,
2539
+ chatType: "channel",
2540
+ envelope: params.envelopeOptions
2541
+ }));
2542
+ }
2543
+ if (historyParts.length > 0) {
2544
+ threadHistoryBody = historyParts.join("\n\n");
2545
+ logVerbose(`slack: populated thread history with ${filteredThreadHistory.length} messages for new session`);
2546
+ }
2547
+ }
2548
+ }
2549
+ return {
2550
+ threadStarterBody,
2551
+ threadHistoryBody,
2552
+ threadSessionPreviousTimestamp,
2553
+ threadLabel,
2554
+ threadStarterMedia
2555
+ };
2556
+ }
2557
+ //#endregion
2558
+ //#region extensions/slack/src/monitor/message-handler/subteam-mentions.ts
2559
+ const SUBTEAM_MENTION_RE = /<!subteam\^([A-Z0-9]+)(?:\|[^>]*)?>/gi;
2560
+ const SUBTEAM_MEMBER_CACHE_TTL_MS = 300 * 1e3;
2561
+ let subteamMemberCache = /* @__PURE__ */ new WeakMap();
2562
+ function normalizeSlackId(value) {
2563
+ return typeof value === "string" && value.trim() ? value.trim().toUpperCase() : void 0;
2564
+ }
2565
+ function extractSlackSubteamMentionIds(text) {
2566
+ if (!text) return [];
2567
+ const ids = /* @__PURE__ */ new Set();
2568
+ for (const match of text.matchAll(SUBTEAM_MENTION_RE)) {
2569
+ const id = normalizeSlackId(match[1]);
2570
+ if (id) ids.add(id);
2571
+ }
2572
+ return [...ids];
2573
+ }
2574
+ async function readSlackSubteamUsers(params) {
2575
+ let bySubteam = subteamMemberCache.get(params.client);
2576
+ if (!bySubteam) {
2577
+ bySubteam = /* @__PURE__ */ new Map();
2578
+ subteamMemberCache.set(params.client, bySubteam);
2579
+ }
2580
+ const cacheKey = `${normalizeSlackId(params.teamId) ?? ""}:${params.subteamId}`;
2581
+ const cached = bySubteam.get(cacheKey);
2582
+ if (cached && cached.expiresAt > params.now) return cached.users;
2583
+ try {
2584
+ const response = await params.client.usergroups.users.list({
2585
+ usergroup: params.subteamId,
2586
+ ...params.teamId ? { team_id: params.teamId } : {}
2587
+ });
2588
+ if (!response.ok) {
2589
+ params.log?.(`slack: failed to resolve user-group mention ${params.subteamId}: ${response.error ?? "unknown_error"}`);
2590
+ return /* @__PURE__ */ new Set();
2591
+ }
2592
+ const users = new Set((response.users ?? []).map((userId) => normalizeSlackId(userId)).filter(Boolean));
2593
+ bySubteam.set(cacheKey, {
2594
+ expiresAt: params.now + SUBTEAM_MEMBER_CACHE_TTL_MS,
2595
+ users
2596
+ });
2597
+ return users;
2598
+ } catch (err) {
2599
+ params.log?.(`slack: failed to resolve user-group mention ${params.subteamId}: ${formatErrorMessage(err)}`);
2600
+ return /* @__PURE__ */ new Set();
2601
+ }
2602
+ }
2603
+ async function isSlackSubteamMentionForBot(params) {
2604
+ const botUserId = normalizeSlackId(params.botUserId);
2605
+ if (!botUserId) return false;
2606
+ const subteamIds = extractSlackSubteamMentionIds(params.text);
2607
+ if (subteamIds.length === 0) return false;
2608
+ const now = params.now ?? Date.now();
2609
+ for (const subteamId of subteamIds) if ((await readSlackSubteamUsers({
2610
+ client: params.client,
2611
+ subteamId,
2612
+ teamId: normalizeOptionalString(params.teamId),
2613
+ now,
2614
+ log: params.log
2615
+ })).has(botUserId)) return true;
2616
+ return false;
2617
+ }
2618
+ //#endregion
2619
+ //#region extensions/slack/src/monitor/message-handler/prepare.ts
2620
+ const mentionRegexCache = /* @__PURE__ */ new WeakMap();
2621
+ const SLACK_ANY_MENTION_RE = /<@[^>]+>|<!subteam\^[^>]+>/;
2622
+ const SLACK_USER_MENTION_RE = /<@([^>|]+)(?:\|[^>]+)?>/g;
2623
+ const SLACK_SUBTEAM_MENTION_RE = /<!subteam\^([^>|]+)(?:\|[^>]+)?>/g;
2624
+ const SLACK_SUBTEAM_MENTION_MARKER = "<!subteam^";
2625
+ const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4;
2626
+ const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
2627
+ const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1e3;
2628
+ const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3e3;
2629
+ function recordString(record, key) {
2630
+ return normalizeOptionalString(record?.[key]);
2631
+ }
2632
+ function recordNullableString(record, key) {
2633
+ if (!record || !(key in record)) return;
2634
+ if (record[key] === null) return null;
2635
+ return normalizeOptionalString(record[key]);
2636
+ }
2637
+ function mergeSlackAssistantThreadContext(primary, fallback) {
2638
+ if (!primary) return fallback;
2639
+ if (!fallback) return primary;
2640
+ return {
2641
+ assistantChannelId: primary.assistantChannelId || fallback.assistantChannelId,
2642
+ threadTs: primary.threadTs || fallback.threadTs,
2643
+ userId: primary.userId ?? fallback.userId,
2644
+ channelId: primary.channelId ?? fallback.channelId,
2645
+ teamId: primary.teamId ?? fallback.teamId,
2646
+ enterpriseId: primary.enterpriseId !== void 0 ? primary.enterpriseId : fallback.enterpriseId
2647
+ };
2648
+ }
2649
+ function hasSlackAssistantThreadMetadata(context) {
2650
+ return Boolean(context?.channelId || context?.teamId || context?.enterpriseId !== void 0);
2651
+ }
2652
+ function resolveSlackMessageAssistantThreadContext(message) {
2653
+ const thread = asOptionalRecord(message.assistant_thread);
2654
+ if (!thread) return;
2655
+ const context = asOptionalRecord(thread.context);
2656
+ const assistantChannelId = recordString(thread, "channel_id") ?? message.channel;
2657
+ const threadTs = recordString(thread, "thread_ts") ?? message.thread_ts ?? message.ts;
2658
+ if (!assistantChannelId || !threadTs) return;
2659
+ return {
2660
+ assistantChannelId,
2661
+ threadTs,
2662
+ userId: recordString(thread, "user_id") ?? message.user,
2663
+ channelId: recordString(context, "channel_id"),
2664
+ teamId: recordString(context, "team_id"),
2665
+ enterpriseId: recordNullableString(context, "enterprise_id")
2666
+ };
2667
+ }
2668
+ async function restoreSlackAssistantThreadContextFromMetadata(params) {
2669
+ const threadTs = params.message.thread_ts;
2670
+ const parentUserId = params.message.parent_user_id?.trim();
2671
+ if (!params.message.channel || !threadTs || !parentUserId || parentUserId !== params.ctx.botUserId && parentUserId !== params.ctx.botId) return;
2672
+ try {
2673
+ const response = await params.ctx.app.client.conversations.replies({
2674
+ channel: params.message.channel,
2675
+ ts: threadTs,
2676
+ oldest: threadTs,
2677
+ include_all_metadata: true,
2678
+ limit: 4
2679
+ });
2680
+ for (const message of response.messages ?? []) {
2681
+ const context = parseSlackAssistantThreadMetadata(message.metadata);
2682
+ if (!context) continue;
2683
+ return {
2684
+ assistantChannelId: params.message.channel,
2685
+ threadTs,
2686
+ userId: params.message.user,
2687
+ channelId: context.channelId,
2688
+ teamId: context.teamId,
2689
+ enterpriseId: context.enterpriseId
2690
+ };
2691
+ }
2692
+ } catch (err) {
2693
+ logVerbose(`slack assistant context restore failed channel=${params.message.channel} ts=${threadTs}: ${formatErrorMessage(err)}`);
2694
+ }
2695
+ }
2696
+ function resolveCachedMentionRegexes(ctx, agentId) {
2697
+ const key = normalizeOptionalString(agentId) ?? "__default__";
2698
+ let byAgent = mentionRegexCache.get(ctx);
2699
+ if (!byAgent) {
2700
+ byAgent = /* @__PURE__ */ new Map();
2701
+ mentionRegexCache.set(ctx, byAgent);
2702
+ }
2703
+ const cached = byAgent.get(key);
2704
+ if (cached) return cached;
2705
+ const built = buildMentionRegexes(ctx.cfg, agentId);
2706
+ byAgent.set(key, built);
2707
+ return built;
2708
+ }
2709
+ function isSlackImageFileCandidate(file) {
2710
+ if ((file.mimetype?.split(";")[0]?.trim().toLowerCase())?.startsWith("image/")) return true;
2711
+ return Boolean(mimeTypeFromFilePath(file.name)?.startsWith("image/"));
2712
+ }
2713
+ function sliceSlackImageFileCandidates(files, limit) {
2714
+ if (limit <= 0 || !files?.length) return [];
2715
+ return files.filter(isSlackImageFileCandidate).slice(0, limit);
2716
+ }
2717
+ function sliceSlackHistoryAttachmentCandidates(attachments, limit) {
2718
+ if (limit <= 0 || !attachments?.length) return [];
2719
+ const out = [];
2720
+ let remaining = limit;
2721
+ for (const attachment of attachments) {
2722
+ if (attachment.is_share !== true) continue;
2723
+ const hasImageUrl = Boolean(normalizeOptionalString(attachment.image_url));
2724
+ const files = sliceSlackImageFileCandidates(attachment.files, remaining - (hasImageUrl ? 1 : 0));
2725
+ if (!hasImageUrl && files.length === 0) continue;
2726
+ out.push({
2727
+ ...attachment,
2728
+ files
2729
+ });
2730
+ remaining -= (hasImageUrl ? 1 : 0) + files.length;
2731
+ if (remaining <= 0) break;
2732
+ }
2733
+ return out;
2734
+ }
2735
+ function buildSlackHistoryMediaCandidateMessage(message) {
2736
+ const files = sliceSlackImageFileCandidates(message.files, SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS);
2737
+ const attachments = sliceSlackHistoryAttachmentCandidates(message.attachments, Math.max(0, SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS - files.length));
2738
+ if (files.length === 0 && attachments.length === 0) return null;
2739
+ return {
2740
+ ...message,
2741
+ files,
2742
+ attachments
2743
+ };
2744
+ }
2745
+ async function resolveSlackHistoryMediaForPendingRecord(params) {
2746
+ const mediaMessage = buildSlackHistoryMediaCandidateMessage(params.message);
2747
+ if (!mediaMessage) return [];
2748
+ return toInboundMediaFacts((await resolveSlackMessageContent({
2749
+ message: mediaMessage,
2750
+ isThreadReply: params.isThreadReply,
2751
+ threadStarter: params.threadStarter,
2752
+ isBotMessage: params.isBotMessage,
2753
+ client: params.ctx.app.client,
2754
+ botToken: params.ctx.botToken,
2755
+ mediaMaxBytes: Math.min(params.ctx.mediaMaxBytes, SLACK_HISTORY_MEDIA_MAX_BYTES),
2756
+ mediaReadIdleTimeoutMs: SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS,
2757
+ mediaTotalTimeoutMs: SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS
2758
+ }))?.effectiveDirectMedia, {
2759
+ kind: "image",
2760
+ messageId: params.message.ts
2761
+ });
2762
+ }
2763
+ function collectUniqueSlackMentionIds(text, regex) {
2764
+ const ids = [];
2765
+ regex.lastIndex = 0;
2766
+ for (const match of text.matchAll(regex)) {
2767
+ const id = normalizeSlackId(match[1]);
2768
+ if (id && !ids.includes(id)) ids.push(id);
2769
+ }
2770
+ return ids;
2771
+ }
2772
+ function collectSlackMentionMetadata(text) {
2773
+ return {
2774
+ mentionedUserIds: collectUniqueSlackMentionIds(text, SLACK_USER_MENTION_RE),
2775
+ mentionedSubteamIds: collectUniqueSlackMentionIds(text, SLACK_SUBTEAM_MENTION_RE),
2776
+ hasAnyMention: SLACK_ANY_MENTION_RE.test(text),
2777
+ hasSubteamMention: text.includes(SLACK_SUBTEAM_MENTION_MARKER)
2778
+ };
2779
+ }
2780
+ async function resolveSlackExplicitMentionState(params) {
2781
+ const normalizedBotUserId = normalizeSlackId(params.ctx.botUserId);
2782
+ const explicitlyMentionedBotUser = Boolean(normalizedBotUserId && params.mentionedUserIds.includes(normalizedBotUserId));
2783
+ const explicitlyMentionedBotSubteam = Boolean(params.ctx.botUserId && params.hasSubteamMention) && await isSlackSubteamMentionForBot({
2784
+ client: params.ctx.app.client,
2785
+ text: params.messageText,
2786
+ botUserId: params.ctx.botUserId,
2787
+ teamId: params.ctx.teamId,
2788
+ log: logVerbose
2789
+ });
2790
+ return {
2791
+ explicitlyMentionedBotUser,
2792
+ explicitlyMentionedBotSubteam,
2793
+ explicitlyMentioned: explicitlyMentionedBotUser || explicitlyMentionedBotSubteam || params.source === "app_mention"
2794
+ };
2795
+ }
2796
+ function resolveSlackMentionSource(params) {
2797
+ if (params.explicitBotMention) return "explicit_bot";
2798
+ if (params.explicitSubteamMention) return "subteam";
2799
+ if (params.shouldBypassMention) return "command_bypass";
2800
+ if (params.wasMentioned) return "mention_pattern";
2801
+ if (params.matchedImplicitMentionKinds.length > 0) return "implicit_thread";
2802
+ return "none";
2803
+ }
2804
+ function buildSlackMentionContextPayload(params) {
2805
+ if (!params.isRoomish) return {};
2806
+ return {
2807
+ WasMentioned: params.effectiveWasMentioned,
2808
+ ExplicitlyMentionedBot: params.explicitlyMentioned,
2809
+ MentionedUserIds: params.mentionedUserIds.length > 0 ? [...params.mentionedUserIds] : void 0,
2810
+ MentionedSubteamIds: params.mentionedSubteamIds.length > 0 ? [...params.mentionedSubteamIds] : void 0,
2811
+ ImplicitMentionKinds: params.matchedImplicitMentionKinds.length > 0 ? [...params.matchedImplicitMentionKinds] : void 0,
2812
+ MentionSource: params.mentionSource
2813
+ };
2814
+ }
2815
+ async function resolveSlackConversationContext(params) {
2816
+ const { ctx, account, message } = params;
2817
+ const cfg = ctx.cfg;
2818
+ let channelInfo = {};
2819
+ let resolvedChannelType = normalizeSlackChannelType(message.channel_type, message.channel);
2820
+ if (resolvedChannelType !== "im" && (!message.channel_type || message.channel_type !== "im")) {
2821
+ channelInfo = await ctx.resolveChannelName(message.channel);
2822
+ resolvedChannelType = normalizeSlackChannelType(message.channel_type ?? channelInfo.type, message.channel);
2823
+ }
2824
+ const channelName = channelInfo?.name;
2825
+ const isDirectMessage = resolvedChannelType === "im";
2826
+ const isGroupDm = resolvedChannelType === "mpim";
2827
+ const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
2828
+ const isRoomish = isRoom || isGroupDm;
2829
+ const channelConfig = isRoom ? resolveSlackChannelConfig({
2830
+ channelId: message.channel,
2831
+ channelName,
2832
+ channels: ctx.channelsConfig,
2833
+ channelKeys: ctx.channelsConfigKeys,
2834
+ defaultRequireMention: ctx.defaultRequireMention,
2835
+ allowNameMatching: ctx.allowNameMatching
2836
+ }) : null;
2837
+ const allowBotsSetting = channelConfig?.allowBots ?? account.config?.allowBots ?? cfg.channels?.slack?.allowBots ?? false;
2838
+ return {
2839
+ channelInfo,
2840
+ channelName,
2841
+ resolvedChannelType,
2842
+ isDirectMessage,
2843
+ isGroupDm,
2844
+ isRoom,
2845
+ isRoomish,
2846
+ channelConfig,
2847
+ allowBotsMode: allowBotsSetting === "mentions" ? "mentions" : allowBotsSetting ? "all" : "off",
2848
+ isBotMessage: Boolean(message.bot_id)
2849
+ };
2850
+ }
2851
+ async function authorizeSlackInboundMessage(params) {
2852
+ const { ctx, account, message, conversation } = params;
2853
+ const { isDirectMessage, channelName, resolvedChannelType, isBotMessage, allowBotsMode } = conversation;
2854
+ if (isBotMessage) {
2855
+ if (message.user && ctx.botUserId && message.user === ctx.botUserId) return null;
2856
+ if (allowBotsMode === "off") {
2857
+ logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`);
2858
+ return null;
2859
+ }
2860
+ }
2861
+ if (isDirectMessage && !message.user) {
2862
+ logVerbose("slack: drop dm message (missing user id)");
2863
+ return null;
2864
+ }
2865
+ const senderId = message.user ?? (isBotMessage ? message.bot_id : void 0);
2866
+ if (!senderId) {
2867
+ logVerbose("slack: drop message (missing sender id)");
2868
+ return null;
2869
+ }
2870
+ if (!ctx.isChannelAllowed({
2871
+ channelId: message.channel,
2872
+ channelName,
2873
+ channelType: resolvedChannelType
2874
+ })) {
2875
+ logVerbose("slack: drop message (channel not allowed)");
2876
+ return null;
2877
+ }
2878
+ const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, { includePairingStore: isDirectMessage });
2879
+ if (isDirectMessage) {
2880
+ const directUserId = message.user;
2881
+ if (!directUserId) {
2882
+ logVerbose("slack: drop dm message (missing user id)");
2883
+ return null;
2884
+ }
2885
+ if (!await authorizeSlackDirectMessage({
2886
+ ctx,
2887
+ accountId: account.accountId,
2888
+ senderId: directUserId,
2889
+ allowFromLower,
2890
+ resolveSenderName: ctx.resolveUserName,
2891
+ sendPairingReply: async (text) => {
2892
+ await sendMessageSlack(message.channel, text, {
2893
+ cfg: ctx.cfg,
2894
+ token: ctx.botToken,
2895
+ client: ctx.app.client,
2896
+ accountId: account.accountId
2897
+ });
2898
+ },
2899
+ onDisabled: () => {
2900
+ logVerbose("slack: drop dm (dms disabled)");
2901
+ },
2902
+ onUnauthorized: ({ allowMatchMeta }) => {
2903
+ logVerbose(`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`);
2904
+ },
2905
+ log: logVerbose
2906
+ })) return null;
2907
+ }
2908
+ return {
2909
+ senderId,
2910
+ allowFromLower
2911
+ };
2912
+ }
2913
+ async function prepareSlackMessage(params) {
2914
+ const { ctx, account, message, opts } = params;
2915
+ const cfg = ctx.cfg;
2916
+ const conversation = await resolveSlackConversationContext({
2917
+ ctx,
2918
+ account,
2919
+ message
2920
+ });
2921
+ const { channelInfo, channelName, isDirectMessage, isGroupDm, isRoom, isRoomish, channelConfig, allowBotsMode, isBotMessage } = conversation;
2922
+ const authorization = await authorizeSlackInboundMessage({
2923
+ ctx,
2924
+ account,
2925
+ message,
2926
+ conversation
2927
+ });
2928
+ if (!authorization) return null;
2929
+ const { senderId, allowFromLower } = authorization;
2930
+ const messageText = message.text ?? "";
2931
+ const mentionMetadata = collectSlackMentionMetadata(messageText);
2932
+ const { mentionedUserIds, mentionedSubteamIds, hasAnyMention } = mentionMetadata;
2933
+ const { explicitlyMentionedBotUser, explicitlyMentionedBotSubteam, explicitlyMentioned } = await resolveSlackExplicitMentionState({
2934
+ ctx,
2935
+ messageText,
2936
+ mentionedUserIds,
2937
+ hasSubteamMention: mentionMetadata.hasSubteamMention,
2938
+ source: opts.source
2939
+ });
2940
+ const channelRequireMention = channelConfig?.requireMention ?? ctx.defaultRequireMention ?? true;
2941
+ const channelChatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";
2942
+ const messageAssistantThreadContext = resolveSlackMessageAssistantThreadContext(message);
2943
+ const assistantContextLookupChannelId = messageAssistantThreadContext?.assistantChannelId ?? message.channel;
2944
+ const assistantContextLookupThreadTs = messageAssistantThreadContext?.threadTs ?? message.thread_ts ?? message.ts;
2945
+ const cachedAssistantThreadContext = isDirectMessage ? ctx.getSlackAssistantThreadContext(assistantContextLookupChannelId, assistantContextLookupThreadTs) : void 0;
2946
+ const restoredAssistantThreadContext = isDirectMessage && !cachedAssistantThreadContext && !hasSlackAssistantThreadMetadata(messageAssistantThreadContext) ? await restoreSlackAssistantThreadContextFromMetadata({
2947
+ ctx,
2948
+ message
2949
+ }) : void 0;
2950
+ const assistantThreadContext = mergeSlackAssistantThreadContext(messageAssistantThreadContext, cachedAssistantThreadContext ?? restoredAssistantThreadContext);
2951
+ const assistantThreadContextToCache = messageAssistantThreadContext || restoredAssistantThreadContext ? assistantThreadContext : void 0;
2952
+ if (assistantThreadContextToCache) ctx.saveSlackAssistantThreadContext(assistantThreadContextToCache);
2953
+ const willImplicitlyThreadReply = isRoom && !channelRequireMention && resolveSlackReplyToMode(account, channelChatType) !== "off";
2954
+ const seedTopLevelRoomThreadBySource = opts.source === "app_mention" || opts.wasMentioned === true || explicitlyMentioned || willImplicitlyThreadReply;
2955
+ let routing = resolveSlackRoutingContext({
2956
+ ctx,
2957
+ account,
2958
+ message,
2959
+ isDirectMessage,
2960
+ isGroupDm,
2961
+ isRoom,
2962
+ isRoomish,
2963
+ seedTopLevelRoomThread: seedTopLevelRoomThreadBySource,
2964
+ assistantThreadTs: assistantThreadContext?.threadTs
2965
+ });
2966
+ const resolveWasMentioned = (mentionRegexes) => opts.wasMentioned ?? (!isDirectMessage && matchesMentionWithExplicit({
2967
+ text: messageText,
2968
+ mentionRegexes,
2969
+ explicit: {
2970
+ hasAnyMention,
2971
+ isExplicitlyMentioned: explicitlyMentioned,
2972
+ canResolveExplicit: Boolean(ctx.botUserId)
2973
+ }
2974
+ }));
2975
+ let mentionRegexes = resolveCachedMentionRegexes(ctx, routing.route.agentId);
2976
+ let wasMentioned = resolveWasMentioned(mentionRegexes);
2977
+ const hasBoundSession = Boolean(routing.runtimeBoundSessionKey || routing.configuredBindingSessionKey);
2978
+ if (!seedTopLevelRoomThreadBySource && wasMentioned && isRoom && !routing.isThreadReply && !hasBoundSession) {
2979
+ routing = resolveSlackRoutingContext({
2980
+ ctx,
2981
+ account,
2982
+ message,
2983
+ isDirectMessage,
2984
+ isGroupDm,
2985
+ isRoom,
2986
+ isRoomish,
2987
+ seedTopLevelRoomThread: true,
2988
+ assistantThreadTs: assistantThreadContext?.threadTs
2989
+ });
2990
+ mentionRegexes = resolveCachedMentionRegexes(ctx, routing.route.agentId);
2991
+ wasMentioned = resolveWasMentioned(mentionRegexes);
2992
+ }
2993
+ const { route, runtimeBinding, configuredBinding, configuredBindingSessionKey, replyToMode, threadContext, threadTs, isThreadReply, threadKeys, sessionKey, historyKey } = routing;
2994
+ if (runtimeBinding && shouldLogVerbose()) logVerbose(`slack: routed via bound conversation ${runtimeBinding.conversation.conversationId} -> ${runtimeBinding.targetSessionKey}`);
2995
+ if (configuredBinding) {
2996
+ const ensured = await ensureConfiguredBindingRouteReady({
2997
+ cfg,
2998
+ bindingResolution: configuredBinding
2999
+ });
3000
+ if (ensured.ok) {
3001
+ if (shouldLogVerbose()) logVerbose(`slack: using configured ACP binding for ${configuredBinding.record.conversation.conversationId} -> ${configuredBindingSessionKey}`);
3002
+ } else {
3003
+ if (shouldLogVerbose()) logVerbose(`slack: configured ACP binding unavailable for ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`);
3004
+ logInboundDrop({
3005
+ log: logVerbose,
3006
+ channel: "slack",
3007
+ reason: "configured ACP binding unavailable",
3008
+ target: configuredBinding.record.conversation.conversationId
3009
+ });
3010
+ return null;
3011
+ }
3012
+ }
3013
+ const directThreadRoutedToDmSession = !assistantThreadContext && isDirectMessage && isThreadReply && threadTs && runtimeBinding?.conversation.conversationId !== threadTs;
3014
+ let implicitMentionKinds = [];
3015
+ if (!isDirectMessage && ctx.botUserId && message.thread_ts && !ctx.threadRequireExplicitMention && !wasMentioned) {
3016
+ const replyToBotKinds = implicitMentionKindWhen("reply_to_bot", message.parent_user_id === ctx.botUserId);
3017
+ implicitMentionKinds = replyToBotKinds.length > 0 ? replyToBotKinds : implicitMentionKindWhen("bot_thread_participant", await hasSlackThreadParticipationWithPersistence({
3018
+ accountId: account.accountId,
3019
+ channelId: message.channel,
3020
+ threadTs: message.thread_ts
3021
+ }));
3022
+ }
3023
+ let resolvedSenderName = normalizeOptionalString(message.username);
3024
+ const resolveSenderName = async () => {
3025
+ if (resolvedSenderName) return resolvedSenderName;
3026
+ if (message.user) {
3027
+ const normalized = normalizeOptionalString((await ctx.resolveUserName(message.user))?.name);
3028
+ if (normalized) {
3029
+ resolvedSenderName = normalized;
3030
+ return resolvedSenderName;
3031
+ }
3032
+ }
3033
+ resolvedSenderName = message.user ?? message.bot_id ?? "unknown";
3034
+ return resolvedSenderName;
3035
+ };
3036
+ const senderNameForAuth = ctx.allowNameMatching ? await resolveSenderName() : void 0;
3037
+ const allowTextCommands = shouldHandleTextCommands({
3038
+ cfg,
3039
+ surface: "slack"
3040
+ });
3041
+ const shouldRequireMention = isRoom ? channelConfig?.requireMention ?? ctx.defaultRequireMention : false;
3042
+ if (message["_ambiguousThreadReply"]) {
3043
+ ctx.logger.info({
3044
+ channel: message.channel,
3045
+ ts: message.ts,
3046
+ parentUserId: message.parent_user_id
3047
+ }, "skipping ambiguous slack thread reply");
3048
+ return null;
3049
+ }
3050
+ const canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0;
3051
+ const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? "");
3052
+ const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
3053
+ const hasAbortRequest = isAbortRequestText(textForCommandDetection);
3054
+ const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
3055
+ const messageIngress = await resolveSlackCommandIngress({
3056
+ ctx,
3057
+ senderId,
3058
+ senderName: senderNameForAuth,
3059
+ channelType: conversation.resolvedChannelType ?? "channel",
3060
+ channelId: message.channel,
3061
+ ownerAllowFromLower: allowFromLower,
3062
+ channelUsers: isRoom ? channelConfig?.users : void 0,
3063
+ allowTextCommands,
3064
+ hasControlCommand: hasControlCommandInMessage,
3065
+ mentionFacts: {
3066
+ canDetectMention,
3067
+ wasMentioned,
3068
+ hasAnyMention,
3069
+ implicitMentionKinds
3070
+ },
3071
+ activation: {
3072
+ requireMention: shouldRequireMention,
3073
+ allowTextCommands,
3074
+ ...ctx.threadRequireExplicitMention ? { allowedImplicitMentionKinds: [] } : {}
3075
+ }
3076
+ });
3077
+ const effectiveWasMentioned = messageIngress.activationAccess.effectiveWasMentioned ?? false;
3078
+ const shouldBypassMention = messageIngress.activationAccess.shouldBypassMention ?? false;
3079
+ const matchedImplicitMentionKinds = implicitMentionKinds;
3080
+ const mentionSource = resolveSlackMentionSource({
3081
+ explicitBotMention: explicitlyMentionedBotUser || opts.source === "app_mention",
3082
+ explicitSubteamMention: explicitlyMentionedBotSubteam,
3083
+ matchedImplicitMentionKinds,
3084
+ shouldBypassMention,
3085
+ wasMentioned
3086
+ });
3087
+ const senderGate = messageIngress.senderAccess.gate;
3088
+ if (isRoom && senderGate?.allowed === false) {
3089
+ logVerbose(`Blocked unauthorized slack sender ${senderId} (not in channel users)`);
3090
+ return null;
3091
+ }
3092
+ if (isRoom && isBotMessage && allowBotsMode !== "off" && !await authorizeSlackBotRoomMessage({
3093
+ ctx,
3094
+ channelId: message.channel,
3095
+ senderId,
3096
+ senderName: senderNameForAuth,
3097
+ channelUsers: channelConfig?.users,
3098
+ allowFromLower
3099
+ })) return null;
3100
+ if (isBotMessage && allowBotsMode === "mentions") {
3101
+ if (!(isDirectMessage || effectiveWasMentioned || shouldBypassMention)) {
3102
+ logVerbose("slack: drop bot message (allowBots=mentions, missing mention)");
3103
+ return null;
3104
+ }
3105
+ }
3106
+ const threadContextAllowFromLower = isRoom ? channelUsersAllowlistConfigured ? normalizeAllowListLower(channelConfig?.users) : [] : isDirectMessage ? allowFromLower : [];
3107
+ const contextVisibilityMode = resolveChannelContextVisibilityMode({
3108
+ cfg: ctx.cfg,
3109
+ channel: "slack",
3110
+ accountId: account.accountId
3111
+ });
3112
+ const commandAuthorized = messageIngress.commandAccess.authorized;
3113
+ if (isRoomish && messageIngress.commandAccess.shouldBlockControlCommand) {
3114
+ logInboundDrop({
3115
+ log: logVerbose,
3116
+ channel: "slack",
3117
+ reason: "control command (unauthorized)",
3118
+ target: senderId
3119
+ });
3120
+ return null;
3121
+ }
3122
+ if (isRoom && shouldRequireMention && messageIngress.activationAccess.shouldSkip) {
3123
+ ctx.logger.info({
3124
+ channel: message.channel,
3125
+ reason: "no-mention"
3126
+ }, "skipping channel message");
3127
+ const pendingText = (message.text ?? "").trim();
3128
+ const historyMediaCandidate = buildSlackHistoryMediaCandidateMessage(message);
3129
+ const fallbackFile = message.files?.length ? `[Slack file: ${formatSlackFileReference(message.files[0])}]` : "";
3130
+ const pendingBody = pendingText || fallbackFile || (!fallbackFile && historyMediaCandidate ? "[Slack media attachment]" : "");
3131
+ const skippedThreadStarter = historyMediaCandidate && isThreadReply && threadTs ? await resolveSlackThreadStarter({
3132
+ channelId: message.channel,
3133
+ threadTs,
3134
+ client: ctx.app.client
3135
+ }) : null;
3136
+ const timestamp = message.ts ? Math.round(Number(message.ts) * 1e3) : void 0;
3137
+ const senderName = pendingBody ? await resolveSenderName() : void 0;
3138
+ await recordDroppedChannelInboundHistory({
3139
+ input: {
3140
+ id: message.ts ?? `${message.channel}:${Date.now()}`,
3141
+ timestamp,
3142
+ rawText: pendingBody,
3143
+ textForAgent: pendingBody,
3144
+ raw: message
3145
+ },
3146
+ admission: {
3147
+ kind: "drop",
3148
+ reason: "slack-no-mention",
3149
+ recordHistory: true
3150
+ },
3151
+ preflight: {
3152
+ message: pendingBody ? {
3153
+ rawBody: pendingBody,
3154
+ body: pendingBody,
3155
+ bodyForAgent: pendingBody,
3156
+ senderLabel: senderName,
3157
+ envelopeFrom: senderName
3158
+ } : void 0,
3159
+ history: {
3160
+ key: historyKey,
3161
+ historyMap: ctx.channelHistories,
3162
+ limit: ctx.historyLimit,
3163
+ recordOnDrop: true,
3164
+ mediaLimit: SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS
3165
+ },
3166
+ media: () => resolveSlackHistoryMediaForPendingRecord({
3167
+ ctx,
3168
+ message,
3169
+ isThreadReply,
3170
+ threadStarter: skippedThreadStarter,
3171
+ isBotMessage
3172
+ })
3173
+ }
3174
+ });
3175
+ return null;
3176
+ }
3177
+ const threadStarter = isThreadReply && threadTs ? await resolveSlackThreadStarter({
3178
+ channelId: message.channel,
3179
+ threadTs,
3180
+ client: ctx.app.client
3181
+ }) : null;
3182
+ const resolvedMessageContent = await resolveSlackMessageContent({
3183
+ message,
3184
+ isThreadReply,
3185
+ threadStarter,
3186
+ isBotMessage,
3187
+ botToken: ctx.botToken,
3188
+ client: ctx.app.client,
3189
+ mediaMaxBytes: ctx.mediaMaxBytes,
3190
+ resolveUserName: ctx.resolveUserName
3191
+ });
3192
+ if (!resolvedMessageContent) return null;
3193
+ const { rawBody, effectiveDirectMedia } = resolvedMessageContent;
3194
+ const chatType = resolveSlackChatType(conversation.resolvedChannelType);
3195
+ const inboundEventKind = classifyChannelInboundEvent({
3196
+ conversation: { kind: chatType },
3197
+ unmentionedGroupPolicy: resolveUnmentionedGroupInboundPolicy({
3198
+ cfg,
3199
+ agentId: route.agentId
3200
+ }),
3201
+ wasMentioned: effectiveWasMentioned,
3202
+ hasControlCommand: hasControlCommandInMessage,
3203
+ hasAbortRequest
3204
+ });
3205
+ const ackReaction = resolveAckReaction(cfg, route.agentId, {
3206
+ channel: "slack",
3207
+ accountId: account.accountId
3208
+ });
3209
+ const ackReactionValue = ackReaction ?? "";
3210
+ const sourceRepliesAreToolOnly = resolveChannelMessageSourceReplyDeliveryMode({
3211
+ cfg,
3212
+ ctx: {
3213
+ ChatType: chatType,
3214
+ InboundEventKind: inboundEventKind
3215
+ }
3216
+ }) === "message_tool_only";
3217
+ const statusReactionsExplicitlyEnabled = cfg.messages?.statusReactions?.enabled === true;
3218
+ const shouldAckReaction$1 = () => Boolean(ackReaction && shouldAckReaction({
3219
+ scope: ctx.ackReactionScope,
3220
+ isDirect: isDirectMessage,
3221
+ isGroup: isRoomish,
3222
+ isMentionableGroup: isRoom,
3223
+ requireMention: shouldRequireMention,
3224
+ canDetectMention,
3225
+ effectiveWasMentioned,
3226
+ shouldBypassMention
3227
+ }));
3228
+ const ackReactionMessageTs = message.ts;
3229
+ const allowToolOnlyStatusReaction = statusReactionsExplicitlyEnabled && (effectiveWasMentioned || shouldBypassMention);
3230
+ const shouldSendAckReaction = shouldAckReaction$1() && (!sourceRepliesAreToolOnly || allowToolOnlyStatusReaction);
3231
+ const statusReactionsWillHandle = Boolean(ackReactionMessageTs) && cfg.messages?.statusReactions?.enabled !== false && shouldSendAckReaction;
3232
+ const ackReactionPromise = !statusReactionsWillHandle && shouldSendAckReaction && ackReactionMessageTs && ackReactionValue ? reactSlackMessage(message.channel, ackReactionMessageTs, ackReactionValue, {
3233
+ token: ctx.botToken,
3234
+ client: ctx.app.client
3235
+ }).then(() => true, (err) => {
3236
+ logVerbose(`slack react failed for channel ${message.channel}: ${formatSlackError(err)}`);
3237
+ return false;
3238
+ }) : statusReactionsWillHandle ? Promise.resolve(true) : null;
3239
+ const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
3240
+ const senderName = await resolveSenderName();
3241
+ const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
3242
+ const inboundLabel = isDirectMessage ? `Slack DM from ${senderName}` : `Slack message in ${roomLabel} from ${senderName}`;
3243
+ const slackFrom = isDirectMessage ? `slack:${message.user}` : isRoom ? `slack:channel:${message.channel}` : `slack:group:${message.channel}`;
3244
+ enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
3245
+ sessionKey,
3246
+ contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`
3247
+ });
3248
+ const envelopeFrom = resolveConversationLabel$1({
3249
+ ChatType: chatType,
3250
+ SenderName: senderName,
3251
+ GroupSubject: isRoomish ? roomLabel : void 0,
3252
+ From: slackFrom
3253
+ }) ?? (isDirectMessage ? senderName : roomLabel);
3254
+ const threadInfo = isThreadReply && threadTs ? ` thread_ts: ${threadTs}${message.parent_user_id ? ` parent_user_id: ${message.parent_user_id}` : ""}` : "";
3255
+ const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}${threadInfo}]`;
3256
+ const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId });
3257
+ const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
3258
+ const previousTimestamp = readSessionUpdatedAt({
3259
+ storePath,
3260
+ sessionKey
3261
+ });
3262
+ const channelHistory = createChannelHistoryWindow({ historyMap: ctx.channelHistories });
3263
+ const dmHistoryLimit = isDirectMessage ? resolveSlackDmHistoryLimit({
3264
+ account,
3265
+ userId: message.user,
3266
+ defaultLimit: ctx.dmHistoryLimit
3267
+ }) : 0;
3268
+ let combinedBody = formatInboundEnvelope({
3269
+ channel: "Slack",
3270
+ from: envelopeFrom,
3271
+ timestamp: message.ts ? Math.round(Number(message.ts) * 1e3) : void 0,
3272
+ body: textWithId,
3273
+ chatType,
3274
+ sender: {
3275
+ name: senderName,
3276
+ id: senderId
3277
+ },
3278
+ previousTimestamp,
3279
+ envelope: envelopeOptions
3280
+ });
3281
+ const dmHistoryContext = isDirectMessage && !isThreadReply && dmHistoryLimit > 0 && !previousTimestamp ? await resolveSlackDmHistoryContext({
3282
+ ctx,
3283
+ channelId: message.channel,
3284
+ currentMessageTs: message.ts,
3285
+ limit: dmHistoryLimit,
3286
+ envelopeOptions
3287
+ }) : {
3288
+ body: void 0,
3289
+ inboundHistory: void 0
3290
+ };
3291
+ if (dmHistoryContext.body) combinedBody = `${dmHistoryContext.body}\n\n${combinedBody}`;
3292
+ if (isRoomish && ctx.historyLimit > 0) combinedBody = channelHistory.buildPendingContext({
3293
+ historyKey,
3294
+ limit: ctx.historyLimit,
3295
+ currentMessage: combinedBody,
3296
+ formatEntry: (entry) => formatInboundEnvelope({
3297
+ channel: "Slack",
3298
+ from: roomLabel,
3299
+ timestamp: entry.timestamp,
3300
+ body: `${entry.body}${entry.messageId ? ` [id:${entry.messageId} channel:${message.channel}]` : ""}`,
3301
+ chatType: "channel",
3302
+ senderLabel: entry.sender,
3303
+ envelope: envelopeOptions
3304
+ })
3305
+ });
3306
+ const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`;
3307
+ const { untrustedChannelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({
3308
+ isRoomish,
3309
+ channelInfo,
3310
+ channelConfig
3311
+ });
3312
+ const { threadStarterBody, threadHistoryBody, threadSessionPreviousTimestamp, threadLabel, threadStarterMedia } = await resolveSlackThreadContextData({
3313
+ ctx,
3314
+ account,
3315
+ message,
3316
+ isThreadReply,
3317
+ threadTs,
3318
+ threadStarter,
3319
+ roomLabel,
3320
+ storePath,
3321
+ sessionKey,
3322
+ forceInitialHistory: Boolean(directThreadRoutedToDmSession),
3323
+ allowFromLower: threadContextAllowFromLower,
3324
+ allowNameMatching: ctx.allowNameMatching,
3325
+ contextVisibilityMode,
3326
+ envelopeOptions,
3327
+ effectiveDirectMedia
3328
+ });
3329
+ const effectiveMedia = effectiveDirectMedia ?? threadStarterMedia;
3330
+ const inboundHistory = isRoomish && ctx.historyLimit > 0 ? channelHistory.buildInboundHistory({
3331
+ historyKey,
3332
+ limit: ctx.historyLimit
3333
+ }) : dmHistoryContext.inboundHistory;
3334
+ const commandBody = textForCommandDetection.trim();
3335
+ const supplementalThreadHistoryBody = directThreadRoutedToDmSession && !threadHistoryBody ? threadStarterBody : threadHistoryBody;
3336
+ const effectiveMessageThreadId = assistantThreadContext?.threadTs ?? threadContext.messageThreadId;
3337
+ const ctxPayload = buildChannelInboundEventContext({
3338
+ channel: "slack",
3339
+ accountId: route.accountId,
3340
+ messageId: message.ts,
3341
+ timestamp: message.ts ? Math.round(Number(message.ts) * 1e3) : void 0,
3342
+ from: slackFrom,
3343
+ sender: {
3344
+ id: senderId,
3345
+ name: senderName,
3346
+ displayLabel: senderName
3347
+ },
3348
+ conversation: {
3349
+ kind: chatType,
3350
+ id: message.channel,
3351
+ label: envelopeFrom,
3352
+ spaceId: ctx.teamId || void 0,
3353
+ threadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
3354
+ nativeChannelId: message.channel
3355
+ },
3356
+ route: {
3357
+ agentId: route.agentId,
3358
+ accountId: route.accountId,
3359
+ routeSessionKey: sessionKey,
3360
+ parentSessionKey: threadKeys.parentSessionKey
3361
+ },
3362
+ reply: {
3363
+ to: slackTo,
3364
+ replyToId: threadContext.replyToId,
3365
+ messageThreadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
3366
+ nativeChannelId: message.channel
3367
+ },
3368
+ message: {
3369
+ inboundEventKind,
3370
+ body: combinedBody,
3371
+ bodyForAgent: rawBody,
3372
+ rawBody,
3373
+ commandBody,
3374
+ inboundHistory
3375
+ },
3376
+ access: {
3377
+ mentions: {
3378
+ canDetectMention: isRoomish,
3379
+ wasMentioned: effectiveWasMentioned,
3380
+ hasAnyMention: explicitlyMentioned || mentionedSubteamIds.length > 0,
3381
+ implicitMentionKinds: matchedImplicitMentionKinds,
3382
+ requireMention: shouldRequireMention,
3383
+ effectiveWasMentioned
3384
+ },
3385
+ commands: { authorized: commandAuthorized }
3386
+ },
3387
+ media: toInboundMediaFacts(effectiveMedia),
3388
+ supplemental: {
3389
+ thread: {
3390
+ starterBody: !directThreadRoutedToDmSession && !threadSessionPreviousTimestamp ? threadStarterBody : void 0,
3391
+ historyBody: supplementalThreadHistoryBody,
3392
+ label: directThreadRoutedToDmSession ? void 0 : threadLabel
3393
+ },
3394
+ groupSystemPrompt
3395
+ },
3396
+ extra: {
3397
+ GroupSubject: isRoomish ? roomLabel : void 0,
3398
+ UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : void 0,
3399
+ TransportThreadId: directThreadRoutedToDmSession ? threadContext.messageThreadId : void 0,
3400
+ SlackAssistantThread: assistantThreadContext ? true : void 0,
3401
+ SlackAssistantThreadContextChannelId: assistantThreadContext?.channelId,
3402
+ SlackAssistantThreadContextTeamId: assistantThreadContext?.teamId,
3403
+ SlackAssistantThreadContextEnterpriseId: assistantThreadContext?.enterpriseId ?? void 0,
3404
+ IsFirstThreadTurn: isThreadReply && threadTs && !directThreadRoutedToDmSession && !threadSessionPreviousTimestamp ? true : void 0,
3405
+ ...buildSlackMentionContextPayload({
3406
+ isRoomish,
3407
+ effectiveWasMentioned,
3408
+ explicitlyMentioned,
3409
+ mentionedUserIds,
3410
+ mentionedSubteamIds,
3411
+ matchedImplicitMentionKinds,
3412
+ mentionSource
3413
+ })
3414
+ }
3415
+ });
3416
+ if (isRoomish && !shouldRequireMention) channelHistory.record({
3417
+ historyKey,
3418
+ limit: ctx.historyLimit,
3419
+ entry: {
3420
+ sender: senderName,
3421
+ body: rawBody,
3422
+ timestamp: message.ts ? Math.round(Number(message.ts) * 1e3) : void 0,
3423
+ messageId: message.ts
3424
+ }
3425
+ });
3426
+ const pinnedMainDmOwner = isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({
3427
+ dmScope: cfg.session?.dmScope,
3428
+ allowFrom: ctx.allowFrom,
3429
+ normalizeEntry: normalizeSlackAllowOwnerEntry
3430
+ }) : null;
3431
+ const replyTarget = isDirectMessage ? `channel:${message.channel}` : ctxPayload.To ?? void 0;
3432
+ if (!replyTarget) return null;
3433
+ if (shouldLogVerbose()) logVerbose(`slack inbound: channel=${message.channel} from=${slackFrom} preview="${preview}"`);
3434
+ const updateLastRouteSessionKey = resolveInboundLastRouteSessionKey({
3435
+ route,
3436
+ sessionKey
3437
+ });
3438
+ return {
3439
+ ctx,
3440
+ account,
3441
+ message,
3442
+ route,
3443
+ channelConfig,
3444
+ replyTarget,
3445
+ ctxPayload,
3446
+ turn: {
3447
+ storePath,
3448
+ record: {
3449
+ updateLastRoute: isDirectMessage ? {
3450
+ sessionKey: updateLastRouteSessionKey,
3451
+ channel: "slack",
3452
+ to: `user:${message.user}`,
3453
+ accountId: route.accountId,
3454
+ threadId: effectiveMessageThreadId,
3455
+ mainDmOwnerPin: updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && message.user ? {
3456
+ ownerRecipient: pinnedMainDmOwner,
3457
+ senderRecipient: normalizeLowercaseStringOrEmpty(message.user),
3458
+ onSkip: ({ ownerRecipient, senderRecipient }) => {
3459
+ logVerbose(`slack: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`);
3460
+ }
3461
+ } : void 0
3462
+ } : void 0,
3463
+ onRecordError: (err) => {
3464
+ ctx.logger.warn({
3465
+ error: formatErrorMessage(err),
3466
+ storePath,
3467
+ sessionKey
3468
+ }, "failed updating session meta");
3469
+ }
3470
+ },
3471
+ history: isRoomish && shouldRequireMention ? {
3472
+ isGroup: true,
3473
+ historyKey,
3474
+ historyMap: ctx.channelHistories,
3475
+ limit: ctx.historyLimit
3476
+ } : void 0
3477
+ },
3478
+ replyToMode,
3479
+ ...assistantThreadContext?.threadTs ? { forcedReplyThreadTs: assistantThreadContext.threadTs } : {},
3480
+ ...assistantThreadContext ? { slackMessageMetadata: buildSlackAssistantThreadMetadata(assistantThreadContext) } : {},
3481
+ requireMention: shouldRequireMention,
3482
+ isDirectMessage,
3483
+ isRoomish,
3484
+ historyKey,
3485
+ preview,
3486
+ ackReactionMessageTs,
3487
+ ackReactionValue,
3488
+ ackReactionPromise
3489
+ };
3490
+ }
3491
+ //#endregion
3492
+ export { dispatchPreparedSlackMessage, prepareSlackMessage };