@openclaw/slack 2026.7.1 → 2026.7.2-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 (72) hide show
  1. package/dist/accounts.runtime-KvftRRek.js +2 -0
  2. package/dist/{action-runtime-DoQGk_8D.js → action-runtime-Cee2ih0s.js} +209 -30
  3. package/dist/action-runtime.runtime-DZfd7IFr.js +2 -0
  4. package/dist/{actions-DMpDmKEA.js → actions-BZiuSTIq.js} +88 -143
  5. package/dist/{actions.runtime-C103_BPW.js → actions.runtime-DoD0-gxw.js} +1 -1
  6. package/dist/api.js +14 -13
  7. package/dist/{approval-handler.runtime-CIUbOt5T.js → approval-handler.runtime-CUPtIM4k.js} +3 -3
  8. package/dist/{channel-BjlsaGHn.js → channel-DaIiGUOp.js} +119 -141
  9. package/dist/channel-config-api.js +1 -1
  10. package/dist/channel-plugin-api.js +1 -1
  11. package/dist/channel-type-Cfm6_6n1.js +109 -0
  12. package/dist/{channel.setup-Dq6syTNU.js → channel.setup-C-mLz50O.js} +3 -3
  13. package/dist/{client-DGxwSL7f.js → client-WIPAS3Yr.js} +46 -2
  14. package/dist/config-api-Df9a1B8C.js +3 -0
  15. package/dist/{config-schema-BzqzFW__.js → config-schema-xPSnK2VK.js} +32 -37
  16. package/dist/cursor-pages-DSmTg0su.js +13 -0
  17. package/dist/{directory-config-BZuS8141.js → directory-config-DRkFujwG.js} +2 -2
  18. package/dist/directory-contract-api.js +1 -1
  19. package/dist/{directory-live-BNsxCgqP.js → directory-live-D_yZhjgp.js} +2 -2
  20. package/dist/{doctor-contract-XL6HeNCb.js → doctor-contract-DddfkMlF.js} +25 -38
  21. package/dist/doctor-contract-api.js +1 -1
  22. package/dist/group-policy-85UWiriS.js +2345 -0
  23. package/dist/{interactive-replies-CXpT-vA_.js → interactive-replies-BilWUdcI.js} +5 -1
  24. package/dist/interactive-replies-api.js +1 -1
  25. package/dist/{message-tool-api-BgWKyu2r.js → message-tool-api-CA9jZrCY.js} +1 -1
  26. package/dist/message-tool-api.js +1 -1
  27. package/dist/{monitor-Ux-wzUWI.js → monitor-CalGciTs.js} +3 -3
  28. package/dist/outbound-adapter-BiLKcnyH.js +228 -0
  29. package/dist/{pipeline.runtime-rpVpay59.js → pipeline.runtime-DofAvTtb.js} +657 -221
  30. package/dist/{policy-Dj-puVPb.js → policy-BVs4LiSa.js} +7 -7
  31. package/dist/preflight-audio.runtime-D5Bgags0.js +3 -0
  32. package/dist/{probe-CuwRDE5j.js → probe-CN0ssfLN.js} +2 -2
  33. package/dist/{provider-C1-DFSpw.js → provider-CmeoUiIM.js} +907 -206
  34. package/dist/{relay-source-D2jv1iGg.js → relay-source-DMmgtvg4.js} +6 -17
  35. package/dist/replies-CJJCp6qz.js +420 -0
  36. package/dist/{resolve-channels-CstkChBE.js → resolve-channels-Rfts6POK.js} +17 -6
  37. package/dist/{resolve-users-C3NP2c-Q.js → resolve-users-y6wAxR8E.js} +11 -6
  38. package/dist/runtime-JSVZSWAj.js +8 -0
  39. package/dist/{runtime-api-CEmqYxR-.js → runtime-api-BGDyOt1D.js} +3 -3
  40. package/dist/runtime-api.js +11 -11
  41. package/dist/runtime-setter-api.js +1 -1
  42. package/dist/{scopes-DCWVD5-8.js → scopes-CuxNQZ6v.js} +2 -2
  43. package/dist/{secret-contract-C3ChHo5A.js → secret-contract-CMohXFWS.js} +18 -113
  44. package/dist/secret-contract-api.js +1 -1
  45. package/dist/{send-DKDXoeoV.js → send-DYp8ca-l.js} +662 -325
  46. package/dist/{send.runtime-GPRa63l1.js → send.runtime-CJLxAt26.js} +1 -1
  47. package/dist/{setup-core-fGd6Gquy.js → setup-core-CkwVLzFO.js} +15 -21
  48. package/dist/setup-plugin-api.js +1 -1
  49. package/dist/{setup-surface-Dv2fIJNB.js → setup-surface-oSd3NX9J.js} +4 -4
  50. package/dist/{shared-CosfcFri.js → shared-D9yz5Otg.js} +17 -38
  51. package/dist/{slash-dispatch.runtime-CcMbbT3_.js → slash-dispatch.runtime-CSja9VuD.js} +1 -1
  52. package/dist/{streaming-compat-DjlgH-Be.js → streaming-compat-3_X-21wW.js} +1 -0
  53. package/dist/{target-parsing-3jYH8LfB.js → target-parsing-N6lHogYZ.js} +16 -3
  54. package/dist/{targets-Dll4NJgs.js → targets-KmjP2E8s.js} +1 -1
  55. package/node_modules/p-map/index.d.ts +155 -0
  56. package/node_modules/p-map/index.js +284 -0
  57. package/node_modules/p-map/license +9 -0
  58. package/node_modules/p-map/package.json +57 -0
  59. package/node_modules/p-map/readme.md +190 -0
  60. package/npm-shrinkwrap.json +16 -3
  61. package/openclaw.plugin.json +84 -0
  62. package/package.json +7 -7
  63. package/dist/accounts.runtime-DWvaCb-X.js +0 -2
  64. package/dist/action-runtime.runtime-BABj3aM-.js +0 -2
  65. package/dist/config-api-DnT9ohtE.js +0 -2
  66. package/dist/configured-state.js +0 -11
  67. package/dist/format-YCNoNkM2.js +0 -107
  68. package/dist/outbound-adapter-DVoBfBW5.js +0 -272
  69. package/dist/replies-DpF38e1y.js +0 -287
  70. package/dist/reply-blocks-BGQ6zSqM.js +0 -877
  71. package/dist/resolve-allowlist-common-CP8SH0Zf.js +0 -43
  72. package/dist/runtime-BOk7xkOl.js +0 -8
@@ -1,13 +1,13 @@
1
1
  import { n as registerSlackHttpHandler, r as normalizeSlackWebhookPath } from "./registry-bbjH7IHX.js";
2
- import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-BOJJiHSr.js";
3
- import { B as isSlackExecApprovalAuthorizedSender, D as isSlackAnyNativeApprovalClientEnabled, R as isSlackApprovalAuthorizedSender, a as SLACK_TEXT_LIMIT, b as truncateSlackText, c as formatSlackError, d as normalizeAllowListLower, f as normalizeSlackAllowOwnerEntry, l as allowListMatches, m as resolveSlackAllowListMatch, p as normalizeSlackSlug, u as normalizeAllowList } from "./reply-blocks-BGQ6zSqM.js";
4
- import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-CXpT-vA_.js";
5
- import { c as resolveSlackWebClientOptions } from "./client-DGxwSL7f.js";
6
- import { t as getOptionalSlackRuntime } from "./runtime-BOk7xkOl.js";
7
- import { i as setSlackDefaultSendIdentity } from "./send-DKDXoeoV.js";
8
- import { t as resolveSlackChannelAllowlist } from "./resolve-channels-CstkChBE.js";
9
- import { t as resolveSlackUserAllowlist } from "./resolve-users-C3NP2c-Q.js";
10
- import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-Dj-puVPb.js";
2
+ import { a as resolveSlackAccount, d as formatSlackBotTokenIdentityWarning, f as resolveSlackAppToken, i as resolveDefaultSlackAccountId, o as resolveSlackAccountAllowFrom, p as resolveSlackBotToken, s as resolveSlackAccountDmPolicy } from "./accounts-BOJJiHSr.js";
3
+ import { B as truncateSlackText, G as isSlackCallbackActionId, I as SLACK_TEXT_LIMIT, J as decodeSlackApprovalAction, W as isSlackApprovalActionId, c as normalizeAllowListLower, d as resolveSlackAllowListMatch, et as isSlackAnyNativeApprovalClientEnabled, ft as isSlackExecApprovalAuthorizedSender, i as formatSlackError, l as normalizeSlackAllowOwnerEntry, o as allowListMatches, q as escapeSlackMrkdwn, s as normalizeAllowList, u as normalizeSlackSlug, ut as isSlackApprovalAuthorizedSender } from "./group-policy-85UWiriS.js";
4
+ import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-BilWUdcI.js";
5
+ import { d as resolveSlackWebClientOptions, r as createSlackStartupAuthClient, s as getSlackListenerUploadCompletionClient } from "./client-WIPAS3Yr.js";
6
+ import { n as getSlackRuntime, t as getOptionalSlackRuntime } from "./runtime-JSVZSWAj.js";
7
+ import { i as setSlackDefaultSendIdentity } from "./send-DYp8ca-l.js";
8
+ import { t as resolveSlackChannelAllowlist } from "./resolve-channels-Rfts6POK.js";
9
+ import { t as resolveSlackUserAllowlist } from "./resolve-users-y6wAxR8E.js";
10
+ import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-BVs4LiSa.js";
11
11
  import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
12
12
  import { asOptionalRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringEntriesLower, normalizeUniqueTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
13
13
  import { normalizeAccountId, normalizeMainKey, resolveAgentRoute, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
@@ -20,9 +20,10 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu
20
20
  import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
21
21
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
22
22
  import { addAllowlistUserEntriesFromConfigEntry, buildAllowlistResolutionSummary, formatAllowlistMatchMeta, mergeAllowlist, patchAllowlistUsersInConfigEntries, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
23
- import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
+ import { collectErrorGraphCandidates, formatErrorMessage, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
24
24
  import { asDateTimestampMs, parseFiniteNumber, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
25
25
  import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking";
26
+ import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
26
27
  import { createDedupeCache, createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
27
28
  import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, recordInboundSession, resolveConversationLabel as resolveConversationLabel$1, resolvePluginConversationBindingApproval, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
28
29
  import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
@@ -45,7 +46,6 @@ import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveH
45
46
  import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
46
47
  import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
47
48
  import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime";
48
- import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
49
49
  //#region extensions/slack/src/monitor/commands.ts
50
50
  /**
51
51
  * Strip Slack mentions (<@U123>, <@U123|name>) so command detection works on
@@ -94,8 +94,28 @@ function resolveSlackChatType(channelType) {
94
94
  return "channel";
95
95
  }
96
96
  //#endregion
97
+ //#region extensions/slack/src/monitor/lru-map-cache.ts
98
+ /** LRU-touch read: move hit to newest so pruneMapToMaxSize keeps active keys. */
99
+ function readLruMapEntry(cache, cacheKey) {
100
+ const cached = cache.get(cacheKey);
101
+ if (cached) {
102
+ cache.delete(cacheKey);
103
+ cache.set(cacheKey, cached);
104
+ }
105
+ return cached;
106
+ }
107
+ function writeLruMapEntry(cache, cacheKey, entry, maxEntries) {
108
+ cache.delete(cacheKey);
109
+ cache.set(cacheKey, entry);
110
+ pruneMapToMaxSize(cache, maxEntries);
111
+ }
112
+ //#endregion
97
113
  //#region extensions/slack/src/monitor/context.ts
98
114
  const SLACK_ASSISTANT_THREAD_CONTEXT_METADATA_EVENT = "assistant_thread_context";
115
+ const SLACK_CHANNEL_CACHE_MAX_ENTRIES = 1024;
116
+ const SLACK_USER_CACHE_MAX_ENTRIES = 2048;
117
+ const SLACK_CHANNEL_DENIAL_WARNING_TTL_MS = 5 * 6e4;
118
+ const SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES = 1024;
99
119
  function buildSlackAssistantThreadMetadata(context) {
100
120
  const eventPayload = {};
101
121
  if (context.channelId) eventPayload.channel_id = context.channelId;
@@ -134,23 +154,48 @@ function createSlackMonitorContext(params) {
134
154
  ttlMs: 6e4,
135
155
  maxSize: 500
136
156
  });
157
+ const channelDenialWarnings = createDedupeCache({
158
+ ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
159
+ maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
160
+ });
137
161
  const assistantThreadContexts = /* @__PURE__ */ new Map();
138
162
  let lastAssistantContextCleanupAt = Date.now();
139
163
  const allowFrom = normalizeAllowList(params.allowFrom);
140
164
  const groupDmChannels = normalizeAllowList(params.groupDmChannels);
141
- const groupDmChannelsLower = normalizeAllowListLower(groupDmChannels);
165
+ const groupDmChannelsLower = new Set(normalizeAllowListLower(groupDmChannels).map((entry) => entry.replace(/^channel:/, "")));
142
166
  const defaultRequireMention = params.defaultRequireMention ?? true;
143
167
  const hasChannelAllowlistConfig = Object.keys(params.channelsConfig ?? {}).length > 0;
144
168
  const channelsConfigKeys = Object.keys(params.channelsConfig ?? {});
145
- const markMessageSeen = (channelId, ts) => {
169
+ const scopedKey = (key, eventScope) => eventScope ? `${params.accountId}:${eventScope.teamId}:${key}` : key;
170
+ const rememberSlackChannelType = (channelId, channelType, eventScope) => {
171
+ const id = normalizeOptionalString(channelId);
172
+ const normalizedType = normalizeOptionalString(channelType)?.toLowerCase();
173
+ if (!id || normalizedType !== "im" && normalizedType !== "mpim" && normalizedType !== "channel" && normalizedType !== "group") return;
174
+ const cacheKey = scopedKey(id, eventScope);
175
+ const cached = readLruMapEntry(channelCache, cacheKey);
176
+ const type = normalizeSlackChannelType(normalizedType, id);
177
+ if (cached?.info.type === type) return;
178
+ writeLruMapEntry(channelCache, cacheKey, {
179
+ info: {
180
+ ...cached?.info,
181
+ type
182
+ },
183
+ metadataLoaded: cached?.metadataLoaded ?? false
184
+ }, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
185
+ };
186
+ const recallSlackChannelType = (channelId, eventScope) => {
187
+ const id = normalizeOptionalString(channelId);
188
+ return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
189
+ };
190
+ const markMessageSeen = (channelId, ts, eventScope) => {
146
191
  if (!channelId || !ts) return false;
147
- return seenMessages.check(`${channelId}:${ts}`);
192
+ return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
148
193
  };
149
- const releaseSeenMessage = (channelId, ts) => {
194
+ const releaseSeenMessage = (channelId, ts, eventScope) => {
150
195
  if (!channelId || !ts) return;
151
- seenMessages.delete(`${channelId}:${ts}`);
196
+ seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
152
197
  };
153
- const assistantContextKey = (channelId, threadTs) => `${channelId}:${threadTs}`;
198
+ const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
154
199
  const cleanupAssistantThreadContexts = () => {
155
200
  const now = Date.now();
156
201
  if (now - lastAssistantContextCleanupAt < SLACK_ASSISTANT_CONTEXT_CLEANUP_INTERVAL_MS) return;
@@ -158,9 +203,9 @@ function createSlackMonitorContext(params) {
158
203
  const cutoff = now - SLACK_ASSISTANT_CONTEXT_TTL_MS;
159
204
  for (const [key, entry] of assistantThreadContexts) if (entry.updatedAt < cutoff) assistantThreadContexts.delete(key);
160
205
  };
161
- const getSlackAssistantThreadContext = (channelId, threadTs) => {
206
+ const getSlackAssistantThreadContext = (channelId, threadTs, eventScope) => {
162
207
  if (!channelId || !threadTs) return;
163
- const key = assistantContextKey(channelId, threadTs);
208
+ const key = assistantContextKey(channelId, threadTs, eventScope);
164
209
  const entry = assistantThreadContexts.get(key);
165
210
  if (!entry) return;
166
211
  if (Date.now() - entry.updatedAt > SLACK_ASSISTANT_CONTEXT_TTL_MS) {
@@ -169,9 +214,9 @@ function createSlackMonitorContext(params) {
169
214
  }
170
215
  return entry;
171
216
  };
172
- const saveSlackAssistantThreadContext = (context) => {
217
+ const saveSlackAssistantThreadContext = (context, eventScope) => {
173
218
  cleanupAssistantThreadContexts();
174
- assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs), {
219
+ assistantThreadContexts.set(assistantContextKey(context.assistantChannelId, context.threadTs, eventScope), {
175
220
  ...context,
176
221
  updatedAt: Date.now()
177
222
  });
@@ -179,7 +224,7 @@ function createSlackMonitorContext(params) {
179
224
  const resolveSlackSystemEventSessionKey = (p) => {
180
225
  const channelId = normalizeOptionalString(p.channelId) ?? "";
181
226
  const senderId = normalizeOptionalString(p.senderId) ?? "";
182
- const channelType = normalizeSlackChannelType(p.channelType, channelId);
227
+ const channelType = normalizeSlackChannelType(p.channelType ?? recallSlackChannelType(channelId), channelId);
183
228
  const isDirectMessage = channelType === "im";
184
229
  if (!channelId && (!isDirectMessage || !senderId)) return params.mainKey;
185
230
  const isGroup = channelType === "mpim";
@@ -237,39 +282,47 @@ function createSlackMonitorContext(params) {
237
282
  parentSessionKey: normalizeOptionalString(p.threadTs) && params.threadInheritParent ? legacySessionKey : void 0
238
283
  }).sessionKey;
239
284
  };
240
- const resolveChannelName = async (channelId) => {
241
- const cached = channelCache.get(channelId);
242
- if (cached) return cached;
285
+ const resolveChannelName = async (channelId, eventScope) => {
286
+ const cacheKey = scopedKey(channelId, eventScope);
287
+ const cached = readLruMapEntry(channelCache, cacheKey);
288
+ if (cached?.metadataLoaded) return cached.info;
243
289
  try {
244
- const info = await params.app.client.conversations.info({
290
+ const info = await (eventScope?.client ?? params.app.client).conversations.info({
245
291
  token: params.botToken,
246
292
  channel: channelId
247
293
  });
248
294
  const name = info.channel && "name" in info.channel ? info.channel.name : void 0;
249
295
  const channel = info.channel ?? void 0;
296
+ const type = channel?.is_im ? "im" : channel?.is_mpim ? "mpim" : channel?.is_channel ? "channel" : channel?.is_group ? "group" : void 0;
297
+ const topic = channel && "topic" in channel ? channel.topic?.value ?? void 0 : void 0;
298
+ const purpose = channel && "purpose" in channel ? channel.purpose?.value ?? void 0 : void 0;
250
299
  const entry = {
251
- name,
252
- type: channel?.is_im ? "im" : channel?.is_mpim ? "mpim" : channel?.is_channel ? "channel" : channel?.is_group ? "group" : void 0,
253
- topic: channel && "topic" in channel ? channel.topic?.value ?? void 0 : void 0,
254
- purpose: channel && "purpose" in channel ? channel.purpose?.value ?? void 0 : void 0
300
+ info: {
301
+ name,
302
+ type: cached?.info.type ?? type,
303
+ topic,
304
+ purpose
305
+ },
306
+ metadataLoaded: true
255
307
  };
256
- channelCache.set(channelId, entry);
257
- return entry;
308
+ writeLruMapEntry(channelCache, cacheKey, entry, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
309
+ return entry.info;
258
310
  } catch {
259
- return {};
311
+ return cached?.info ?? {};
260
312
  }
261
313
  };
262
- const resolveUserName = async (userId) => {
263
- const cached = userCache.get(userId);
314
+ const resolveUserName = async (userId, eventScope) => {
315
+ const cacheKey = scopedKey(userId, eventScope);
316
+ const cached = readLruMapEntry(userCache, cacheKey);
264
317
  if (cached) return cached;
265
318
  try {
266
- const info = await params.app.client.users.info({
319
+ const info = await (eventScope?.client ?? params.app.client).users.info({
267
320
  token: params.botToken,
268
321
  user: userId
269
322
  });
270
323
  const profile = info.user?.profile;
271
324
  const entry = { name: profile?.display_name || profile?.real_name || info.user?.name || void 0 };
272
- userCache.set(userId, entry);
325
+ writeLruMapEntry(userCache, cacheKey, entry, SLACK_USER_CACHE_MAX_ENTRIES);
273
326
  return entry;
274
327
  } catch {
275
328
  return {};
@@ -278,7 +331,7 @@ function createSlackMonitorContext(params) {
278
331
  const setSlackThreadStatus = async (p) => {
279
332
  if (!p.threadTs) return;
280
333
  try {
281
- await params.app.client.assistant.threads.setStatus({
334
+ await (p.eventScope?.client ?? params.app.client).assistant.threads.setStatus({
282
335
  token: params.botToken,
283
336
  channel_id: p.channelId,
284
337
  thread_ts: p.threadTs,
@@ -323,7 +376,7 @@ function createSlackMonitorContext(params) {
323
376
  p.channelName,
324
377
  p.channelName ? normalizeSlackSlug(p.channelName) : void 0
325
378
  ].filter((value) => Boolean(value)).map((value) => normalizeLowercaseStringOrEmpty(value));
326
- if (!(groupDmChannelsLower.includes("*") || candidates.some((candidate) => groupDmChannelsLower.includes(candidate)))) return false;
379
+ if (!(groupDmChannelsLower.has("*") || candidates.some((candidate) => groupDmChannelsLower.has(candidate)))) return false;
327
380
  }
328
381
  if (isRoom && p.channelId) {
329
382
  const channelConfig = resolveSlackChannelConfig({
@@ -337,19 +390,33 @@ function createSlackMonitorContext(params) {
337
390
  const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
338
391
  const channelAllowed = channelConfig?.allowed !== false;
339
392
  const channelAllowlistConfigured = hasChannelAllowlistConfig;
340
- if (!isSlackChannelAllowedByPolicy({
393
+ const allowedByPolicy = isSlackChannelAllowedByPolicy({
341
394
  groupPolicy: params.groupPolicy,
342
395
  channelAllowlistConfigured,
343
396
  channelAllowed
344
- })) {
397
+ });
398
+ const explicitlyDisabled = params.groupPolicy !== "disabled" && channelConfig?.allowed === false && channelConfig.matchSource !== void 0;
399
+ if (!allowedByPolicy || params.groupPolicy === "open" && explicitlyDisabled) {
400
+ if (explicitlyDisabled) {
401
+ const reason = "channel_not_allowed";
402
+ const warningKey = `${params.accountId}:${p.channelId}:${reason}`;
403
+ if (!channelDenialWarnings.peek(warningKey)) {
404
+ channelDenialWarnings.check(warningKey);
405
+ logger.warn({
406
+ provider: "slack",
407
+ accountId: params.accountId,
408
+ channelId: p.channelId,
409
+ reason,
410
+ cause: "channel_disabled",
411
+ groupPolicy: params.groupPolicy,
412
+ matchSource: channelConfig.matchSource,
413
+ matchKey: channelConfig.matchKey
414
+ }, "Slack channel denied by configuration");
415
+ }
416
+ }
345
417
  logVerbose(`slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`);
346
418
  return false;
347
419
  }
348
- const hasExplicitConfig = Boolean(channelConfig?.matchSource);
349
- if (!channelAllowed && (params.groupPolicy !== "open" || hasExplicitConfig)) {
350
- logVerbose(`slack: drop channel ${p.channelId} (${channelMatchMeta})`);
351
- return false;
352
- }
353
420
  logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
354
421
  }
355
422
  return true;
@@ -380,6 +447,10 @@ function createSlackMonitorContext(params) {
380
447
  botId: params.botId,
381
448
  teamId: params.teamId,
382
449
  apiAppId: params.apiAppId,
450
+ installationIdentity: params.installationIdentity ?? {
451
+ kind: "degraded",
452
+ reason: "auth_test_failed"
453
+ },
383
454
  historyLimit: params.historyLimit,
384
455
  dmHistoryLimit: Math.max(0, params.dmHistoryLimit ?? 0),
385
456
  channelHistories,
@@ -415,6 +486,8 @@ function createSlackMonitorContext(params) {
415
486
  resolveSlackSystemEventSessionKey,
416
487
  isChannelAllowed,
417
488
  resolveChannelName,
489
+ rememberSlackChannelType,
490
+ recallSlackChannelType,
418
491
  resolveUserName,
419
492
  setSlackThreadStatus,
420
493
  getSlackAssistantThreadContext,
@@ -423,6 +496,135 @@ function createSlackMonitorContext(params) {
423
496
  };
424
497
  }
425
498
  //#endregion
499
+ //#region extensions/slack/src/monitor/enterprise-install.ts
500
+ const SLACK_CHANNEL_ID_RE = /^[CDG][A-Z0-9]{8,}$/;
501
+ const SLACK_USER_ID_RE = /^[UW][A-Z0-9]{8,}$/;
502
+ function isStableSlackChannelEntry(value, options) {
503
+ if (typeof value !== "string") return false;
504
+ const normalized = value.trim();
505
+ if (normalized === "*") return options?.allowWildcard === true;
506
+ if (/^channel:([CDG][A-Z0-9]{8,})$/.exec(normalized)?.[1]) return true;
507
+ return SLACK_CHANNEL_ID_RE.test(normalized);
508
+ }
509
+ function isStableSlackAllowlistUserEntry(value) {
510
+ if (typeof value !== "string") return false;
511
+ const normalized = value.trim();
512
+ if (normalized === "*") return true;
513
+ const prefixed = /^(?:slack|user):([UW][A-Z0-9]{8,})$/.exec(normalized);
514
+ return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
515
+ }
516
+ function isStableSlackToolsBySenderEntry(value) {
517
+ if (typeof value !== "string") return false;
518
+ const normalized = value.trim();
519
+ if (normalized === "*") return true;
520
+ const prefixed = /^(?:id:|channel:slack:)([UW][A-Z0-9]{8,})$/.exec(normalized);
521
+ return Boolean(prefixed?.[1]) || SLACK_USER_ID_RE.test(normalized);
522
+ }
523
+ function assertStableEntries(params) {
524
+ const invalid = params.values?.find((value) => !params.predicate(value));
525
+ if (invalid !== void 0) throw new Error(`Slack Enterprise Grid org installs require stable Slack IDs in ${params.path}; invalid entry ${JSON.stringify(invalid)}`);
526
+ }
527
+ /** Validate every policy surface that would otherwise require name resolution. */
528
+ function assertEnterpriseSlackPolicyConfig(params) {
529
+ const { config, accountId } = params;
530
+ if (config.dangerouslyAllowNameMatching === true) throw new Error(`Slack Enterprise Grid org account "${accountId}" cannot use dangerouslyAllowNameMatching`);
531
+ if (config.mentionPatterns?.allowIn !== void 0 || config.mentionPatterns?.denyIn !== void 0) throw new Error(`Slack Enterprise Grid org account "${accountId}" cannot use mentionPatterns.allowIn or mentionPatterns.denyIn because Slack channel IDs are not workspace-qualified`);
532
+ assertStableEntries({
533
+ values: config.allowFrom,
534
+ path: `channels.slack.accounts.${accountId}.allowFrom`,
535
+ predicate: isStableSlackAllowlistUserEntry
536
+ });
537
+ assertStableEntries({
538
+ values: config.dm?.allowFrom,
539
+ path: `channels.slack.accounts.${accountId}.dm.allowFrom`,
540
+ predicate: isStableSlackAllowlistUserEntry
541
+ });
542
+ assertStableEntries({
543
+ values: config.dm?.groupChannels,
544
+ path: `channels.slack.accounts.${accountId}.dm.groupChannels`,
545
+ predicate: (value) => isStableSlackChannelEntry(value)
546
+ });
547
+ if (config.reactionNotifications === "allowlist") assertStableEntries({
548
+ values: config.reactionAllowlist,
549
+ path: `channels.slack.accounts.${accountId}.reactionAllowlist`,
550
+ predicate: isStableSlackAllowlistUserEntry
551
+ });
552
+ for (const [channelKey, channel] of Object.entries(config.channels ?? {})) {
553
+ if (!isStableSlackChannelEntry(channelKey, { allowWildcard: true })) throw new Error(`Slack Enterprise Grid org installs require stable Slack channel IDs; invalid channels key ${JSON.stringify(channelKey)}`);
554
+ assertStableEntries({
555
+ values: channel?.users,
556
+ path: `channels.slack.accounts.${accountId}.channels.${channelKey}.users`,
557
+ predicate: isStableSlackAllowlistUserEntry
558
+ });
559
+ assertStableEntries({
560
+ values: Object.keys(channel?.toolsBySender ?? {}),
561
+ path: `channels.slack.accounts.${accountId}.channels.${channelKey}.toolsBySender`,
562
+ predicate: isStableSlackToolsBySenderEntry
563
+ });
564
+ }
565
+ }
566
+ /** Prevent account-wide user authorization state from crossing workspace boundaries. */
567
+ function assertEnterpriseSlackDmPolicy(params) {
568
+ if (!params.dmEnabled || params.dmPolicy === "disabled") return;
569
+ if (params.dmPolicy === "open" && params.allowFrom?.includes("*")) return;
570
+ throw new Error(`Slack Enterprise Grid org account "${params.accountId}" supports DMs only with dm.enabled=false, dmPolicy="disabled", or dmPolicy="open" with effective allowFrom containing "*"; dmPolicy=${JSON.stringify(params.dmPolicy)} and allowFrom=${JSON.stringify(params.allowFrom ?? [])} would share per-user authorization across workspaces`);
571
+ }
572
+ function assertNoEnterpriseSlackBindings(params) {
573
+ const defaultAccountId = resolveDefaultSlackAccountId(params.cfg);
574
+ if (params.cfg.bindings?.find((binding) => {
575
+ if (binding.match.channel.trim().toLowerCase() !== "slack") return false;
576
+ const accountId = binding.match.accountId?.trim();
577
+ return accountId === "*" || accountId === params.accountId || !accountId && params.accountId === defaultAccountId;
578
+ })) throw new Error(`Slack Enterprise Grid org account "${params.accountId}" cannot use configured Slack bindings`);
579
+ }
580
+ function resolveSlackInstallationIdentity(params) {
581
+ const auth = params.auth;
582
+ if (!auth) {
583
+ if (params.enterpriseOrgInstall) throw new Error(`Slack enterpriseOrgInstall=true requires a successful auth.test (${formatSlackError(params.authError)})`);
584
+ return {
585
+ kind: "degraded",
586
+ reason: "auth_test_failed"
587
+ };
588
+ }
589
+ const isEnterpriseInstall = auth.is_enterprise_install === true;
590
+ if (isEnterpriseInstall !== params.enterpriseOrgInstall) throw new Error(isEnterpriseInstall ? "Slack auth.test detected an org-wide installation; set enterpriseOrgInstall=true" : "Slack enterpriseOrgInstall=true requires an org-wide bot installation");
591
+ const apiAppId = normalizeOptionalString(auth.app_id);
592
+ const enterpriseId = normalizeOptionalString(auth.enterprise_id);
593
+ if (params.enterpriseOrgInstall) {
594
+ if (!enterpriseId) throw new Error("Slack org-wide auth.test returned no enterprise_id");
595
+ const transportApiAppId = normalizeOptionalString(params.transportApiAppId);
596
+ if (apiAppId && transportApiAppId && apiAppId !== transportApiAppId) throw new Error(`Slack token mismatch: bot token app_id=${apiAppId} but transport app_id=${transportApiAppId}`);
597
+ const effectiveApiAppId = apiAppId ?? transportApiAppId;
598
+ return {
599
+ kind: "enterprise",
600
+ ...effectiveApiAppId ? { apiAppId: effectiveApiAppId } : {},
601
+ enterpriseId
602
+ };
603
+ }
604
+ const teamId = normalizeOptionalString(auth.team_id);
605
+ if (!teamId) throw new Error("Slack workspace auth.test returned no team_id");
606
+ return {
607
+ kind: "workspace",
608
+ teamId,
609
+ ...apiAppId ? { apiAppId } : {},
610
+ ...enterpriseId ? { enterpriseId } : {}
611
+ };
612
+ }
613
+ function resolveSlackIdentityHealth(params) {
614
+ if (params.installationIdentity.kind === "enterprise") return {
615
+ healthState: "healthy",
616
+ lastError: null
617
+ };
618
+ const lastError = normalizeOptionalString(params.authTestError) ?? normalizeOptionalString(params.authIdentityWarning) ?? (params.installationIdentity.kind === "degraded" || !params.botUserId.trim() ? "slack bot identity unavailable" : void 0);
619
+ return lastError ? {
620
+ healthState: "degraded",
621
+ lastError
622
+ } : {
623
+ healthState: "healthy",
624
+ lastError: null
625
+ };
626
+ }
627
+ //#endregion
426
628
  //#region extensions/slack/src/monitor/events/assistant.ts
427
629
  const DEFAULT_ASSISTANT_PROMPTS = [
428
630
  {
@@ -557,7 +759,12 @@ function migrateSlackChannelsInPlace(channels, oldChannelId, newChannelId) {
557
759
  migrated: false,
558
760
  skippedExisting: true
559
761
  };
560
- channels[newChannelId] = channels[oldChannelId];
762
+ const channelConfig = channels[oldChannelId];
763
+ if (!channelConfig) return {
764
+ migrated: false,
765
+ skippedExisting: false
766
+ };
767
+ channels[newChannelId] = channelConfig;
561
768
  delete channels[oldChannelId];
562
769
  return {
563
770
  migrated: true,
@@ -702,7 +909,7 @@ function registerSlackChannelEvents(params) {
702
909
  }
703
910
  //#endregion
704
911
  //#region extensions/slack/src/monitor/events/home.ts
705
- function buildSlackHomeView() {
912
+ function buildSlackHomeView(slashCommandName) {
706
913
  return {
707
914
  type: "home",
708
915
  callback_id: "openclaw:home",
@@ -718,7 +925,7 @@ function buildSlackHomeView() {
718
925
  type: "section",
719
926
  text: {
720
927
  type: "mrkdwn",
721
- text: "Send a DM, mention OpenClaw in a channel, or use `/openclaw` to start a session."
928
+ text: slashCommandName ? `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.` : "Send a DM or mention OpenClaw in a channel to start a session."
722
929
  }
723
930
  },
724
931
  {
@@ -732,7 +939,7 @@ function buildSlackHomeView() {
732
939
  };
733
940
  }
734
941
  function registerSlackHomeEvents(params) {
735
- const { ctx, trackEvent } = params;
942
+ const { ctx, slashCommandName, trackEvent } = params;
736
943
  ctx.app.event("app_home_opened", async ({ event, body }) => {
737
944
  try {
738
945
  if (ctx.shouldDropMismatchedSlackEvent(body)) return;
@@ -742,7 +949,7 @@ function registerSlackHomeEvents(params) {
742
949
  await ctx.app.client.views.publish({
743
950
  token: ctx.botToken,
744
951
  user_id: payload.user,
745
- view: buildSlackHomeView()
952
+ view: buildSlackHomeView(slashCommandName)
746
953
  });
747
954
  } catch (err) {
748
955
  ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
@@ -783,7 +990,7 @@ async function dispatchSlackPluginInteractiveHandler(params) {
783
990
  }
784
991
  //#endregion
785
992
  //#region extensions/slack/src/monitor/auth.ts
786
- let slackChannelMembersCache = /* @__PURE__ */ new WeakMap();
993
+ const slackChannelMembersCache = /* @__PURE__ */ new WeakMap();
787
994
  const DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS = 6e4;
788
995
  const CHANNEL_MEMBERS_CACHE_MAX = 512;
789
996
  const SLACK_CHANNEL_ID = "slack";
@@ -891,11 +1098,11 @@ async function resolveSlackEffectiveAllowFrom(ctx, options) {
891
1098
  }
892
1099
  return normalizeAllowListLower([...base, ...storeAllowFrom]);
893
1100
  }
894
- async function fetchSlackChannelMemberIds(ctx, channelId) {
1101
+ async function fetchSlackChannelMemberIds(ctx, channelId, eventScope) {
895
1102
  const members = /* @__PURE__ */ new Set();
896
1103
  let cursor;
897
1104
  do {
898
- const response = await ctx.app.client.conversations.members({
1105
+ const response = await (eventScope?.client ?? ctx.app.client).conversations.members({
899
1106
  token: ctx.botToken,
900
1107
  channel: channelId,
901
1108
  limit: 999,
@@ -907,9 +1114,9 @@ async function fetchSlackChannelMemberIds(ctx, channelId) {
907
1114
  } while (cursor);
908
1115
  return members;
909
1116
  }
910
- async function resolveSlackChannelMemberIds(ctx, channelId) {
1117
+ async function resolveSlackChannelMemberIds(ctx, channelId, eventScope) {
911
1118
  const cache = getChannelMembersCache(ctx);
912
- const key = `${ctx.accountId}:${channelId}`;
1119
+ const key = `${ctx.accountId}:${eventScope ? `${eventScope.teamId}:` : ""}${channelId}`;
913
1120
  const ttlMs = readSlackCacheTtlMs("OPENCLAW_SLACK_CHANNEL_MEMBERS_CACHE_TTL_MS", DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS);
914
1121
  const rawNowMs = Date.now();
915
1122
  const nowMs = asDateTimestampMs(rawNowMs);
@@ -919,7 +1126,7 @@ async function resolveSlackChannelMemberIds(ctx, channelId) {
919
1126
  cache.delete(key);
920
1127
  }
921
1128
  if (cached?.pending) return await cached.pending;
922
- const pending = fetchSlackChannelMemberIds(ctx, channelId);
1129
+ const pending = fetchSlackChannelMemberIds(ctx, channelId, eventScope);
923
1130
  const pendingExpiresAtMs = ttlMs > 0 ? resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs }) : void 0;
924
1131
  cache.set(key, {
925
1132
  expiresAtMs: pendingExpiresAtMs ?? 0,
@@ -963,7 +1170,7 @@ async function authorizeSlackBotRoomMessage(params) {
963
1170
  return false;
964
1171
  }
965
1172
  try {
966
- const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId);
1173
+ const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId, params.eventScope);
967
1174
  if (explicitOwnerIds.some((ownerId) => channelMemberIds.has(ownerId))) return true;
968
1175
  logVerbose(`slack: drop bot message ${params.senderId} in ${params.channelId} (no owner present)`);
969
1176
  } catch (error) {
@@ -979,8 +1186,10 @@ function slackIngressConversationKind(channelType) {
979
1186
  }
980
1187
  async function resolveSlackCommandIngress(params) {
981
1188
  const isDirectMessage = params.channelType === "im";
1189
+ const isGroupDm = params.channelType === "mpim";
982
1190
  const channelUsers = normalizeAllowListLower(params.channelUsers);
983
- const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
1191
+ const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
1192
+ const groupAllowFrom = isGroupDm ? params.ownerAllowFromLower : channelUsersConfigured ? channelUsers : [];
984
1193
  return await createSlackIngressResolver(params.ctx).message({
985
1194
  subject: createSlackIngressSubject({
986
1195
  senderId: params.senderId,
@@ -996,7 +1205,7 @@ async function resolveSlackCommandIngress(params) {
996
1205
  mayPair: false
997
1206
  },
998
1207
  dmPolicy: isDirectMessage ? "open" : "disabled",
999
- groupPolicy: channelUsersConfigured ? "allowlist" : "open",
1208
+ groupPolicy: isGroupDm || channelUsersConfigured ? "allowlist" : "open",
1000
1209
  policy: {
1001
1210
  groupAllowFromFallbackToAllowFrom: false,
1002
1211
  mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled",
@@ -1004,7 +1213,7 @@ async function resolveSlackCommandIngress(params) {
1004
1213
  },
1005
1214
  mentionFacts: params.mentionFacts,
1006
1215
  allowFrom: isDirectMessage ? ["*"] : params.ownerAllowFromLower,
1007
- groupAllowFrom: channelUsersConfigured ? channelUsers : [],
1216
+ groupAllowFrom,
1008
1217
  command: {
1009
1218
  allowTextCommands: params.allowTextCommands,
1010
1219
  hasControlCommand: params.hasControlCommand,
@@ -1015,12 +1224,14 @@ async function resolveSlackCommandIngress(params) {
1015
1224
  }
1016
1225
  async function decideSlackSystemIngress(params) {
1017
1226
  const isDirectMessage = params.channelType === "im";
1227
+ const isGroupDm = params.channelType === "mpim";
1018
1228
  const channelUsers = normalizeAllowListLower(params.channelUsers);
1019
- const channelUsersConfigured = !isDirectMessage && channelUsers.length > 0;
1229
+ const channelUsersConfigured = !isDirectMessage && !isGroupDm && channelUsers.length > 0;
1020
1230
  const ownerAllowFrom = params.interactiveEvent && channelUsersConfigured ? params.ownerAllowFromLower.filter((entry) => entry !== "*") : params.ownerAllowFromLower;
1021
1231
  const hasAnyCommandAllowlist = ownerAllowFrom.length > 0 || channelUsersConfigured;
1022
1232
  const groupAllowFrom = (() => {
1023
1233
  if (isDirectMessage) return [];
1234
+ if (isGroupDm) return ownerAllowFrom;
1024
1235
  if (params.interactiveEvent && hasAnyCommandAllowlist) return channelUsersConfigured ? channelUsers : [];
1025
1236
  if (channelUsersConfigured) return channelUsers;
1026
1237
  return params.channelId ? ["*"] : wildcardWhenOpen(params.ownerAllowFromLower);
@@ -1040,7 +1251,7 @@ async function decideSlackSystemIngress(params) {
1040
1251
  mayPair: false
1041
1252
  },
1042
1253
  dmPolicy: isDirectMessage ? "open" : "disabled",
1043
- groupPolicy: params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
1254
+ groupPolicy: isGroupDm ? "allowlist" : params.interactiveEvent && hasAnyCommandAllowlist ? "open" : channelUsersConfigured || !params.channelId && params.ownerAllowFromLower.length > 0 ? "allowlist" : "open",
1044
1255
  policy: {
1045
1256
  groupAllowFromFallbackToAllowFrom: false,
1046
1257
  mutableIdentifierMatching: params.ctx.allowNameMatching ? "enabled" : "disabled"
@@ -1149,11 +1360,6 @@ async function authorizeSlackSystemEventSender(params) {
1149
1360
  };
1150
1361
  }
1151
1362
  //#endregion
1152
- //#region extensions/slack/src/monitor/mrkdwn.ts
1153
- function escapeSlackMrkdwn(value) {
1154
- return value.replaceAll("\\", "\\\\").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replace(/([*_`~])/g, "\\$1");
1155
- }
1156
- //#endregion
1157
1363
  //#region extensions/slack/src/monitor/events/interactions.block-actions.ts
1158
1364
  function readOptionValues(options) {
1159
1365
  if (!Array.isArray(options)) return;
@@ -1235,9 +1441,6 @@ function summarizeAction(action) {
1235
1441
  workflowId: typed.workflow?.workflow_id
1236
1442
  };
1237
1443
  }
1238
- function isBulkActionsBlock(block) {
1239
- return block.type === "actions" && Array.isArray(block.elements) && block.elements.length > 0 && block.elements.every((el) => typeof el.action_id === "string" && el.action_id.includes("_all_"));
1240
- }
1241
1444
  function formatInteractionSelectionLabel(params) {
1242
1445
  if (params.summary.actionType === "button" && params.buttonText?.trim()) return params.buttonText.trim();
1243
1446
  if (params.summary.selectedLabels?.length) {
@@ -1267,12 +1470,15 @@ function buildSlackPluginInteractionData(params) {
1267
1470
  const actionId = normalizeOptionalString(params.actionId) ?? "";
1268
1471
  if (!actionId) return null;
1269
1472
  const payload = normalizeOptionalString(params.summary.value) || params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || "";
1270
- if (actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`)) return payload || null;
1473
+ if (actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || isSlackCallbackActionId(actionId) || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`)) return payload || null;
1271
1474
  return payload ? `${actionId}:${payload}` : actionId;
1272
1475
  }
1273
1476
  function isSlackReplyActionId(actionId) {
1274
1477
  return actionId === "openclaw:reply_button" || actionId === "openclaw:reply_select" || actionId.startsWith(`openclaw:reply_button:`) || actionId.startsWith(`openclaw:reply_select:`);
1275
1478
  }
1479
+ function readSlackApprovalAction(parsed) {
1480
+ return decodeSlackApprovalAction(normalizeOptionalString(parsed.actionSummary.value) ?? parsed.actionSummary.selectedValues?.map((entry) => normalizeOptionalString(entry)).find((entry) => Boolean(entry)));
1481
+ }
1276
1482
  function isSlackReplyLinkAction(parsed) {
1277
1483
  if (parsed.actionId === "openclaw:reply_link" || parsed.actionId.startsWith(`openclaw:reply_link:`)) return true;
1278
1484
  const legacyUrl = normalizeOptionalString(parsed.typedAction.url);
@@ -1328,6 +1534,37 @@ async function updateSlackInteractionMessage(params) {
1328
1534
  ...params.blocks ? { blocks: params.blocks } : {}
1329
1535
  });
1330
1536
  }
1537
+ function resolveSlackApprovalTerminalLabel(approval) {
1538
+ if (approval.status === "allowed") return approval.decision === "allow-always" ? "Allowed always" : "Allowed once";
1539
+ if (approval.status === "denied") return "Denied";
1540
+ if (approval.status === "expired") return "Expired";
1541
+ return "Cancelled";
1542
+ }
1543
+ function removeSlackApprovalControls(blocks) {
1544
+ return blocks.flatMap((block) => {
1545
+ if (!block || typeof block !== "object" || Array.isArray(block)) return [block];
1546
+ const typedBlock = block;
1547
+ if (typedBlock.type !== "actions" || !Array.isArray(typedBlock.elements)) return [block];
1548
+ const elements = typedBlock.elements.filter((element) => typeof element.action_id !== "string" || !isSlackApprovalActionId(element.action_id));
1549
+ return elements.length > 0 ? [{
1550
+ ...block,
1551
+ elements
1552
+ }] : [];
1553
+ });
1554
+ }
1555
+ function buildSlackApprovalTerminalBlocks(params) {
1556
+ const blocks = removeSlackApprovalControls(params.blocks ?? []).filter((block) => {
1557
+ const text = block.text?.text;
1558
+ return !(block.type === "section" && typeof text === "string" && /^\*(?:Exec|Plugin) approval required\*/u.test(text));
1559
+ });
1560
+ return [{
1561
+ type: "section",
1562
+ text: {
1563
+ type: "mrkdwn",
1564
+ text: `*${params.prefix}: ${params.label}*`
1565
+ }
1566
+ }, ...blocks];
1567
+ }
1331
1568
  async function authorizeSlackBlockAction(params) {
1332
1569
  const auth = await authorizeSlackSystemEventSender({
1333
1570
  ctx: params.ctx,
@@ -1361,9 +1598,7 @@ async function handleSlackPluginBindingApproval(params) {
1361
1598
  await respondEphemeral(params.respond, buildPluginBindingResolvedText(resolved));
1362
1599
  return true;
1363
1600
  }
1364
- async function handleSlackExecApprovalInteraction(params) {
1365
- const approval = parseExecApprovalCommandText(params.pluginInteractionData);
1366
- if (!approval) return false;
1601
+ async function handleSlackApprovalInteraction(params) {
1367
1602
  const pluginApprovalAuthorizedSender = isSlackApprovalAuthorizedSender({
1368
1603
  cfg: params.ctx.cfg,
1369
1604
  accountId: params.ctx.accountId,
@@ -1374,38 +1609,95 @@ async function handleSlackExecApprovalInteraction(params) {
1374
1609
  accountId: params.ctx.accountId,
1375
1610
  senderId: params.parsed.userId
1376
1611
  });
1377
- const isPluginApproval = approval.approvalId.startsWith("plugin:");
1378
- const resolveUnprefixedAsPlugin = !isPluginApproval && !execApprovalAuthorizedSender && pluginApprovalAuthorizedSender;
1379
- const authorized = isPluginApproval ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender || resolveUnprefixedAsPlugin;
1380
- const allowPluginFallback = !isPluginApproval && execApprovalAuthorizedSender && pluginApprovalAuthorizedSender;
1381
- if (!authorized) {
1382
- params.ctx.runtime.log?.(`slack:interaction drop exec approval user=${params.parsed.userId} (not authorized)`);
1612
+ if (!(params.approval.approvalKind === "plugin" ? pluginApprovalAuthorizedSender : execApprovalAuthorizedSender)) {
1613
+ params.ctx.runtime.log?.(`slack:interaction drop ${params.approval.approvalKind} approval user=${params.parsed.userId} (not authorized)`);
1383
1614
  await respondEphemeral(params.respond, "You are not authorized to approve this request.");
1384
1615
  return true;
1385
1616
  }
1386
1617
  try {
1387
- await resolveApprovalOverGateway({
1618
+ const result = await resolveApprovalOverGateway({
1388
1619
  cfg: params.ctx.cfg,
1389
- approvalId: approval.approvalId,
1390
- decision: approval.decision,
1620
+ approvalId: params.approval.approvalId,
1621
+ approvalKind: params.approval.approvalKind,
1622
+ decision: params.approval.decision,
1391
1623
  senderId: params.parsed.userId,
1392
- allowPluginFallback,
1393
- ...resolveUnprefixedAsPlugin ? { resolveMethod: "plugin" } : {},
1394
1624
  clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
1395
1625
  });
1626
+ const terminalLabel = resolveSlackApprovalTerminalLabel(result.approval);
1627
+ const prefix = result.applied ? "Resolved" : "Already resolved";
1628
+ let terminalized = false;
1629
+ try {
1630
+ const terminalText = `${prefix}: ${terminalLabel}`;
1631
+ await updateSlackInteractionMessage({
1632
+ ctx: params.ctx,
1633
+ channelId: params.parsed.channelId,
1634
+ messageTs: params.parsed.messageTs,
1635
+ text: truncateSlackText(terminalText, 4e3),
1636
+ blocks: buildSlackApprovalTerminalBlocks({
1637
+ blocks: params.parsed.typedBody.message?.blocks,
1638
+ label: terminalLabel,
1639
+ prefix
1640
+ })
1641
+ });
1642
+ terminalized = true;
1643
+ } catch {}
1644
+ if (!terminalized || !result.applied) await respondEphemeral(params.respond, result.applied ? `Approval resolved: ${terminalLabel}.` : `This approval was already resolved: ${terminalLabel}.`);
1396
1645
  } catch (error) {
1397
- params.ctx.runtime.log?.(`slack:interaction exec approval resolve failed id=${approval.approvalId}: ${String(error)}`);
1646
+ params.ctx.runtime.log?.(`slack:interaction approval resolve failed id=${params.approval.approvalId}: ${String(error)}`);
1647
+ if (isApprovalNotFoundError(error)) {
1648
+ await respondEphemeral(params.respond, "This approval is no longer pending.");
1649
+ return true;
1650
+ }
1651
+ await respondEphemeral(params.respond, "Could not reach the Gateway to resolve this approval. Try again.");
1398
1652
  throw error;
1399
1653
  }
1400
- try {
1401
- await updateSlackInteractionMessage({
1402
- ctx: params.ctx,
1403
- channelId: params.parsed.channelId,
1404
- messageTs: params.parsed.messageTs,
1405
- text: params.parsed.typedBody.message?.text ?? "",
1406
- blocks: []
1654
+ return true;
1655
+ }
1656
+ async function handleSlackLegacyApprovalInteraction(params) {
1657
+ const parsedApproval = parseExecApprovalCommandText(params.pluginInteractionData);
1658
+ if (!parsedApproval) return false;
1659
+ const pluginAuthorized = isSlackApprovalAuthorizedSender({
1660
+ cfg: params.ctx.cfg,
1661
+ accountId: params.ctx.accountId,
1662
+ senderId: params.parsed.userId
1663
+ });
1664
+ const execAuthorized = isSlackExecApprovalAuthorizedSender({
1665
+ cfg: params.ctx.cfg,
1666
+ accountId: params.ctx.accountId,
1667
+ senderId: params.parsed.userId
1668
+ });
1669
+ const resolveMethods = [];
1670
+ if (execAuthorized) resolveMethods.push("exec");
1671
+ if (pluginAuthorized) resolveMethods.push("plugin");
1672
+ if (resolveMethods.length === 0) {
1673
+ params.ctx.runtime.log?.(`slack:interaction drop legacy approval user=${params.parsed.userId} (not authorized)`);
1674
+ await respondEphemeral(params.respond, "You are not authorized to approve this request.");
1675
+ return true;
1676
+ }
1677
+ for (const [index, resolveMethod] of resolveMethods.entries()) try {
1678
+ await resolveApprovalOverGateway({
1679
+ cfg: params.ctx.cfg,
1680
+ approvalId: parsedApproval.approvalId,
1681
+ decision: parsedApproval.decision,
1682
+ senderId: params.parsed.userId,
1683
+ resolveMethod,
1684
+ clientDisplayName: `Slack approval (${params.parsed.userId.trim() || "unknown"})`
1407
1685
  });
1408
- } catch {}
1686
+ try {
1687
+ await updateSlackInteractionMessage({
1688
+ ctx: params.ctx,
1689
+ channelId: params.parsed.channelId,
1690
+ messageTs: params.parsed.messageTs,
1691
+ text: params.parsed.typedBody.message?.text ?? "",
1692
+ blocks: []
1693
+ });
1694
+ } catch {}
1695
+ return true;
1696
+ } catch (error) {
1697
+ if (index + 1 < resolveMethods.length && isApprovalNotFoundError(error)) continue;
1698
+ params.ctx.runtime.log?.(`slack:interaction legacy approval resolve failed id=${parsedApproval.approvalId}: ${String(error)}`);
1699
+ throw error;
1700
+ }
1409
1701
  return true;
1410
1702
  }
1411
1703
  async function dispatchSlackPluginInteraction(params) {
@@ -1572,7 +1864,7 @@ function buildSlackConfirmationBlocks(params) {
1572
1864
  summary: params.parsed.actionSummary,
1573
1865
  buttonText: params.parsed.typedActionWithText.text?.text
1574
1866
  });
1575
- let updatedBlocks = params.originalBlocks.map((block) => {
1867
+ return params.originalBlocks.map((block) => {
1576
1868
  const typedBlock = block;
1577
1869
  if (typedBlock.type === "actions" && typedBlock.block_id === params.parsed.blockId) return {
1578
1870
  type: "context",
@@ -1586,17 +1878,6 @@ function buildSlackConfirmationBlocks(params) {
1586
1878
  };
1587
1879
  return block;
1588
1880
  });
1589
- if (!updatedBlocks.some((block) => {
1590
- const typedBlock = block;
1591
- return typedBlock.type === "actions" && !isBulkActionsBlock(typedBlock);
1592
- })) updatedBlocks = updatedBlocks.filter((block, index) => {
1593
- const typedBlock = block;
1594
- if (isBulkActionsBlock(typedBlock)) return false;
1595
- if (typedBlock.type !== "divider") return true;
1596
- const next = updatedBlocks[index + 1];
1597
- return !next || !isBulkActionsBlock(next);
1598
- });
1599
- return updatedBlocks;
1600
1881
  }
1601
1882
  async function updateSlackLegacyBlockAction(params) {
1602
1883
  const originalBlocks = params.parsed.typedBody.message?.blocks;
@@ -1631,12 +1912,27 @@ async function handleSlackBlockAction(params) {
1631
1912
  if (!parsed) return;
1632
1913
  if (isSlackReplyLinkAction(parsed)) return;
1633
1914
  params.trackEvent?.();
1915
+ if (isSlackApprovalActionId(parsed.actionId)) {
1916
+ const approval = readSlackApprovalAction(parsed);
1917
+ if (!approval) {
1918
+ params.ctx.runtime.log?.(`slack:interaction drop malformed approval action user=${parsed.userId} channel=${parsed.channelId ?? "unknown"}`);
1919
+ await respondEphemeral(respond, "This approval action is invalid or expired.");
1920
+ return;
1921
+ }
1922
+ await handleSlackApprovalInteraction({
1923
+ ctx: params.ctx,
1924
+ parsed,
1925
+ approval,
1926
+ respond
1927
+ });
1928
+ return;
1929
+ }
1634
1930
  const pluginInteractionData = buildSlackPluginInteractionData({
1635
1931
  actionId: parsed.actionId,
1636
1932
  summary: parsed.actionSummary
1637
1933
  });
1638
1934
  if (pluginInteractionData && isSlackReplyActionId(parsed.actionId)) {
1639
- if (await handleSlackExecApprovalInteraction({
1935
+ if (await handleSlackLegacyApprovalInteraction({
1640
1936
  ctx: params.ctx,
1641
1937
  parsed,
1642
1938
  pluginInteractionData,
@@ -2293,6 +2589,66 @@ function registerSlackMemberEvents(params) {
2293
2589
  });
2294
2590
  }
2295
2591
  //#endregion
2592
+ //#region extensions/slack/src/monitor/event-scope.ts
2593
+ function resolveSlackEventScope(params) {
2594
+ const context = params.context ?? {};
2595
+ if (params.identity.kind !== "enterprise") return context.isEnterpriseInstall === true ? {
2596
+ ok: false,
2597
+ reason: "enterprise_event_for_workspace_account"
2598
+ } : { ok: true };
2599
+ const apiAppId = normalizeOptionalString((params.body && typeof params.body === "object" ? params.body : {}).api_app_id);
2600
+ if (!apiAppId) return {
2601
+ ok: false,
2602
+ reason: "missing_api_app_id"
2603
+ };
2604
+ if (params.identity.apiAppId && apiAppId !== params.identity.apiAppId) return {
2605
+ ok: false,
2606
+ reason: "wrong_app"
2607
+ };
2608
+ if (context.isEnterpriseInstall !== true) return {
2609
+ ok: false,
2610
+ reason: "not_enterprise_install"
2611
+ };
2612
+ const enterpriseId = normalizeOptionalString(context.enterpriseId);
2613
+ if (!enterpriseId) return {
2614
+ ok: false,
2615
+ reason: "missing_enterprise_id"
2616
+ };
2617
+ if (enterpriseId !== params.identity.enterpriseId) return {
2618
+ ok: false,
2619
+ reason: "wrong_enterprise"
2620
+ };
2621
+ const teamId = normalizeOptionalString(context.teamId);
2622
+ if (!teamId) return {
2623
+ ok: false,
2624
+ reason: "missing_team_id"
2625
+ };
2626
+ if (!/^T[A-Z0-9]+$/i.test(teamId)) return {
2627
+ ok: false,
2628
+ reason: "invalid_team_id"
2629
+ };
2630
+ if (!params.client) return {
2631
+ ok: false,
2632
+ reason: "missing_listener_client"
2633
+ };
2634
+ const uploadCompletionClient = getSlackListenerUploadCompletionClient({
2635
+ listenerClient: params.client,
2636
+ teamId,
2637
+ clientOptions: params.clientOptions
2638
+ });
2639
+ return {
2640
+ ok: true,
2641
+ scope: {
2642
+ apiAppId,
2643
+ enterpriseId,
2644
+ teamId,
2645
+ isEnterpriseInstall: true,
2646
+ client: params.client,
2647
+ ...uploadCompletionClient ? { uploadCompletionClient } : {}
2648
+ }
2649
+ };
2650
+ }
2651
+ //#endregion
2296
2652
  //#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
2297
2653
  const SUBTYPE_HANDLER_REGISTRY = {
2298
2654
  message_changed: {
@@ -2340,6 +2696,9 @@ function formatSlackInboundLogLine(params) {
2340
2696
  function isSlackUserId(value) {
2341
2697
  return /^[UW][A-Z0-9]+$/.test(value);
2342
2698
  }
2699
+ function isBotAuthoredEnterpriseEvent(event) {
2700
+ return Boolean(normalizeOptionalString(event.bot_id)) || event.subtype === "bot_message";
2701
+ }
2343
2702
  function addUserCandidate(candidates, value, botUserId) {
2344
2703
  const id = normalizeOptionalString(value);
2345
2704
  if (!id || id === botUserId || !isSlackUserId(id)) return;
@@ -2396,21 +2755,57 @@ function resolveAssistantMessageChangedInbound(params) {
2396
2755
  event_ts: changed.event_ts,
2397
2756
  assistant_thread: asOptionalRecord(message.assistant_thread) ?? asOptionalRecord(changed.assistant_thread),
2398
2757
  files: Array.isArray(message.files) ? message.files : void 0,
2399
- attachments: Array.isArray(message.attachments) ? message.attachments : void 0
2758
+ attachments: Array.isArray(message.attachments) ? message.attachments : void 0,
2759
+ blocks: Array.isArray(message.blocks) ? message.blocks : void 0
2400
2760
  };
2401
2761
  }
2402
2762
  function registerSlackMessageEvents(params) {
2403
2763
  const { ctx, handleSlackMessage } = params;
2404
- const handleIncomingMessageEvent = async ({ event, body }) => {
2764
+ const resolveEventScope = (args) => {
2765
+ const resolved = resolveSlackEventScope({
2766
+ identity: ctx.installationIdentity,
2767
+ body: args.body,
2768
+ context: args.context,
2769
+ client: args.client,
2770
+ clientOptions: ctx.app.webClientOptions
2771
+ });
2772
+ if (!resolved.ok) {
2773
+ logVerbose(`slack: drop event (${resolved.reason})`);
2774
+ return null;
2775
+ }
2776
+ return resolved.scope;
2777
+ };
2778
+ const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
2405
2779
  try {
2780
+ const eventScope = resolveEventScope({
2781
+ body,
2782
+ context,
2783
+ client
2784
+ });
2785
+ if (eventScope === null) return;
2406
2786
  if (ctx.shouldDropMismatchedSlackEvent(body)) return;
2407
2787
  const message = event;
2788
+ ctx.rememberSlackChannelType(message.channel, message.channel_type, eventScope);
2789
+ if (eventScope && isBotAuthoredEnterpriseEvent(message)) {
2790
+ logVerbose("slack: drop enterprise bot-authored message");
2791
+ return;
2792
+ }
2793
+ if (eventScope && message.subtype && message.subtype !== "file_share") {
2794
+ logVerbose(`slack: drop enterprise message subtype=${message.subtype}`);
2795
+ return;
2796
+ }
2408
2797
  const assistantChangedInbound = resolveAssistantMessageChangedInbound({
2409
2798
  event: message,
2410
2799
  ctx
2411
2800
  });
2412
2801
  if (assistantChangedInbound) {
2413
- await handleSlackMessage(assistantChangedInbound, { source: "message" });
2802
+ await handleSlackMessage(assistantChangedInbound, {
2803
+ source: "message",
2804
+ ...eventScope ? {
2805
+ eventScope,
2806
+ awaitDispatch: true
2807
+ } : {}
2808
+ });
2414
2809
  return;
2415
2810
  }
2416
2811
  if (message.subtype === "message_changed" && isSelfAttributedMessageChange({
@@ -2435,25 +2830,39 @@ function registerSlackMessageEvents(params) {
2435
2830
  });
2436
2831
  return;
2437
2832
  }
2438
- await handleSlackMessage(message, { source: "message" });
2833
+ await handleSlackMessage(message, {
2834
+ source: "message",
2835
+ ...eventScope ? {
2836
+ eventScope,
2837
+ awaitDispatch: true
2838
+ } : {}
2839
+ });
2439
2840
  } catch (err) {
2440
2841
  ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
2441
2842
  }
2442
2843
  };
2443
- ctx.app.event("message", async ({ event, body }) => {
2444
- await handleIncomingMessageEvent({
2445
- event,
2446
- body
2447
- });
2844
+ ctx.app.event("message", async (args) => {
2845
+ await handleIncomingMessageEvent(args);
2448
2846
  });
2449
- ctx.app.event("app_mention", async ({ event, body }) => {
2847
+ ctx.app.event("app_mention", async (args) => {
2848
+ const { event, body, context, client } = args;
2450
2849
  try {
2850
+ const eventScope = resolveEventScope({
2851
+ body,
2852
+ context,
2853
+ client
2854
+ });
2855
+ if (eventScope === null) return;
2451
2856
  if (ctx.shouldDropMismatchedSlackEvent(body)) return;
2452
2857
  const mention = event;
2858
+ if (eventScope && isBotAuthoredEnterpriseEvent(mention)) {
2859
+ logVerbose("slack: drop enterprise bot-authored app_mention");
2860
+ return;
2861
+ }
2453
2862
  const channelType = normalizeSlackChannelType(mention.channel_type, mention.channel);
2454
2863
  if (channelType === "im" || channelType === "mpim") return;
2455
2864
  slackInboundLog.info(formatSlackInboundLogLine({
2456
- workspaceId: ctx.teamId,
2865
+ workspaceId: eventScope?.teamId ?? ctx.teamId,
2457
2866
  channelId: mention.channel,
2458
2867
  channelType: channelType ?? "channel",
2459
2868
  userId: normalizeOptionalString(mention.user) ?? "unknown",
@@ -2462,7 +2871,11 @@ function registerSlackMessageEvents(params) {
2462
2871
  }));
2463
2872
  await handleSlackMessage(mention, {
2464
2873
  source: "app_mention",
2465
- wasMentioned: true
2874
+ wasMentioned: true,
2875
+ ...eventScope ? {
2876
+ eventScope,
2877
+ awaitDispatch: true
2878
+ } : {}
2466
2879
  });
2467
2880
  } catch (err) {
2468
2881
  ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
@@ -2591,6 +3004,7 @@ function registerSlackMonitorEvents(params) {
2591
3004
  ctx: params.ctx,
2592
3005
  handleSlackMessage: params.handleSlackMessage
2593
3006
  });
3007
+ if (params.ctx.installationIdentity.kind === "enterprise") return;
2594
3008
  registerSlackReactionEvents({
2595
3009
  ctx: params.ctx,
2596
3010
  trackEvent: params.trackEvent
@@ -2609,6 +3023,7 @@ function registerSlackMonitorEvents(params) {
2609
3023
  });
2610
3024
  registerSlackHomeEvents({
2611
3025
  ctx: params.ctx,
3026
+ slashCommandName: params.appHomeSlashCommandName,
2612
3027
  trackEvent: params.trackEvent
2613
3028
  });
2614
3029
  registerSlackInteractionEvents({
@@ -2640,12 +3055,12 @@ const deliveredMessages = createPersistentDedupeCache({
2640
3055
  }
2641
3056
  }
2642
3057
  });
2643
- function makeKey(accountId, channelId, ts) {
2644
- return `${accountId}:${channelId}:${ts}`;
3058
+ function makeKey(accountId, channelId, ts, teamId) {
3059
+ return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
2645
3060
  }
2646
3061
  async function hasSlackInboundMessageDelivery(params) {
2647
3062
  if (!params.accountId || !params.channelId || !params.ts) return false;
2648
- return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts));
3063
+ return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
2649
3064
  }
2650
3065
  async function recordSlackInboundMessageDeliveries(params) {
2651
3066
  if (!params.accountId || params.messages.length === 0) return;
@@ -2653,7 +3068,7 @@ async function recordSlackInboundMessageDeliveries(params) {
2653
3068
  const keys = /* @__PURE__ */ new Set();
2654
3069
  for (const message of params.messages) {
2655
3070
  if (!message.channel || !message.ts) continue;
2656
- keys.add(makeKey(params.accountId, message.channel, message.ts));
3071
+ keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
2657
3072
  }
2658
3073
  await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
2659
3074
  }
@@ -2668,17 +3083,18 @@ function isSlackDirectMessageChannel(channelId) {
2668
3083
  function isTopLevelSlackMessage(message) {
2669
3084
  return !message.thread_ts && !message.parent_user_id;
2670
3085
  }
2671
- function buildTopLevelSlackConversationKey(message, accountId) {
3086
+ function buildTopLevelSlackConversationKey(message, accountId, teamId) {
2672
3087
  if (!isTopLevelSlackMessage(message)) return null;
2673
3088
  const senderId = resolveSlackSenderId(message);
2674
3089
  if (!senderId) return null;
2675
- return `slack:${accountId}:${message.channel}:${senderId}`;
3090
+ return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${message.channel}:${senderId}`;
2676
3091
  }
2677
- function buildSlackDebounceKey(message, accountId) {
3092
+ function buildSlackDebounceKey(message, accountId, teamId) {
2678
3093
  const senderId = resolveSlackSenderId(message);
2679
3094
  if (!senderId) return null;
2680
3095
  const messageTs = message.ts ?? message.event_ts;
2681
- return `slack:${accountId}:${message.thread_ts ? `${message.channel}:${message.thread_ts}` : message.parent_user_id && messageTs ? `${message.channel}:maybe-thread:${messageTs}` : messageTs && !isSlackDirectMessageChannel(message.channel) ? `${message.channel}:${messageTs}` : message.channel}:${senderId}`;
3096
+ const threadKey = message.thread_ts ? `${message.channel}:${message.thread_ts}` : message.parent_user_id && messageTs ? `${message.channel}:maybe-thread:${messageTs}` : messageTs && !isSlackDirectMessageChannel(message.channel) ? `${message.channel}:${messageTs}` : message.channel;
3097
+ return `slack:${accountId}:${teamId ? `${teamId}:` : ""}${threadKey}:${senderId}`;
2682
3098
  }
2683
3099
  //#endregion
2684
3100
  //#region extensions/slack/src/monitor/thread-resolution.ts
@@ -2782,7 +3198,7 @@ function createSlackThreadTsResolver(params) {
2782
3198
  }
2783
3199
  //#endregion
2784
3200
  //#region extensions/slack/src/monitor/message-handler.ts
2785
- const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-rpVpay59.js"));
3201
+ const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-DofAvTtb.js"));
2786
3202
  function createSlackDispatchCompletion() {
2787
3203
  let resolve;
2788
3204
  let reject;
@@ -2799,14 +3215,7 @@ const APP_MENTION_RETRY_TTL_MS = 6e4;
2799
3215
  const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
2800
3216
  const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
2801
3217
  const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
2802
- var SlackRetryableInboundError = class extends Error {
2803
- constructor(message, options) {
2804
- super(message, options);
2805
- this.name = "SlackRetryableInboundError";
2806
- }
2807
- };
2808
3218
  function isRetryableSlackInboundError(error) {
2809
- if (error instanceof SlackRetryableInboundError) return true;
2810
3219
  return collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)));
2811
3220
  }
2812
3221
  function shouldDebounceSlackMessage(message, cfg) {
@@ -2816,20 +3225,20 @@ function shouldDebounceSlackMessage(message, cfg) {
2816
3225
  hasMedia: Boolean(message.files && message.files.length > 0)
2817
3226
  });
2818
3227
  }
2819
- function buildSeenMessageKey(channelId, ts) {
3228
+ function buildSeenMessageKey(channelId, ts, teamId) {
2820
3229
  if (!channelId || !ts) return null;
2821
- return `${channelId}:${ts}`;
3230
+ return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
2822
3231
  }
2823
3232
  function createSlackMessageHandler(params) {
2824
- const { ctx, account, trackEvent } = params;
3233
+ const { ctx, account, trackEvent, onPrepared } = params;
2825
3234
  const { debounceMs, debouncer } = createChannelInboundDebouncer({
2826
3235
  cfg: ctx.cfg,
2827
3236
  channel: "slack",
2828
- buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId),
2829
- shouldDebounce: (entry) => shouldDebounceSlackMessage(entry.message, ctx.cfg),
3237
+ buildKey: (entry) => buildSlackDebounceKey(entry.message, ctx.accountId, entry.opts.eventScope?.teamId),
3238
+ shouldDebounce: (entry) => !entry.opts.eventScope && shouldDebounceSlackMessage(entry.message, ctx.cfg),
2830
3239
  onFlush: async (entries) => {
2831
3240
  const retryEntries = (sourceError) => {
2832
- if (!isRetryableSlackInboundError(sourceError)) return false;
3241
+ if (!isRetryableSlackInboundError(sourceError) || entries.some((entry) => entry.opts.eventScope)) return false;
2833
3242
  const nextEntries = entries.map((entry) => {
2834
3243
  if (entry.opts.dispatchCompletion) return null;
2835
3244
  const retryAttempt = entry.opts.retryAttempt ?? 0;
@@ -2856,8 +3265,9 @@ function createSlackMessageHandler(params) {
2856
3265
  await (async () => {
2857
3266
  const last = entries.at(-1);
2858
3267
  if (!last) return;
2859
- const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId);
2860
- const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId);
3268
+ const teamId = last.opts.eventScope?.teamId;
3269
+ const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
3270
+ const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
2861
3271
  if (flushedKey && topLevelConversationKey) {
2862
3272
  const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
2863
3273
  if (pendingKeys) {
@@ -2871,7 +3281,7 @@ function createSlackMessageHandler(params) {
2871
3281
  ...last.message,
2872
3282
  text: combinedText
2873
3283
  };
2874
- const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts);
3284
+ const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts, last.opts.eventScope?.teamId);
2875
3285
  try {
2876
3286
  const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
2877
3287
  const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, ...lastOpts } = last.opts;
@@ -2905,6 +3315,7 @@ function createSlackMessageHandler(params) {
2905
3315
  }
2906
3316
  })();
2907
3317
  if (!prepared) return;
3318
+ onPrepared?.(prepared);
2908
3319
  if (entries.length > 1) {
2909
3320
  const ids = entries.map((entry) => entry.message.ts).filter(Boolean);
2910
3321
  if (ids.length > 0) {
@@ -2917,20 +3328,22 @@ function createSlackMessageHandler(params) {
2917
3328
  await dispatchPreparedSlackMessage(prepared);
2918
3329
  await recordSlackInboundMessageDeliveries({
2919
3330
  accountId: ctx.accountId,
3331
+ ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
2920
3332
  messages: entries.map((entry) => entry.message)
2921
3333
  });
2922
3334
  } catch (error) {
2923
3335
  if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
2924
3336
  accountId: ctx.accountId,
3337
+ ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
2925
3338
  messages: entries.map((entry) => entry.message)
2926
3339
  });
2927
3340
  throw error;
2928
3341
  }
2929
3342
  } catch (error) {
2930
3343
  if (isRetryableSlackInboundError(error)) for (const entry of entries) {
2931
- const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts);
3344
+ const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
2932
3345
  if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
2933
- ctx.releaseSeenMessage(entry.message.channel, entry.message.ts);
3346
+ ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
2934
3347
  }
2935
3348
  throw error;
2936
3349
  }
@@ -2980,25 +3393,28 @@ function createSlackMessageHandler(params) {
2980
3393
  async function enqueueSlackMessage(message, opts) {
2981
3394
  if (opts.source === "message" && message.type !== "message") return;
2982
3395
  if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
2983
- const seenMessageKey = buildSeenMessageKey(message.channel, message.ts);
3396
+ ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
3397
+ const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
2984
3398
  if (seenMessageKey && await hasSlackInboundMessageDelivery({
2985
3399
  accountId: ctx.accountId,
2986
3400
  channelId: message.channel,
2987
- ts: message.ts
3401
+ ts: message.ts,
3402
+ ...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
2988
3403
  })) return;
2989
- const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts) : false;
3404
+ const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
2990
3405
  if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
2991
3406
  if (seenMessageKey && wasSeen) {
2992
3407
  if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
2993
3408
  }
2994
3409
  trackEvent?.();
2995
- const resolvedMessage = await threadTsResolver.resolve({
3410
+ const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
2996
3411
  message,
2997
3412
  source: opts.source
2998
3413
  });
2999
- const debounceKey = buildSlackDebounceKey(resolvedMessage, ctx.accountId);
3000
- const conversationKey = buildTopLevelSlackConversationKey(resolvedMessage, ctx.accountId);
3001
- const canDebounce = debounceMs > 0 && shouldDebounceSlackMessage(resolvedMessage, ctx.cfg);
3414
+ const teamId = opts.eventScope?.teamId;
3415
+ const debounceKey = buildSlackDebounceKey(resolvedMessage, ctx.accountId, teamId);
3416
+ const conversationKey = buildTopLevelSlackConversationKey(resolvedMessage, ctx.accountId, teamId);
3417
+ const canDebounce = !opts.eventScope && debounceMs > 0 && shouldDebounceSlackMessage(resolvedMessage, ctx.cfg);
3002
3418
  if (!canDebounce && conversationKey) {
3003
3419
  const pendingKeys = pendingTopLevelDebounceKeys.get(conversationKey);
3004
3420
  if (pendingKeys && pendingKeys.size > 0) {
@@ -3029,6 +3445,203 @@ function createSlackMessageHandler(params) {
3029
3445
  };
3030
3446
  }
3031
3447
  //#endregion
3448
+ //#region extensions/slack/src/monitor/presence-monitor.ts
3449
+ const SLACK_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
3450
+ const SLACK_PRESENCE_POLL_INTERVAL_MS = 6e4;
3451
+ const SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS = 8;
3452
+ const SLACK_PRESENCE_TARGET_TTL_MS = 1440 * 60 * 1e3;
3453
+ const SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL = 45;
3454
+ const SLACK_PRESENCE_MAX_TARGETS = 2e3;
3455
+ function resolveMode(channelConfig, accountConfig) {
3456
+ return channelConfig?.mode ?? accountConfig?.mode ?? "off";
3457
+ }
3458
+ function hasSlackPresenceEventsEnabled(params) {
3459
+ if (resolveMode(void 0, params.account) !== "off") return true;
3460
+ return Object.values(params.channels ?? {}).some((entry) => resolveMode(entry?.presenceEvents, void 0) !== "off");
3461
+ }
3462
+ function isTargetEligible(target) {
3463
+ if (target.mode === "on") return true;
3464
+ if (target.autoEligibleKind === "channel") return false;
3465
+ return target.participants.size <= SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS;
3466
+ }
3467
+ function formatSlackPresenceEvent(target, userId) {
3468
+ return [
3469
+ "Slack presence event:",
3470
+ `A human participant became active on Slack after being observed away: user_id=${JSON.stringify(userId)} channel_id=${JSON.stringify(target.channelId)}${target.threadId ? ` thread_ts=${JSON.stringify(target.threadId)}` : ""}.`,
3471
+ "Before greeting, retrieve relevant memory and wiki context for this immutable user_id, including a known timezone when available. Use their local time; if their timezone is unknown, do not guess.",
3472
+ "Send at most one short, natural greeting in this Slack conversation. Do not reveal private memory. If no greeting is appropriate, stay silent."
3473
+ ].join("\n");
3474
+ }
3475
+ function resolveObservedTarget(params) {
3476
+ const { prepared } = params;
3477
+ const userId = prepared.message.user?.trim();
3478
+ if (!userId || prepared.message.bot_id || prepared.message.subtype === "bot_message") return null;
3479
+ const mode = resolveMode(prepared.channelConfig?.presenceEvents, params.accountConfig);
3480
+ if (mode === "off") return null;
3481
+ const channelId = prepared.message.channel;
3482
+ const rawThreadId = prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId ?? void 0;
3483
+ const threadId = rawThreadId === void 0 ? void 0 : String(rawThreadId);
3484
+ const channelType = prepared.message.channel_type;
3485
+ const autoEligibleKind = prepared.isDirectMessage ? "direct" : channelType === "mpim" ? "group" : threadId ? "thread" : "channel";
3486
+ if (mode === "auto" && autoEligibleKind === "channel") return null;
3487
+ return {
3488
+ key: `${channelId}${threadId ? `:thread:${threadId}` : ":top"}`,
3489
+ mode,
3490
+ channelId,
3491
+ threadId,
3492
+ to: prepared.isDirectMessage ? `user:${userId}` : `channel:${channelId}`,
3493
+ sessionKey: prepared.route.sessionKey,
3494
+ agentId: prepared.route.agentId,
3495
+ participants: /* @__PURE__ */ new Map([[userId, params.nowMs]]),
3496
+ lastActivityAtMs: params.nowMs,
3497
+ autoEligibleKind
3498
+ };
3499
+ }
3500
+ function createSlackPresenceMonitor(params) {
3501
+ const targets = /* @__PURE__ */ new Map();
3502
+ const presenceByUser = /* @__PURE__ */ new Map();
3503
+ const nowMs = params.nowMs ?? Date.now;
3504
+ const enqueue = params.enqueue ?? enqueueSystemEvent;
3505
+ const wake = params.wake ?? requestHeartbeat;
3506
+ let pollOffset = 0;
3507
+ let timer;
3508
+ let activePoll;
3509
+ let stopped = false;
3510
+ const pruneTargets = (now) => {
3511
+ for (const [key, target] of targets) if (now - target.lastActivityAtMs >= SLACK_PRESENCE_TARGET_TTL_MS) targets.delete(key);
3512
+ while (targets.size > SLACK_PRESENCE_MAX_TARGETS) {
3513
+ const oldestKey = targets.keys().next().value;
3514
+ if (typeof oldestKey !== "string") break;
3515
+ targets.delete(oldestKey);
3516
+ }
3517
+ const eligibleUsers = new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())));
3518
+ for (const userId of presenceByUser.keys()) if (!eligibleUsers.has(userId)) presenceByUser.delete(userId);
3519
+ };
3520
+ const observe = (prepared) => {
3521
+ const now = nowMs();
3522
+ pruneTargets(now);
3523
+ const observed = resolveObservedTarget({
3524
+ prepared,
3525
+ accountConfig: params.accountConfig,
3526
+ nowMs: now
3527
+ });
3528
+ if (!observed) return;
3529
+ const current = targets.get(observed.key);
3530
+ if (current) {
3531
+ current.mode = observed.mode;
3532
+ current.sessionKey = observed.sessionKey;
3533
+ current.agentId = observed.agentId;
3534
+ current.to = observed.to;
3535
+ current.lastActivityAtMs = now;
3536
+ for (const [participant, observedAt] of observed.participants) current.participants.set(participant, observedAt);
3537
+ targets.delete(observed.key);
3538
+ targets.set(observed.key, current);
3539
+ } else targets.set(observed.key, observed);
3540
+ pruneTargets(now);
3541
+ };
3542
+ const emitTransition = (userId, now) => {
3543
+ const target = Array.from(targets.values()).filter((candidate) => candidate.participants.has(userId) && isTargetEligible(candidate)).toSorted((a, b) => (b.participants.get(userId) ?? 0) - (a.participants.get(userId) ?? 0))[0];
3544
+ if (!target) return;
3545
+ const cooldownKey = `${params.accountId}:${userId}`;
3546
+ let reserved;
3547
+ try {
3548
+ reserved = params.cooldownStore.registerIfAbsent(cooldownKey, now, { ttlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS });
3549
+ } catch (err) {
3550
+ params.error?.(`slack presence cooldown persistence failed: ${String(err)}`);
3551
+ return;
3552
+ }
3553
+ if (!reserved) return;
3554
+ if (!enqueue(formatSlackPresenceEvent(target, userId), {
3555
+ sessionKey: target.sessionKey,
3556
+ contextKey: `slack:presence-active:${params.accountId}:${userId}`,
3557
+ deliveryContext: {
3558
+ channel: "slack",
3559
+ to: target.to,
3560
+ accountId: params.accountId,
3561
+ threadId: target.threadId
3562
+ }
3563
+ })) {
3564
+ params.cooldownStore.delete(cooldownKey);
3565
+ return;
3566
+ }
3567
+ wake({
3568
+ source: "notifications-event",
3569
+ intent: "immediate",
3570
+ reason: "wake",
3571
+ agentId: target.agentId,
3572
+ sessionKey: target.sessionKey,
3573
+ heartbeat: {
3574
+ target: "slack",
3575
+ to: target.to,
3576
+ accountId: params.accountId
3577
+ }
3578
+ });
3579
+ };
3580
+ const performPoll = async () => {
3581
+ const now = nowMs();
3582
+ pruneTargets(now);
3583
+ const candidates = Array.from(new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())))).toSorted();
3584
+ if (candidates.length === 0) return;
3585
+ const count = Math.min(candidates.length, SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL);
3586
+ const selected = Array.from({ length: count }, (_, index) => candidates[(pollOffset + index) % candidates.length]).filter((userId) => Boolean(userId));
3587
+ pollOffset = (pollOffset + count) % candidates.length;
3588
+ for (const userId of selected) {
3589
+ if (stopped) return;
3590
+ try {
3591
+ const response = await params.client.getPresence({ user: userId });
3592
+ if (stopped) return;
3593
+ const next = response.presence === "active" || response.presence === "away" ? response.presence : void 0;
3594
+ if (!next) continue;
3595
+ const previous = presenceByUser.get(userId);
3596
+ presenceByUser.set(userId, next);
3597
+ if (previous === "away" && next === "active") emitTransition(userId, now);
3598
+ } catch (err) {
3599
+ if (stopped) return;
3600
+ params.error?.(`slack presence poll failed for user ${userId}: ${String(err)}`);
3601
+ }
3602
+ }
3603
+ };
3604
+ const pollOnce = () => {
3605
+ if (stopped) return Promise.resolve();
3606
+ if (activePoll) return activePoll;
3607
+ const run = performPoll().finally(() => {
3608
+ if (activePoll === run) activePoll = void 0;
3609
+ });
3610
+ activePoll = run;
3611
+ return run;
3612
+ };
3613
+ return {
3614
+ observe,
3615
+ pollOnce,
3616
+ start: () => {
3617
+ if (timer) return;
3618
+ stopped = false;
3619
+ params.log?.(`slack presence polling enabled for account ${params.accountId}`);
3620
+ timer = setInterval(() => void pollOnce(), SLACK_PRESENCE_POLL_INTERVAL_MS);
3621
+ timer.unref?.();
3622
+ },
3623
+ stop: async () => {
3624
+ stopped = true;
3625
+ if (timer) {
3626
+ clearInterval(timer);
3627
+ timer = void 0;
3628
+ }
3629
+ await activePoll;
3630
+ }
3631
+ };
3632
+ }
3633
+ //#endregion
3634
+ //#region extensions/slack/src/monitor/presence-cooldown-store.ts
3635
+ const SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
3636
+ function openSlackPresenceCooldownStore() {
3637
+ return getSlackRuntime().state.openSyncKeyedStore({
3638
+ namespace: "presence-greeting-cooldowns",
3639
+ maxEntries: SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES,
3640
+ overflowPolicy: "reject-new",
3641
+ defaultTtlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS
3642
+ });
3643
+ }
3644
+ //#endregion
3032
3645
  //#region extensions/slack/src/monitor/reconnect-policy.ts
3033
3646
  const SLACK_AUTH_ERROR_RE = /account_inactive|invalid_auth|token_revoked|token_expired|not_authed|org_login_required|team_access_not_granted|user_removed_from_team|team_disabled|missing_scope|cannot_find_service|invalid_token/i;
3034
3647
  const NO_ERROR_DETAIL = "no error detail";
@@ -3217,13 +3830,15 @@ function resolveSlackBoltInterop(params) {
3217
3830
  };
3218
3831
  throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
3219
3832
  }
3220
- function publishSlackConnectedStatus(setStatus) {
3833
+ function publishSlackConnectedStatus(setStatus, identityHealth = {
3834
+ healthState: "healthy",
3835
+ lastError: null
3836
+ }) {
3221
3837
  if (!setStatus) return;
3222
3838
  setStatus({
3223
3839
  connected: true,
3224
3840
  lastConnectedAt: Date.now(),
3225
- healthState: "healthy",
3226
- lastError: null
3841
+ ...identityHealth
3227
3842
  });
3228
3843
  }
3229
3844
  function publishSlackDisconnectedStatus(setStatus, error) {
@@ -3389,8 +4004,11 @@ async function gracefulStopSlackApp(app) {
3389
4004
  }
3390
4005
  function formatSlackResolvedLabel(params) {
3391
4006
  const extras = params.extra?.filter(Boolean) ?? [];
3392
- const suffix = extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`;
3393
- return `${params.input}→${params.name ?? params.id}${suffix}`;
4007
+ const display = params.name ?? params.id;
4008
+ if (params.input === params.id && !params.name && extras.length === 0) return null;
4009
+ const details = [...params.input === params.id || display === params.id ? [] : [`id:${params.id}`], ...extras];
4010
+ const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
4011
+ return `${params.input}→${display}${suffix}`;
3394
4012
  }
3395
4013
  function formatSlackChannelResolved(entry) {
3396
4014
  const id = entry.id ?? entry.input;
@@ -3511,6 +4129,25 @@ function createSlackExternalArgMenuStore() {
3511
4129
  };
3512
4130
  }
3513
4131
  //#endregion
4132
+ //#region extensions/slack/src/monitor/response-url-budget.ts
4133
+ const SLACK_RESPONSE_URL_MAX_CALLS = 5;
4134
+ var SlackResponseAlreadyReportedError = class extends Error {};
4135
+ function isSlackResponseAlreadyReportedError(error) {
4136
+ return error instanceof SlackResponseAlreadyReportedError;
4137
+ }
4138
+ /** Count every response_url attempt, including requests Slack rejects. */
4139
+ function createSlackResponseUrlBudget(respond, maxCalls = SLACK_RESPONSE_URL_MAX_CALLS) {
4140
+ let remaining = maxCalls;
4141
+ return {
4142
+ remaining: () => remaining,
4143
+ respond: async (payload) => {
4144
+ if (remaining <= 0) throw new Error(`Slack response_url cannot be used more than ${String(maxCalls)} times.`);
4145
+ remaining -= 1;
4146
+ return await respond(payload);
4147
+ }
4148
+ };
4149
+ }
4150
+ //#endregion
3514
4151
  //#region extensions/slack/src/monitor/room-context.ts
3515
4152
  function resolveSlackRoomContextHints(params) {
3516
4153
  const untrustedChannelMetadata = params.isRoomish ? buildUntrustedChannelMetadata({
@@ -3541,7 +4178,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
3541
4178
  const SLACK_HEADER_TEXT_MAX = 150;
3542
4179
  const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
3543
4180
  const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
3544
- const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-CcMbbT3_.js"));
4181
+ const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-CSja9VuD.js"));
3545
4182
  const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
3546
4183
  const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
3547
4184
  function resolveSlackCommandMenuModelContext(params) {
@@ -3771,7 +4408,12 @@ async function registerSlackMonitorSlashCommands(params) {
3771
4408
  let supportsExternalArgMenus = typeof ctx.app.options === "function";
3772
4409
  const slashCommand = resolveSlackSlashCommandConfig(ctx.slashCommand ?? account.config.slashCommand);
3773
4410
  const handleSlashCommand = async (p) => {
3774
- const { command, ack, respond, body, prompt, commandArgs, commandDefinition } = p;
4411
+ const { command, ack, respond: respondWithoutBudget, body, prompt, commandArgs, commandDefinition } = p;
4412
+ const responseBudget = p.responseTransport === "web-api" ? {
4413
+ respond: respondWithoutBudget,
4414
+ remaining: () => void 0
4415
+ } : createSlackResponseUrlBudget(respondWithoutBudget);
4416
+ const respond = responseBudget.respond;
3775
4417
  const cfg = getRuntimeConfigSnapshot() ?? ctx.cfg;
3776
4418
  try {
3777
4419
  if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
@@ -3887,7 +4529,7 @@ async function registerSlackMonitorSlashCommands(params) {
3887
4529
  modeWhenAccessGroupsOff: "configured"
3888
4530
  });
3889
4531
  const senderGate = slashIngress.senderAccess.gate;
3890
- if (isRoom && senderGate?.allowed === false) {
4532
+ if (isRoomish && senderGate?.allowed === false) {
3891
4533
  await respond({
3892
4534
  text: "You are not authorized to use this command here.",
3893
4535
  response_type: "ephemeral"
@@ -4062,15 +4704,19 @@ async function registerSlackMonitorSlashCommands(params) {
4062
4704
  cfg,
4063
4705
  channel: "slack",
4064
4706
  accountId: route.accountId
4065
- })
4707
+ }),
4708
+ responseBudget
4066
4709
  });
4067
4710
  };
4711
+ const pendingSlashReplies = [];
4068
4712
  const { counts } = await dispatchReplyWithDispatcher({
4069
4713
  ctx: ctxPayload,
4070
4714
  cfg,
4071
4715
  dispatcherOptions: {
4072
4716
  ...replyPipeline,
4073
- deliver: async (payload) => deliverSlashPayloads([payload]),
4717
+ deliver: async (payload) => {
4718
+ pendingSlashReplies.push(payload);
4719
+ },
4074
4720
  onError: (err, info) => {
4075
4721
  runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${formatSlackError(err)}`));
4076
4722
  }
@@ -4080,10 +4726,11 @@ async function registerSlackMonitorSlashCommands(params) {
4080
4726
  onModelSelected
4081
4727
  }
4082
4728
  });
4083
- if (counts.final + counts.tool + counts.block === 0) await deliverSlashPayloads([]);
4729
+ if (pendingSlashReplies.length > 0) await deliverSlashPayloads(pendingSlashReplies);
4730
+ else if (counts.final + counts.tool + counts.block === 0) await deliverSlashPayloads([]);
4084
4731
  } catch (err) {
4085
4732
  runtime.error?.(danger(`slack slash handler failed: ${formatErrorMessage(err)}`));
4086
- await respond({
4733
+ if (!isSlackResponseAlreadyReportedError(err) && responseBudget.remaining() !== 0) await respond({
4087
4734
  text: "Sorry, something went wrong handling that command.",
4088
4735
  response_type: "ephemeral"
4089
4736
  });
@@ -4117,7 +4764,16 @@ async function registerSlackMonitorSlashCommands(params) {
4117
4764
  nativeCommands.push(pluginCommand);
4118
4765
  }
4119
4766
  }
4120
- if (nativeCommands.length > 0) {
4767
+ if (slashCommand.enabled) ctx.app.command(buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }) => {
4768
+ await handleSlashCommand({
4769
+ command,
4770
+ ack,
4771
+ respond,
4772
+ body,
4773
+ prompt: command.text?.trim() ?? ""
4774
+ });
4775
+ });
4776
+ else if (nativeCommands.length > 0) {
4121
4777
  if (!slashCommandsRuntime) throw new Error("Missing commands runtime for native Slack commands.");
4122
4778
  for (const command of nativeCommands) ctx.app.command(`/${command.name}`, async ({ command: cmd, ack, respond, body }) => {
4123
4779
  const commandDefinition = slashCommandsRuntime.findCommandByNativeName(command.name, "slack");
@@ -4134,17 +4790,12 @@ async function registerSlackMonitorSlashCommands(params) {
4134
4790
  commandDefinition: commandDefinition ?? void 0
4135
4791
  });
4136
4792
  });
4137
- } else if (slashCommand.enabled) ctx.app.command(buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond, body }) => {
4138
- await handleSlashCommand({
4139
- command,
4140
- ack,
4141
- respond,
4142
- body,
4143
- prompt: command.text?.trim() ?? ""
4144
- });
4145
- });
4146
- else logVerbose("slack: slash commands disabled");
4147
- if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return;
4793
+ } else logVerbose("slack: slash commands disabled");
4794
+ const registration = nativeCommands.length > 0 ? { mode: "native" } : slashCommand.enabled ? {
4795
+ mode: "single",
4796
+ name: slashCommand.name
4797
+ } : { mode: "disabled" };
4798
+ if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) return registration;
4148
4799
  const registerArgOptions = () => {
4149
4800
  const appWithOptions = ctx.app;
4150
4801
  if (typeof appWithOptions.options !== "function") return;
@@ -4190,21 +4841,24 @@ async function registerSlackMonitorSlashCommands(params) {
4190
4841
  }
4191
4842
  const registerArgAction = (actionId) => {
4192
4843
  ctx.app.action(actionId, async (args) => {
4193
- const { ack, body, respond } = args;
4844
+ const { ack, body } = args;
4845
+ const respond = args.respond;
4194
4846
  const action = args.action;
4195
4847
  await ack();
4196
4848
  if (ctx.shouldDropMismatchedSlackEvent?.(body)) {
4197
4849
  runtime.log?.("slack: drop slash arg action payload (mismatched app/team)");
4198
4850
  return;
4199
4851
  }
4200
- const respondFn = respond ?? (async (payload) => {
4852
+ const respondFn = respond ?? (async (message) => {
4201
4853
  if (!body.channel?.id || !body.user?.id) return;
4854
+ const payload = typeof message === "string" ? { text: message } : message;
4202
4855
  await ctx.app.client.chat.postEphemeral({
4203
4856
  token: ctx.botToken,
4204
4857
  channel: body.channel.id,
4205
4858
  user: body.user.id,
4206
- text: payload.text,
4207
- blocks: payload.blocks
4859
+ text: payload.text ?? "",
4860
+ ...payload.blocks ? { blocks: payload.blocks } : {},
4861
+ ...typeof payload.mrkdwn === "boolean" ? { mrkdwn: payload.mrkdwn } : {}
4208
4862
  });
4209
4863
  });
4210
4864
  const parsed = parseSlackCommandArgValue(action?.value ?? action?.selected_option?.value);
@@ -4240,6 +4894,7 @@ async function registerSlackMonitorSlashCommands(params) {
4240
4894
  command: commandPayload,
4241
4895
  ack: async () => {},
4242
4896
  respond: respondFn,
4897
+ responseTransport: respond ? "response-url" : "web-api",
4243
4898
  body,
4244
4899
  prompt,
4245
4900
  commandArgs,
@@ -4248,6 +4903,7 @@ async function registerSlackMonitorSlashCommands(params) {
4248
4903
  });
4249
4904
  };
4250
4905
  registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
4906
+ return registration;
4251
4907
  }
4252
4908
  //#endregion
4253
4909
  //#region extensions/slack/src/monitor/provider.ts
@@ -4262,7 +4918,7 @@ async function getSlackBoltInterop() {
4262
4918
  }
4263
4919
  return slackBoltInterop;
4264
4920
  }
4265
- const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-D2jv1iGg.js"));
4921
+ const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-DMmgtvg4.js"));
4266
4922
  const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
4267
4923
  const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
4268
4924
  function resolveStableSlackUserIdEntry(raw) {
@@ -4336,6 +4992,19 @@ async function monitorSlackProvider(opts = {}) {
4336
4992
  const sessionScope = sessionCfg?.scope ?? "per-sender";
4337
4993
  const mainKey = normalizeMainKey(sessionCfg?.mainKey);
4338
4994
  const slackMode = opts.mode ?? account.config.mode ?? "socket";
4995
+ const enterpriseOrgInstall = account.config.enterpriseOrgInstall === true;
4996
+ if (enterpriseOrgInstall && slackMode === "relay") throw new Error(`Slack Enterprise Grid org account "${account.accountId}" requires direct socket or HTTP delivery; relay mode is unsupported`);
4997
+ if (enterpriseOrgInstall && account.config.execApprovals?.enabled === true) throw new Error(`Slack Enterprise Grid org account "${account.accountId}" does not support Slack-native exec approvals`);
4998
+ if (enterpriseOrgInstall) {
4999
+ assertEnterpriseSlackPolicyConfig({
5000
+ config: account.config,
5001
+ accountId: account.accountId
5002
+ });
5003
+ assertNoEnterpriseSlackBindings({
5004
+ cfg,
5005
+ accountId: account.accountId
5006
+ });
5007
+ }
4339
5008
  const slackWebhookPath = normalizeSlackWebhookPath(account.config.webhookPath);
4340
5009
  const signingSecret = normalizeResolvedSecretInputString({
4341
5010
  value: account.config.signingSecret,
@@ -4363,6 +5032,12 @@ async function monitorSlackProvider(opts = {}) {
4363
5032
  cfg,
4364
5033
  accountId: account.accountId
4365
5034
  });
5035
+ if (enterpriseOrgInstall) assertEnterpriseSlackDmPolicy({
5036
+ accountId: account.accountId,
5037
+ dmEnabled,
5038
+ dmPolicy,
5039
+ allowFrom
5040
+ });
4366
5041
  const groupDmEnabled = dmConfig?.groupEnabled ?? false;
4367
5042
  const groupDmChannels = dmConfig?.groupChannels;
4368
5043
  let channelsConfig = slackCfg.channels;
@@ -4432,34 +5107,41 @@ async function monitorSlackProvider(opts = {}) {
4432
5107
  }) : () => {};
4433
5108
  let botUserId = "";
4434
5109
  let botId = "";
4435
- let teamId = "";
4436
- let apiAppId = "";
4437
5110
  const expectedApiAppIdFromAppToken = slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : void 0;
4438
- let authTestFailed = false;
4439
5111
  let authTestError;
4440
5112
  let authIdentityWarning;
5113
+ let authTestIdentity;
4441
5114
  try {
4442
- const auth = await app.client.auth.test();
5115
+ const auth = await createSlackStartupAuthClient(botToken, clientOptions).auth.test();
4443
5116
  const authUserId = normalizeOptionalString(auth.user_id) ?? "";
4444
5117
  botId = normalizeOptionalString(auth.bot_id) ?? "";
4445
5118
  botUserId = botId ? authUserId : "";
4446
- teamId = auth.team_id ?? "";
4447
- apiAppId = auth.api_app_id ?? "";
5119
+ authTestIdentity = auth;
4448
5120
  authIdentityWarning = formatSlackBotTokenIdentityWarning({
4449
5121
  auth,
4450
5122
  accountId: account.accountId
4451
5123
  });
4452
- if (!authUserId) {
4453
- authTestFailed = true;
4454
- authTestError = "auth.test returned no user_id";
4455
- }
5124
+ if (!authUserId && !enterpriseOrgInstall) authTestError = "auth.test returned no user_id";
4456
5125
  } catch (err) {
4457
- authTestFailed = true;
4458
5126
  authTestError = err instanceof Error ? err.message : String(err);
4459
5127
  }
4460
- if (authTestFailed) runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError ?? "unknown error"}); explicit bot-mention detection will be disabled until restart with a valid bot token; required-mention channels will fail closed without another trusted activation signal`));
5128
+ const installationIdentity = resolveSlackInstallationIdentity({
5129
+ enterpriseOrgInstall,
5130
+ auth: authTestError === void 0 ? authTestIdentity : void 0,
5131
+ authError: authTestError,
5132
+ transportApiAppId: expectedApiAppIdFromAppToken
5133
+ });
5134
+ const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
5135
+ const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
5136
+ if (authTestError !== void 0) runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError}); explicit bot-mention detection will be disabled until restart with a valid bot token; required-mention channels will fail closed without another trusted activation signal`));
4461
5137
  if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
4462
- if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token api_app_id=${apiAppId} but app token looks like api_app_id=${expectedApiAppIdFromAppToken}`);
5138
+ const identityHealth = resolveSlackIdentityHealth({
5139
+ installationIdentity,
5140
+ botUserId,
5141
+ authTestError,
5142
+ authIdentityWarning
5143
+ });
5144
+ if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
4463
5145
  const ctx = createSlackMonitorContext({
4464
5146
  cfg,
4465
5147
  accountId: account.accountId,
@@ -4471,6 +5153,7 @@ async function monitorSlackProvider(opts = {}) {
4471
5153
  botId,
4472
5154
  teamId,
4473
5155
  apiAppId,
5156
+ installationIdentity,
4474
5157
  historyLimit,
4475
5158
  dmHistoryLimit,
4476
5159
  sessionScope,
@@ -4504,12 +5187,26 @@ async function monitorSlackProvider(opts = {}) {
4504
5187
  lastInboundAt: Date.now()
4505
5188
  });
4506
5189
  } : void 0;
5190
+ const presenceEventsEnabled = hasSlackPresenceEventsEnabled({
5191
+ account: slackCfg.presenceEvents,
5192
+ channels: slackCfg.channels
5193
+ });
5194
+ const presenceMonitor = installationIdentity.kind !== "enterprise" && presenceEventsEnabled ? createSlackPresenceMonitor({
5195
+ accountId: account.accountId,
5196
+ accountConfig: slackCfg.presenceEvents,
5197
+ client: app.client.users,
5198
+ cooldownStore: openSlackPresenceCooldownStore(),
5199
+ log: runtime.log,
5200
+ error: runtime.error
5201
+ }) : void 0;
5202
+ if (installationIdentity.kind === "enterprise" && presenceEventsEnabled) runtime.log?.(warn("slack presence events are unavailable for Enterprise Grid org installs"));
4507
5203
  const handleSlackMessage = createSlackMessageHandler({
4508
5204
  ctx,
4509
5205
  account,
4510
- trackEvent
5206
+ trackEvent,
5207
+ onPrepared: presenceMonitor?.observe
4511
5208
  });
4512
- if (isSlackAnyNativeApprovalClientEnabled({
5209
+ if (installationIdentity.kind !== "enterprise" && isSlackAnyNativeApprovalClientEnabled({
4513
5210
  cfg,
4514
5211
  accountId: account.accountId
4515
5212
  })) registerChannelRuntimeContext({
@@ -4523,24 +5220,26 @@ async function monitorSlackProvider(opts = {}) {
4523
5220
  },
4524
5221
  abortSignal: opts.abortSignal
4525
5222
  });
4526
- registerSlackMonitorEvents({
5223
+ const commandRegistration = installationIdentity.kind === "enterprise" ? { mode: "disabled" } : await registerSlackMonitorSlashCommands({
4527
5224
  ctx,
4528
5225
  account,
4529
- handleSlackMessage,
4530
5226
  trackEvent
4531
5227
  });
4532
- await registerSlackMonitorSlashCommands({
5228
+ registerSlackMonitorEvents({
4533
5229
  ctx,
4534
5230
  account,
5231
+ handleSlackMessage,
5232
+ appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
4535
5233
  trackEvent
4536
5234
  });
5235
+ presenceMonitor?.start();
4537
5236
  if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
4538
5237
  path: slackWebhookPath,
4539
5238
  handler: slackHttpHandler,
4540
5239
  log: runtime.log,
4541
5240
  accountId: account.accountId
4542
5241
  });
4543
- if (resolveToken) (async () => {
5242
+ if (resolveToken && installationIdentity.kind !== "enterprise") (async () => {
4544
5243
  if (opts.abortSignal?.aborted) return;
4545
5244
  if (channelsConfig && Object.keys(channelsConfig).length > 0) try {
4546
5245
  const entries = Object.keys(channelsConfig).filter((key) => key !== "*");
@@ -4559,7 +5258,8 @@ async function monitorSlackProvider(opts = {}) {
4559
5258
  unresolved.push(entry.input);
4560
5259
  continue;
4561
5260
  }
4562
- mapping.push(formatSlackChannelResolved(entry));
5261
+ const resolvedLabel = formatSlackChannelResolved(entry);
5262
+ if (resolvedLabel) mapping.push(resolvedLabel);
4563
5263
  const existing = nextChannels[entry.id] ?? {};
4564
5264
  nextChannels[entry.id] = {
4565
5265
  ...source,
@@ -4647,10 +5347,10 @@ async function monitorSlackProvider(opts = {}) {
4647
5347
  abortSignal: opts.abortSignal,
4648
5348
  onStarted: () => {
4649
5349
  reconnectAttempts = 0;
4650
- publishSlackConnectedStatus(opts.setStatus);
5350
+ publishSlackConnectedStatus(opts.setStatus, identityHealth);
4651
5351
  if (!hasLoggedSocketConnected) {
4652
5352
  hasLoggedSocketConnected = true;
4653
- runtime.log?.("slack socket mode connected");
5353
+ runtime.log?.(identityHealth.healthState === "degraded" ? "slack socket mode connected (degraded identity)" : "slack socket mode connected");
4654
5354
  }
4655
5355
  }
4656
5356
  });
@@ -4712,6 +5412,7 @@ async function monitorSlackProvider(opts = {}) {
4712
5412
  });
4713
5413
  }
4714
5414
  } finally {
5415
+ await presenceMonitor?.stop();
4715
5416
  if (slackMode === "relay") setSlackDefaultSendIdentity(account.accountId, void 0);
4716
5417
  opts.abortSignal?.removeEventListener("abort", stopOnAbort);
4717
5418
  unregisterSocketModeConnectionDiagnostics();
@@ -4721,4 +5422,4 @@ async function monitorSlackProvider(opts = {}) {
4721
5422
  }
4722
5423
  const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
4723
5424
  //#endregion
4724
- export { stripSlackMentionsForCommandDetection as C, buildSlackSlashCommandMatcher as S, readSessionUpdatedAt as _, SLACK_SOCKET_RECONNECT_POLICY as a, resolveStorePath$1 as b, recordInboundSession as c, resolveSlackCommandIngress as d, resolveSlackEffectiveAllowFrom as f, resolveSlackChatType as g, normalizeSlackChannelType as h, authorizeSlackDirectMessage as i, resolveConversationLabel$1 as l, parseSlackAssistantThreadMetadata as m, resolveSlackRuntimeGroupPolicy as n, formatUnknownError as o, buildSlackAssistantThreadMetadata as p, resolveSlackRoomContextHints as r, escapeSlackMrkdwn as s, monitorSlackProvider as t, authorizeSlackBotRoomMessage as u, resolveChannelContextVisibilityMode as v, updateLastRoute as x, resolveChannelResetConfig as y };
5425
+ export { buildSlackSlashCommandMatcher as C, updateLastRoute as S, resolveSlackChatType as _, createSlackResponseUrlBudget as a, resolveChannelResetConfig as b, formatUnknownError as c, authorizeSlackBotRoomMessage as d, resolveSlackCommandIngress as f, normalizeSlackChannelType as g, parseSlackAssistantThreadMetadata as h, SlackResponseAlreadyReportedError as i, recordInboundSession as l, buildSlackAssistantThreadMetadata as m, resolveSlackRuntimeGroupPolicy as n, authorizeSlackDirectMessage as o, resolveSlackEffectiveAllowFrom as p, resolveSlackRoomContextHints as r, SLACK_SOCKET_RECONNECT_POLICY as s, monitorSlackProvider as t, resolveConversationLabel$1 as u, readSessionUpdatedAt as v, stripSlackMentionsForCommandDetection as w, resolveStorePath$1 as x, resolveChannelContextVisibilityMode as y };