@openclaw/slack 2026.7.2-beta.1 → 2026.7.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/{account-configured-DzC407uv.js → account-configured-BwfXBo_F.js} +1 -1
  2. package/dist/{account-inspect-Di7ditqZ.js → account-inspect-Bvbtu3iH.js} +9 -2
  3. package/dist/account-inspect-api.js +1 -1
  4. package/dist/{accounts-BOJJiHSr.js → accounts-DQEbYKh9.js} +9 -0
  5. package/dist/accounts.runtime-BIKmJdtJ.js +2 -0
  6. package/dist/{action-runtime-p6M2ULpv.js → action-runtime-DqkRwO3Z.js} +14 -17
  7. package/dist/action-runtime.runtime-DEWOcFM4.js +2 -0
  8. package/dist/{actions-DkTcWmpT.js → actions-CF0tUY5q.js} +5 -9
  9. package/dist/{actions.runtime-BSxpwgUc.js → actions.runtime-hCbx4RXg.js} +1 -1
  10. package/dist/api.js +13 -14
  11. package/dist/{approval-handler.runtime-UGbnz9OC.js → approval-handler.runtime-Em8I1-8-.js} +2 -2
  12. package/dist/{channel-B0_j6C6z.js → channel-4-TRi1D6.js} +75 -41
  13. package/dist/channel-config-api.js +1 -1
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/{channel-type-BpeTezY4.js → channel-type-Cu1yjNg2.js} +3 -3
  16. package/dist/{channel.setup-BQFut21T.js → channel.setup-CgkADtZg.js} +6 -4
  17. package/dist/{config-schema-DYI01PV2.js → config-schema-XjJGfwQC.js} +18 -5
  18. package/dist/{directory-config-DRkFujwG.js → directory-config-Bv0lcUlH.js} +1 -1
  19. package/dist/directory-contract-api.js +1 -1
  20. package/dist/{directory-live-DX7yFsM-.js → directory-live-CzUIB7th.js} +2 -2
  21. package/dist/{doctor-contract-DddfkMlF.js → doctor-contract-DKpe5VBr.js} +80 -12
  22. package/dist/doctor-contract-api.js +1 -1
  23. package/dist/{group-policy-C-b2ZnSl.js → group-policy-Bw-65m3V.js} +94 -135
  24. package/dist/{interactive-replies-BilWUdcI.js → interactive-replies-Dm_t2R9S.js} +1 -1
  25. package/dist/interactive-replies-api.js +1 -1
  26. package/dist/{message-tool-api-CA9jZrCY.js → message-tool-api-C2pcf5du.js} +3 -3
  27. package/dist/message-tool-api.js +1 -1
  28. package/dist/{monitor-BN_uuvrQ.js → monitor-CNFeF30h.js} +3 -3
  29. package/dist/{outbound-adapter-ECl0go7d.js → outbound-adapter-CVi2uok0.js} +49 -5
  30. package/dist/{pipeline.runtime-f0eJJoJL.js → pipeline.runtime-CTvokbty.js} +485 -251
  31. package/dist/{policy-f8b0edSi.js → policy-b_dOPDCq.js} +7 -7
  32. package/dist/probe-Borhn9PQ.js +340 -0
  33. package/dist/{provider-Ds-y-imP.js → provider-B-HVL5ZE.js} +831 -329
  34. package/dist/{relay-source-B-6LFUdz.js → relay-source-Ea_P9VcO.js} +6 -11
  35. package/dist/{replies-BMvf3ihj.js → replies-B4bOdrYb.js} +3 -3
  36. package/dist/{resolve-channels-DOMgNCWb.js → resolve-channels-KTc568SU.js} +1 -1
  37. package/dist/{resolve-users-aL6-Ssee.js → resolve-users-DeGJf1yp.js} +1 -1
  38. package/dist/{runtime-CauEvswP.js → runtime-JSVZSWAj.js} +2 -2
  39. package/dist/{runtime-api-CVb6CQjX.js → runtime-api-BFfNpzDe.js} +1 -1
  40. package/dist/runtime-api.js +12 -12
  41. package/dist/runtime-setter-api.js +1 -1
  42. package/dist/{scopes-Dw8wbU0j.js → scopes-lx08L56K.js} +1 -2
  43. package/dist/{secret-contract-CMohXFWS.js → secret-contract-DxmvbxBF.js} +1 -0
  44. package/dist/secret-contract-api.js +1 -1
  45. package/dist/{send-NEfJ0GvM.js → send-KuTDky7i.js} +29 -12
  46. package/dist/send.runtime-CXyxws1D.js +2 -0
  47. package/dist/setup-core-CeiAVwQ3.js +502 -0
  48. package/dist/setup-plugin-api.js +1 -1
  49. package/dist/{setup-surface-BN2WeG5c.js → setup-surface-DLA0vHXp.js} +23 -13
  50. package/dist/{shared-BLnB9R1T.js → shared-C4yVB0Q-.js} +64 -41
  51. package/dist/{slash-dispatch.runtime-C0Otuf_T.js → slash-dispatch.runtime-BT-mMm-f.js} +1 -1
  52. package/npm-shrinkwrap.json +3 -3
  53. package/openclaw.plugin.json +134 -10
  54. package/package.json +5 -7
  55. package/dist/accounts.runtime-KvftRRek.js +0 -2
  56. package/dist/action-runtime.runtime-DQstDkHG.js +0 -2
  57. package/dist/client-D2F6m8CP.js +0 -152
  58. package/dist/configured-state.js +0 -11
  59. package/dist/probe-CfALk0l4.js +0 -48
  60. package/dist/send.runtime-BkKpitPu.js +0 -2
  61. package/dist/setup-core-fGd6Gquy.js +0 -343
@@ -1,17 +1,17 @@
1
1
  import { n as registerSlackHttpHandler, r as normalizeSlackWebhookPath } from "./registry-bbjH7IHX.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 { $ as isSlackAnyNativeApprovalClientEnabled, F as SLACK_TEXT_LIMIT, K as escapeSlackMrkdwn, U as isSlackApprovalActionId, W as isSlackCallbackActionId, a as allowListMatches, c as normalizeSlackAllowOwnerEntry, dt as isSlackExecApprovalAuthorizedSender, l as normalizeSlackSlug, lt as isSlackApprovalAuthorizedSender, o as normalizeAllowList, q as decodeSlackApprovalAction, r as formatSlackError, s as normalizeAllowListLower, u as resolveSlackAllowListMatch, z as truncateSlackText } from "./group-policy-C-b2ZnSl.js";
4
- import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-BilWUdcI.js";
5
- import { o as getSlackListenerUploadCompletionClient, u as resolveSlackWebClientOptions } from "./client-D2F6m8CP.js";
6
- import { t as getOptionalSlackRuntime } from "./runtime-CauEvswP.js";
7
- import { i as setSlackDefaultSendIdentity } from "./send-NEfJ0GvM.js";
8
- import { t as resolveSlackChannelAllowlist } from "./resolve-channels-DOMgNCWb.js";
9
- import { t as resolveSlackUserAllowlist } from "./resolve-users-aL6-Ssee.js";
10
- import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-f8b0edSi.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-DQEbYKh9.js";
3
+ import { F as SLACK_TEXT_LIMIT, G as isSlackQuestionActionId, K as decodeSlackQuestionAction, U as isSlackApprovalActionId, W as isSlackCallbackActionId, X as decodeSlackApprovalAction, Y as escapeSlackMrkdwn, a as allowListMatches, c as normalizeSlackAllowOwnerEntry, ft as isSlackApprovalAuthorizedSender, l as normalizeSlackSlug, mt as isSlackExecApprovalAuthorizedSender, nt as isSlackAnyNativeApprovalClientEnabled, o as normalizeAllowList, q as resolveSlackQuestionAction, s as normalizeAllowListLower, u as resolveSlackAllowListMatch, z as truncateSlackText } from "./group-policy-Bw-65m3V.js";
4
+ import { n as isSlackInteractiveRepliesEnabled, t as compileSlackInteractiveReplies } from "./interactive-replies-Dm_t2R9S.js";
5
+ import { m as resolveSlackWebClientOptions, o as createSlackStartupAuthClient, r as formatSlackError, u as getSlackListenerUploadCompletionClient } from "./probe-Borhn9PQ.js";
6
+ import { n as getSlackRuntime } from "./runtime-JSVZSWAj.js";
7
+ import { i as setSlackDefaultSendIdentity } from "./send-KuTDky7i.js";
8
+ import { t as resolveSlackChannelAllowlist } from "./resolve-channels-KTc568SU.js";
9
+ import { t as resolveSlackUserAllowlist } from "./resolve-users-DeGJf1yp.js";
10
+ import { n as resolveSlackChannelConfig, r as resolveSlackChannelLabel, t as isSlackChannelAllowedByPolicy } from "./policy-b_dOPDCq.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";
14
- import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
14
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain, createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
15
15
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
16
16
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
17
17
  import { computeBackoff, createNonExitingRuntime, createSubsystemLogger, danger, getChildLogger, logVerbose, shouldLogVerbose, sleepWithAbort, warn } from "openclaw/plugin-sdk/runtime-env";
@@ -20,12 +20,12 @@ 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, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
23
+ import { collectErrorGraphCandidates, extractErrorCode, 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
26
  import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime";
27
- import { createDedupeCache, createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
28
- import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, recordInboundSession, resolveConversationLabel as resolveConversationLabel$1, resolvePluginConversationBindingApproval, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
27
+ import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
28
+ import { buildPluginBindingResolvedText, parsePluginBindingApprovalCustomId, resolveConversationLabel as resolveConversationLabel$1, resolvePluginConversationBindingApproval, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
29
29
  import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "openclaw/plugin-sdk/native-command-config-runtime";
30
30
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
31
31
  import { DEFAULT_GROUP_HISTORY_LIMIT } from "openclaw/plugin-sdk/reply-history";
@@ -45,6 +45,7 @@ import { requestHeartbeat } from "openclaw/plugin-sdk/heartbeat-runtime";
45
45
  import { createInteractiveConversationBindingHelpers, dispatchPluginInteractiveHandler } from "openclaw/plugin-sdk/plugin-runtime";
46
46
  import { createChannelIngressResolver, defineStableChannelIngressIdentity, readChannelIngressStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/channel-ingress-runtime";
47
47
  import { createChannelInboundDebouncer, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
48
+ import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
48
49
  import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime";
49
50
  //#region extensions/slack/src/monitor/commands.ts
50
51
  /**
@@ -94,9 +95,26 @@ function resolveSlackChatType(channelType) {
94
95
  return "channel";
95
96
  }
96
97
  //#endregion
98
+ //#region extensions/slack/src/monitor/lru-map-cache.ts
99
+ /** LRU-touch read: move hit to newest so pruneMapToMaxSize keeps active keys. */
100
+ function readLruMapEntry(cache, cacheKey) {
101
+ const cached = cache.get(cacheKey);
102
+ if (cached) {
103
+ cache.delete(cacheKey);
104
+ cache.set(cacheKey, cached);
105
+ }
106
+ return cached;
107
+ }
108
+ function writeLruMapEntry(cache, cacheKey, entry, maxEntries) {
109
+ cache.delete(cacheKey);
110
+ cache.set(cacheKey, entry);
111
+ pruneMapToMaxSize(cache, maxEntries);
112
+ }
113
+ //#endregion
97
114
  //#region extensions/slack/src/monitor/context.ts
98
115
  const SLACK_ASSISTANT_THREAD_CONTEXT_METADATA_EVENT = "assistant_thread_context";
99
116
  const SLACK_CHANNEL_CACHE_MAX_ENTRIES = 1024;
117
+ const SLACK_USER_CACHE_MAX_ENTRIES = 2048;
100
118
  const SLACK_CHANNEL_DENIAL_WARNING_TTL_MS = 5 * 6e4;
101
119
  const SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES = 1024;
102
120
  function buildSlackAssistantThreadMetadata(context) {
@@ -133,10 +151,6 @@ function createSlackMonitorContext(params) {
133
151
  const logger = getChildLogger({ module: "slack-auto-reply" });
134
152
  const channelCache = /* @__PURE__ */ new Map();
135
153
  const userCache = /* @__PURE__ */ new Map();
136
- const seenMessages = createDedupeCache({
137
- ttlMs: 6e4,
138
- maxSize: 500
139
- });
140
154
  const channelDenialWarnings = createDedupeCache({
141
155
  ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
142
156
  maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
@@ -150,46 +164,25 @@ function createSlackMonitorContext(params) {
150
164
  const hasChannelAllowlistConfig = Object.keys(params.channelsConfig ?? {}).length > 0;
151
165
  const channelsConfigKeys = Object.keys(params.channelsConfig ?? {});
152
166
  const scopedKey = (key, eventScope) => eventScope ? `${params.accountId}:${eventScope.teamId}:${key}` : key;
153
- const readCachedChannel = (cacheKey) => {
154
- const cached = channelCache.get(cacheKey);
155
- if (cached) {
156
- channelCache.delete(cacheKey);
157
- channelCache.set(cacheKey, cached);
158
- }
159
- return cached;
160
- };
161
- const writeCachedChannel = (cacheKey, entry) => {
162
- channelCache.delete(cacheKey);
163
- channelCache.set(cacheKey, entry);
164
- pruneMapToMaxSize(channelCache, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
165
- };
166
167
  const rememberSlackChannelType = (channelId, channelType, eventScope) => {
167
168
  const id = normalizeOptionalString(channelId);
168
169
  const normalizedType = normalizeOptionalString(channelType)?.toLowerCase();
169
170
  if (!id || normalizedType !== "im" && normalizedType !== "mpim" && normalizedType !== "channel" && normalizedType !== "group") return;
170
171
  const cacheKey = scopedKey(id, eventScope);
171
- const cached = readCachedChannel(cacheKey);
172
+ const cached = readLruMapEntry(channelCache, cacheKey);
172
173
  const type = normalizeSlackChannelType(normalizedType, id);
173
174
  if (cached?.info.type === type) return;
174
- writeCachedChannel(cacheKey, {
175
+ writeLruMapEntry(channelCache, cacheKey, {
175
176
  info: {
176
177
  ...cached?.info,
177
178
  type
178
179
  },
179
180
  metadataLoaded: cached?.metadataLoaded ?? false
180
- });
181
+ }, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
181
182
  };
182
183
  const recallSlackChannelType = (channelId, eventScope) => {
183
184
  const id = normalizeOptionalString(channelId);
184
- return id ? readCachedChannel(scopedKey(id, eventScope))?.info.type : void 0;
185
- };
186
- const markMessageSeen = (channelId, ts, eventScope) => {
187
- if (!channelId || !ts) return false;
188
- return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
189
- };
190
- const releaseSeenMessage = (channelId, ts, eventScope) => {
191
- if (!channelId || !ts) return;
192
- seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
185
+ return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
193
186
  };
194
187
  const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
195
188
  const cleanupAssistantThreadContexts = () => {
@@ -280,7 +273,7 @@ function createSlackMonitorContext(params) {
280
273
  };
281
274
  const resolveChannelName = async (channelId, eventScope) => {
282
275
  const cacheKey = scopedKey(channelId, eventScope);
283
- const cached = readCachedChannel(cacheKey);
276
+ const cached = readLruMapEntry(channelCache, cacheKey);
284
277
  if (cached?.metadataLoaded) return cached.info;
285
278
  try {
286
279
  const info = await (eventScope?.client ?? params.app.client).conversations.info({
@@ -301,7 +294,7 @@ function createSlackMonitorContext(params) {
301
294
  },
302
295
  metadataLoaded: true
303
296
  };
304
- writeCachedChannel(cacheKey, entry);
297
+ writeLruMapEntry(channelCache, cacheKey, entry, SLACK_CHANNEL_CACHE_MAX_ENTRIES);
305
298
  return entry.info;
306
299
  } catch {
307
300
  return cached?.info ?? {};
@@ -309,7 +302,7 @@ function createSlackMonitorContext(params) {
309
302
  };
310
303
  const resolveUserName = async (userId, eventScope) => {
311
304
  const cacheKey = scopedKey(userId, eventScope);
312
- const cached = userCache.get(cacheKey);
305
+ const cached = readLruMapEntry(userCache, cacheKey);
313
306
  if (cached) return cached;
314
307
  try {
315
308
  const info = await (eventScope?.client ?? params.app.client).users.info({
@@ -318,7 +311,7 @@ function createSlackMonitorContext(params) {
318
311
  });
319
312
  const profile = info.user?.profile;
320
313
  const entry = { name: profile?.display_name || profile?.real_name || info.user?.name || void 0 };
321
- userCache.set(cacheKey, entry);
314
+ writeLruMapEntry(userCache, cacheKey, entry, SLACK_USER_CACHE_MAX_ENTRIES);
322
315
  return entry;
323
316
  } catch {
324
317
  return {};
@@ -468,7 +461,6 @@ function createSlackMonitorContext(params) {
468
461
  replyToMode: params.replyToMode,
469
462
  threadHistoryScope: params.threadHistoryScope,
470
463
  threadInheritParent: params.threadInheritParent,
471
- threadRequireExplicitMention: params.threadRequireExplicitMention,
472
464
  slashCommand: params.slashCommand,
473
465
  textLimit: params.textLimit,
474
466
  ackReactionScope: params.ackReactionScope,
@@ -476,8 +468,6 @@ function createSlackMonitorContext(params) {
476
468
  mediaMaxBytes: params.mediaMaxBytes,
477
469
  removeAckAfterReply: params.removeAckAfterReply,
478
470
  logger,
479
- markMessageSeen,
480
- releaseSeenMessage,
481
471
  shouldDropMismatchedSlackEvent,
482
472
  resolveSlackSystemEventSessionKey,
483
473
  isChannelAllowed,
@@ -606,6 +596,20 @@ function resolveSlackInstallationIdentity(params) {
606
596
  ...enterpriseId ? { enterpriseId } : {}
607
597
  };
608
598
  }
599
+ function resolveSlackIdentityHealth(params) {
600
+ if (params.installationIdentity.kind === "enterprise") return {
601
+ healthState: "healthy",
602
+ lastError: null
603
+ };
604
+ const lastError = normalizeOptionalString(params.authTestError) ?? normalizeOptionalString(params.authIdentityWarning) ?? (params.installationIdentity.kind === "degraded" || !params.botUserId.trim() ? "slack bot identity unavailable" : void 0);
605
+ return lastError ? {
606
+ healthState: "degraded",
607
+ lastError
608
+ } : {
609
+ healthState: "healthy",
610
+ lastError: null
611
+ };
612
+ }
609
613
  //#endregion
610
614
  //#region extensions/slack/src/monitor/events/assistant.ts
611
615
  const DEFAULT_ASSISTANT_PROMPTS = [
@@ -1909,6 +1913,26 @@ async function handleSlackBlockAction(params) {
1909
1913
  });
1910
1914
  return;
1911
1915
  }
1916
+ if (isSlackQuestionActionId(parsed.actionId)) {
1917
+ const question = decodeSlackQuestionAction(parsed.actionSummary.value);
1918
+ if (!question) {
1919
+ await respondEphemeral(respond, "This question action is invalid or expired.");
1920
+ return;
1921
+ }
1922
+ if (!(await authorizeSlackBlockAction({
1923
+ ctx: params.ctx,
1924
+ parsed,
1925
+ respond
1926
+ })).allowed) return;
1927
+ await resolveSlackQuestionAction({
1928
+ action: question,
1929
+ cfg: params.ctx.cfg,
1930
+ accountId: params.ctx.accountId,
1931
+ userId: parsed.userId,
1932
+ respond: async (text) => await respondEphemeral(respond, text)
1933
+ });
1934
+ return;
1935
+ }
1912
1936
  const pluginInteractionData = buildSlackPluginInteractionData({
1913
1937
  actionId: parsed.actionId,
1914
1938
  summary: parsed.actionSummary
@@ -2631,6 +2655,350 @@ function resolveSlackEventScope(params) {
2631
2655
  };
2632
2656
  }
2633
2657
  //#endregion
2658
+ //#region extensions/slack/src/monitor/reconnect-policy.ts
2659
+ 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;
2660
+ const NO_ERROR_DETAIL = "no error detail";
2661
+ const SLACK_SOCKET_RECONNECT_POLICY = {
2662
+ initialMs: 2e3,
2663
+ maxMs: 3e4,
2664
+ factor: 1.8,
2665
+ jitter: .25
2666
+ };
2667
+ const SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL = "https://docs.slack.dev/apis/events-api/using-socket-mode#using-multiple-connections";
2668
+ const SLACK_SOCKET_HELLO_MARKER = Buffer.from("\"hello\"");
2669
+ function getSocketEmitter(app) {
2670
+ const receiver = app.receiver;
2671
+ const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
2672
+ if (!client || typeof client !== "object") return null;
2673
+ const on = client.on;
2674
+ const off = client.off;
2675
+ if (typeof on !== "function" || typeof off !== "function") return null;
2676
+ return {
2677
+ on: (event, listener) => on.call(client, event, listener),
2678
+ off: (event, listener) => off.call(client, event, listener)
2679
+ };
2680
+ }
2681
+ function isBufferArray(value) {
2682
+ return Array.isArray(value) && value.every((entry) => Buffer.isBuffer(entry));
2683
+ }
2684
+ function resolveSlackSocketModeConnectionCount(message) {
2685
+ const buffer = Buffer.isBuffer(message) ? message : message instanceof ArrayBuffer ? Buffer.from(message) : isBufferArray(message) ? Buffer.concat(message) : void 0;
2686
+ if (!buffer?.includes(SLACK_SOCKET_HELLO_MARKER)) return;
2687
+ let payload;
2688
+ try {
2689
+ payload = JSON.parse(buffer.toString("utf8"));
2690
+ } catch {
2691
+ return;
2692
+ }
2693
+ const count = isRecord(payload) && payload.type === "hello" ? payload.num_connections : void 0;
2694
+ return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
2695
+ }
2696
+ function formatSlackSocketModeSharedConnectionWarning(activeConnections) {
2697
+ return [
2698
+ `slack socket mode reports ${activeConnections} active connections for this Slack app`,
2699
+ "Slack may deliver each event to any one connection",
2700
+ "ensure every OpenClaw gateway sharing this app has equivalent routing and authorization, or use a separate Slack app per gateway, one relay ingress, or HTTP Request URLs behind a load balancer",
2701
+ `See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
2702
+ ].join("; ");
2703
+ }
2704
+ function registerSlackSocketModeConnectionDiagnostics(params) {
2705
+ const emitter = getSocketEmitter(params.app);
2706
+ if (!emitter) return () => {};
2707
+ let hasWarned = false;
2708
+ const listener = (message, isBinary) => {
2709
+ if (isBinary === true || hasWarned) return;
2710
+ const activeConnections = resolveSlackSocketModeConnectionCount(message);
2711
+ if (activeConnections === void 0 || activeConnections <= 1) return;
2712
+ hasWarned = true;
2713
+ params.onSharedConnection(activeConnections);
2714
+ };
2715
+ emitter.on("ws_message", listener);
2716
+ return () => {
2717
+ emitter.off("ws_message", listener);
2718
+ };
2719
+ }
2720
+ function waitForSlackSocketDisconnect(app, abortSignal) {
2721
+ return new Promise((resolve) => {
2722
+ const emitter = getSocketEmitter(app);
2723
+ if (!emitter) {
2724
+ abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
2725
+ return;
2726
+ }
2727
+ const disconnectListener = () => resolveOnce({ event: "disconnect" });
2728
+ const startFailListener = (error) => resolveOnce({
2729
+ event: "unable_to_socket_mode_start",
2730
+ error
2731
+ });
2732
+ const errorListener = (error) => resolveOnce({
2733
+ event: "error",
2734
+ error
2735
+ });
2736
+ const abortListener = () => resolveOnce({ event: "disconnect" });
2737
+ const cleanup = () => {
2738
+ emitter.off("disconnected", disconnectListener);
2739
+ emitter.off("unable_to_socket_mode_start", startFailListener);
2740
+ emitter.off("error", errorListener);
2741
+ abortSignal?.removeEventListener("abort", abortListener);
2742
+ };
2743
+ const resolveOnce = (value) => {
2744
+ cleanup();
2745
+ resolve(value);
2746
+ };
2747
+ emitter.on("disconnected", disconnectListener);
2748
+ emitter.on("unable_to_socket_mode_start", startFailListener);
2749
+ emitter.on("error", errorListener);
2750
+ abortSignal?.addEventListener("abort", abortListener, { once: true });
2751
+ });
2752
+ }
2753
+ /**
2754
+ * Detect permanent Slack account and credential failures.
2755
+ * Transient request and HTTP failures stay in OpenClaw's reconnect loop.
2756
+ */
2757
+ function isNonRecoverableSlackAuthError(error) {
2758
+ return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
2759
+ }
2760
+ function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
2761
+ return formatSlackError(error, fallback);
2762
+ }
2763
+ //#endregion
2764
+ //#region extensions/slack/src/monitor/ingress.ts
2765
+ const SLACK_INGRESS_PAYLOAD_VERSION = 1;
2766
+ const SLACK_INGRESS_POLL_INTERVAL_MS = 1e3;
2767
+ const SLACK_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
2768
+ const SLACK_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
2769
+ const SLACK_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
2770
+ const SLACK_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
2771
+ const SLACK_INGRESS_FAILED_MAX_ENTRIES = 2e4;
2772
+ const SLACK_BOLT_AUTHORIZATION_ERROR = "slack_bolt_authorization_error";
2773
+ const SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY = "openclawIngressLifecycle";
2774
+ /** Logical message identity: mirrors the retired guard key (team:channel:ts). */
2775
+ function resolveSlackRelayIngressEventId(event) {
2776
+ const ts = event.message.ts?.trim();
2777
+ if (!event.message.channel?.trim() || !ts) return `relay:${event.deliveryId}`;
2778
+ const team = event.message.team?.trim();
2779
+ return `message:${team ? `${team}:` : ""}${event.message.channel.trim()}:${ts}`;
2780
+ }
2781
+ var SlackIngressPayloadError = class extends Error {
2782
+ constructor(message) {
2783
+ super(message);
2784
+ this.name = "SlackIngressPayloadError";
2785
+ }
2786
+ };
2787
+ function resolveSlackEventId(body) {
2788
+ const eventId = asOptionalRecord(body)?.event_id;
2789
+ return typeof eventId === "string" && eventId.trim() ? eventId.trim() : null;
2790
+ }
2791
+ function resolveSlackIngressLane(body, eventId) {
2792
+ const envelope = asOptionalRecord(body);
2793
+ const event = asOptionalRecord(envelope?.event);
2794
+ const item = asOptionalRecord(event?.item);
2795
+ const assistantThread = asOptionalRecord(event?.assistant_thread);
2796
+ const team = asOptionalRecord(envelope?.team);
2797
+ const teamId = [
2798
+ envelope?.team_id,
2799
+ team?.id,
2800
+ event?.team
2801
+ ].find((value) => typeof value === "string" && value.trim())?.toString().trim() || "workspace";
2802
+ const channelId = [
2803
+ event?.channel,
2804
+ event?.channel_id,
2805
+ item?.channel,
2806
+ assistantThread?.channel_id
2807
+ ].find((value) => typeof value === "string" && value.trim())?.toString().trim();
2808
+ if (channelId) return `team:${teamId}:conversation:${channelId}`;
2809
+ const userId = [event?.user, event?.user_id].find((value) => typeof value === "string" && value.trim())?.toString().trim();
2810
+ return userId ? `team:${teamId}:user:${userId}` : `event:${eventId}`;
2811
+ }
2812
+ function isSlackEventCallback(body) {
2813
+ return asOptionalRecord(body)?.type === "event_callback";
2814
+ }
2815
+ function assertSlackIngressPayload(payload, eventId) {
2816
+ if (payload.version !== SLACK_INGRESS_PAYLOAD_VERSION) throw new SlackIngressPayloadError(`Slack ingress payload ${eventId} was invalid.`);
2817
+ if (payload.kind === "relay") {
2818
+ if (!asOptionalRecord(payload.message)) throw new SlackIngressPayloadError(`Slack relay ingress payload ${eventId} was invalid.`);
2819
+ return;
2820
+ }
2821
+ if (!asOptionalRecord(payload.body) || resolveSlackEventId(payload.body) !== eventId) throw new SlackIngressPayloadError(`Slack ingress payload ${eventId} was invalid.`);
2822
+ }
2823
+ function resolveSlackIngressNonRetryableFailure(error) {
2824
+ for (const candidate of collectErrorGraphCandidates(error, (current) => [
2825
+ current.cause,
2826
+ current.error,
2827
+ current.original
2828
+ ])) {
2829
+ if (candidate instanceof SlackIngressPayloadError || candidate instanceof SyntaxError) return {
2830
+ reason: "invalid-event",
2831
+ message: formatErrorMessage(candidate)
2832
+ };
2833
+ if (extractErrorCode(candidate) === SLACK_BOLT_AUTHORIZATION_ERROR || isNonRecoverableSlackAuthError(candidate)) return {
2834
+ reason: "slack-auth",
2835
+ message: formatErrorMessage(candidate)
2836
+ };
2837
+ }
2838
+ return null;
2839
+ }
2840
+ function resolveSlackIngressTurnLifecycle(context) {
2841
+ const candidate = asOptionalRecord(context)?.[SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY];
2842
+ if (!candidate || typeof candidate !== "object") return null;
2843
+ const lifecycle = candidate;
2844
+ return typeof lifecycle.onAdopted === "function" && lifecycle.abortSignal instanceof AbortSignal ? lifecycle : null;
2845
+ }
2846
+ function createSlackDurableIngress(options) {
2847
+ let queue = options.queue;
2848
+ let drain;
2849
+ let app;
2850
+ let relayDispatch;
2851
+ let running = false;
2852
+ let requested = false;
2853
+ let pumping;
2854
+ let pollTimer;
2855
+ let lastPrunedAt = 0;
2856
+ const getQueue = () => {
2857
+ queue ??= getSlackRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
2858
+ return queue;
2859
+ };
2860
+ const getDrain = () => {
2861
+ drain ??= createChannelIngressDrain({
2862
+ queue: getQueue(),
2863
+ adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
2864
+ retryPolicy: {
2865
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
2866
+ deadLetterMinAgeMs: DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS
2867
+ },
2868
+ resolveNonRetryableFailure: resolveSlackIngressNonRetryableFailure,
2869
+ deriveLaneKey: (record) => record.payload.kind === "relay" ? resolveSlackIngressLane({ event: record.payload.message }, record.id) : resolveSlackIngressLane(record.payload.body, record.id),
2870
+ ...options.onLog ? { onLog: options.onLog } : {},
2871
+ ...options.abortSignal ? { abortSignal: options.abortSignal } : {},
2872
+ dispatchClaimedEvent: async (event, lifecycle) => {
2873
+ assertSlackIngressPayload(event.payload, event.id);
2874
+ if (lifecycle.abortSignal.aborted) throw lifecycle.abortSignal.reason;
2875
+ const bound = bindIngressLifecycleToReplyOptions(lifecycle);
2876
+ if (event.payload.kind === "relay") {
2877
+ if (!relayDispatch) throw new Error("Slack relay ingress dispatcher is not attached.");
2878
+ await relayDispatch(event.payload.message, bound.turnAdoptionLifecycle);
2879
+ return;
2880
+ }
2881
+ if (!app) throw new Error("Slack ingress receiver is not attached to a Bolt app.");
2882
+ await app.processEvent({
2883
+ body: event.payload.body,
2884
+ ack: async () => {},
2885
+ ...event.payload.retryNum === void 0 ? {} : { retryNum: event.payload.retryNum },
2886
+ ...event.payload.retryReason === void 0 ? {} : { retryReason: event.payload.retryReason },
2887
+ customProperties: { [SLACK_INGRESS_LIFECYCLE_CONTEXT_KEY]: bound.turnAdoptionLifecycle }
2888
+ });
2889
+ }
2890
+ });
2891
+ return drain;
2892
+ };
2893
+ const pruneIfDue = async () => {
2894
+ const now = Date.now();
2895
+ if (now - lastPrunedAt < SLACK_INGRESS_PRUNE_INTERVAL_MS) return;
2896
+ await getQueue().prune({
2897
+ completedTtlMs: SLACK_INGRESS_COMPLETED_TTL_MS,
2898
+ completedMaxEntries: SLACK_INGRESS_COMPLETED_MAX_ENTRIES,
2899
+ failedTtlMs: SLACK_INGRESS_FAILED_TTL_MS,
2900
+ failedMaxEntries: SLACK_INGRESS_FAILED_MAX_ENTRIES,
2901
+ now
2902
+ });
2903
+ lastPrunedAt = now;
2904
+ };
2905
+ const runPump = async () => {
2906
+ try {
2907
+ for (;;) {
2908
+ requested = false;
2909
+ await pruneIfDue();
2910
+ const activeDrain = getDrain();
2911
+ const { started } = await activeDrain.drainOnce();
2912
+ await activeDrain.waitForIdle();
2913
+ if (!running || !requested && started === 0) break;
2914
+ }
2915
+ } catch (error) {
2916
+ options.onLog?.(`slack ingress drain failed: ${formatErrorMessage(error)}`);
2917
+ } finally {
2918
+ pumping = void 0;
2919
+ if (running && requested) requestDrain();
2920
+ }
2921
+ };
2922
+ const requestDrain = () => {
2923
+ requested = true;
2924
+ if (!running || pumping) return;
2925
+ pumping = runPump();
2926
+ };
2927
+ const acceptReceiverEvent = async (event) => {
2928
+ if (!isSlackEventCallback(event.body)) {
2929
+ if (!app) throw new Error("Slack ingress receiver is not attached to a Bolt app.");
2930
+ await app.processEvent(event);
2931
+ return;
2932
+ }
2933
+ const eventId = resolveSlackEventId(event.body);
2934
+ if (!eventId) throw new SlackIngressPayloadError("Slack Events API envelope missing event_id.");
2935
+ const receivedAt = Date.now();
2936
+ await getQueue().enqueue(eventId, {
2937
+ version: SLACK_INGRESS_PAYLOAD_VERSION,
2938
+ receivedAt,
2939
+ kind: "events-api",
2940
+ body: event.body,
2941
+ ...event.retryNum === void 0 ? {} : { retryNum: event.retryNum },
2942
+ ...event.retryReason === void 0 ? {} : { retryReason: event.retryReason }
2943
+ }, { receivedAt });
2944
+ await event.ack();
2945
+ requestDrain();
2946
+ };
2947
+ const acceptRelayEvent = async (event) => {
2948
+ const receivedAt = Date.now();
2949
+ await getQueue().enqueue(resolveSlackRelayIngressEventId(event), {
2950
+ version: SLACK_INGRESS_PAYLOAD_VERSION,
2951
+ receivedAt,
2952
+ kind: "relay",
2953
+ message: event.message
2954
+ }, { receivedAt });
2955
+ requestDrain();
2956
+ };
2957
+ return {
2958
+ wrapReceiver: (receiver) => {
2959
+ const client = Reflect.get(receiver, "client");
2960
+ return {
2961
+ init: (nextApp) => {
2962
+ app = nextApp;
2963
+ receiver.init({ processEvent: acceptReceiverEvent });
2964
+ },
2965
+ start: (...args) => receiver.start(...args),
2966
+ stop: (...args) => receiver.stop(...args),
2967
+ ...client === void 0 ? {} : { client }
2968
+ };
2969
+ },
2970
+ acceptRelayEvent,
2971
+ attachRelayDispatch: (dispatch) => {
2972
+ relayDispatch = dispatch;
2973
+ },
2974
+ start: () => {
2975
+ if (running) return;
2976
+ running = true;
2977
+ pollTimer = setInterval(requestDrain, options.pollIntervalMs ?? SLACK_INGRESS_POLL_INTERVAL_MS);
2978
+ pollTimer.unref?.();
2979
+ requestDrain();
2980
+ },
2981
+ stop: async () => {
2982
+ running = false;
2983
+ if (pollTimer) {
2984
+ clearInterval(pollTimer);
2985
+ pollTimer = void 0;
2986
+ }
2987
+ drain?.dispose();
2988
+ await pumping;
2989
+ await drain?.waitForIdle();
2990
+ },
2991
+ waitForIdle: async () => {
2992
+ for (;;) {
2993
+ const activePump = pumping;
2994
+ if (!activePump) break;
2995
+ await activePump;
2996
+ }
2997
+ await drain?.waitForIdle();
2998
+ }
2999
+ };
3000
+ }
3001
+ //#endregion
2634
3002
  //#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
2635
3003
  const SUBTYPE_HANDLER_REGISTRY = {
2636
3004
  message_changed: {
@@ -2758,6 +3126,7 @@ function registerSlackMessageEvents(params) {
2758
3126
  return resolved.scope;
2759
3127
  };
2760
3128
  const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
3129
+ const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
2761
3130
  try {
2762
3131
  const eventScope = resolveEventScope({
2763
3132
  body,
@@ -2783,10 +3152,9 @@ function registerSlackMessageEvents(params) {
2783
3152
  if (assistantChangedInbound) {
2784
3153
  await handleSlackMessage(assistantChangedInbound, {
2785
3154
  source: "message",
2786
- ...eventScope ? {
2787
- eventScope,
2788
- awaitDispatch: true
2789
- } : {}
3155
+ ...eventScope ? { eventScope } : {},
3156
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3157
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2790
3158
  });
2791
3159
  return;
2792
3160
  }
@@ -2814,12 +3182,12 @@ function registerSlackMessageEvents(params) {
2814
3182
  }
2815
3183
  await handleSlackMessage(message, {
2816
3184
  source: "message",
2817
- ...eventScope ? {
2818
- eventScope,
2819
- awaitDispatch: true
2820
- } : {}
3185
+ ...eventScope ? { eventScope } : {},
3186
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3187
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2821
3188
  });
2822
3189
  } catch (err) {
3190
+ if (turnAdoptionLifecycle) throw err;
2823
3191
  ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
2824
3192
  }
2825
3193
  };
@@ -2828,6 +3196,7 @@ function registerSlackMessageEvents(params) {
2828
3196
  });
2829
3197
  ctx.app.event("app_mention", async (args) => {
2830
3198
  const { event, body, context, client } = args;
3199
+ const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
2831
3200
  try {
2832
3201
  const eventScope = resolveEventScope({
2833
3202
  body,
@@ -2854,12 +3223,12 @@ function registerSlackMessageEvents(params) {
2854
3223
  await handleSlackMessage(mention, {
2855
3224
  source: "app_mention",
2856
3225
  wasMentioned: true,
2857
- ...eventScope ? {
2858
- eventScope,
2859
- awaitDispatch: true
2860
- } : {}
3226
+ ...eventScope ? { eventScope } : {},
3227
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3228
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2861
3229
  });
2862
3230
  } catch (err) {
3231
+ if (turnAdoptionLifecycle) throw err;
2863
3232
  ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
2864
3233
  }
2865
3234
  });
@@ -3018,41 +3387,58 @@ function registerSlackMonitorEvents(params) {
3018
3387
  });
3019
3388
  }
3020
3389
  //#endregion
3021
- //#region extensions/slack/src/monitor/inbound-delivery-state.ts
3022
- const deliveredMessages = createPersistentDedupeCache({
3023
- globalKey: Symbol.for("openclaw.slackInboundDeliveries"),
3024
- ttlMs: 1440 * 60 * 1e3,
3025
- maxSize: 2e4,
3026
- persistent: {
3027
- namespace: "slack.inbound-deliveries",
3028
- maxEntries: 2e4,
3029
- openStore: (options) => getOptionalSlackRuntime()?.state.openKeyedStore(options),
3030
- logError: (error) => {
3031
- try {
3032
- getOptionalSlackRuntime()?.logging.getChildLogger({
3033
- plugin: "slack",
3034
- feature: "inbound-delivery-state"
3035
- }).warn("Slack persistent inbound delivery state failed", { error: String(error) });
3036
- } catch {}
3390
+ //#region extensions/slack/src/monitor/message-dispatch-dedupe.ts
3391
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_TTL_MS = 1440 * 60 * 1e3;
3392
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_MEMORY_MAX_ENTRIES = 2e4;
3393
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_MAX_ENTRIES = 2e4;
3394
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE = "global";
3395
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE_PREFIX = "slack.message-dispatch-dedupe";
3396
+ const SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_PLUGIN_ID = "slack-message-dispatch-dedupe";
3397
+ function buildSlackMessageDispatchReplayKey(params) {
3398
+ const channelId = params.channelId?.trim();
3399
+ const ts = params.ts?.trim();
3400
+ if (!channelId || !ts) return null;
3401
+ const teamId = params.teamId?.trim();
3402
+ return JSON.stringify([
3403
+ "message",
3404
+ params.accountId,
3405
+ teamId ?? "",
3406
+ channelId,
3407
+ ts
3408
+ ]);
3409
+ }
3410
+ function createSlackMessageDispatchReplayGuard(params = {}) {
3411
+ return createChannelReplayGuard({
3412
+ dedupe: {
3413
+ ttlMs: SLACK_MESSAGE_DISPATCH_DEDUPE_TTL_MS,
3414
+ memoryMaxSize: SLACK_MESSAGE_DISPATCH_DEDUPE_MEMORY_MAX_ENTRIES,
3415
+ pluginId: SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_PLUGIN_ID,
3416
+ namespacePrefix: SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE_PREFIX,
3417
+ stateMaxEntries: SLACK_MESSAGE_DISPATCH_DEDUPE_STATE_MAX_ENTRIES,
3418
+ ...params.onDiskError ? { onDiskError: params.onDiskError } : {}
3419
+ },
3420
+ buildReplayKey: (event) => event.keys,
3421
+ namespace: () => SLACK_MESSAGE_DISPATCH_DEDUPE_NAMESPACE
3422
+ });
3423
+ }
3424
+ /** Claim one logical message key; an in-flight sibling claim settles to duplicate. */
3425
+ async function claimSlackMessageDispatchReplay(params) {
3426
+ let releaseRetries = 0;
3427
+ while (true) {
3428
+ const claim = await params.guard.claim({ keys: [params.key] });
3429
+ if (claim.kind === "claimed") return {
3430
+ kind: "claimed",
3431
+ handle: claim.handle
3432
+ };
3433
+ if (claim.kind === "duplicate" || claim.kind === "invalid") return { kind: "duplicate" };
3434
+ try {
3435
+ await claim.pending;
3436
+ return { kind: "duplicate" };
3437
+ } catch {
3438
+ releaseRetries += 1;
3439
+ if (releaseRetries > 1) return { kind: "duplicate" };
3037
3440
  }
3038
3441
  }
3039
- });
3040
- function makeKey(accountId, channelId, ts, teamId) {
3041
- return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
3042
- }
3043
- async function hasSlackInboundMessageDelivery(params) {
3044
- if (!params.accountId || !params.channelId || !params.ts) return false;
3045
- return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
3046
- }
3047
- async function recordSlackInboundMessageDeliveries(params) {
3048
- if (!params.accountId || params.messages.length === 0) return;
3049
- const deliveredAt = Date.now();
3050
- const keys = /* @__PURE__ */ new Set();
3051
- for (const message of params.messages) {
3052
- if (!message.channel || !message.ts) continue;
3053
- keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
3054
- }
3055
- await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
3056
3442
  }
3057
3443
  //#endregion
3058
3444
  //#region extensions/slack/src/monitor/message-handler/debounce-key.ts
@@ -3180,7 +3566,7 @@ function createSlackThreadTsResolver(params) {
3180
3566
  }
3181
3567
  //#endregion
3182
3568
  //#region extensions/slack/src/monitor/message-handler.ts
3183
- const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-f0eJJoJL.js"));
3569
+ const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-CTvokbty.js"));
3184
3570
  function createSlackDispatchCompletion() {
3185
3571
  let resolve;
3186
3572
  let reject;
@@ -3193,7 +3579,6 @@ function createSlackDispatchCompletion() {
3193
3579
  reject
3194
3580
  };
3195
3581
  }
3196
- const APP_MENTION_RETRY_TTL_MS = 6e4;
3197
3582
  const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
3198
3583
  const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
3199
3584
  const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
@@ -3207,12 +3592,9 @@ function shouldDebounceSlackMessage(message, cfg) {
3207
3592
  hasMedia: Boolean(message.files && message.files.length > 0)
3208
3593
  });
3209
3594
  }
3210
- function buildSeenMessageKey(channelId, ts, teamId) {
3211
- if (!channelId || !ts) return null;
3212
- return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
3213
- }
3214
3595
  function createSlackMessageHandler(params) {
3215
- const { ctx, account, trackEvent } = params;
3596
+ const { ctx, account, trackEvent, onPrepared } = params;
3597
+ const dispatchReplayGuard = params.dispatchReplayGuard ?? createSlackMessageDispatchReplayGuard({ onDiskError: (error) => ctx.runtime.error?.(`slack message dispatch dedupe persistence failed: ${formatErrorMessage(error)}`) });
3216
3598
  const { debounceMs, debouncer } = createChannelInboundDebouncer({
3217
3599
  cfg: ctx.cfg,
3218
3600
  channel: "slack",
@@ -3245,8 +3627,41 @@ function createSlackMessageHandler(params) {
3245
3627
  const completions = entries.map((entry) => entry.opts.dispatchCompletion).filter((completion) => completion !== void 0);
3246
3628
  try {
3247
3629
  await (async () => {
3248
- const last = entries.at(-1);
3249
- if (!last) return;
3630
+ const claims = [];
3631
+ const claimedKeys = /* @__PURE__ */ new Set();
3632
+ const surviving = [];
3633
+ for (const entry of entries) {
3634
+ const replayKey = buildSlackMessageDispatchReplayKey({
3635
+ accountId: ctx.accountId,
3636
+ channelId: entry.message.channel,
3637
+ ts: entry.message.ts,
3638
+ teamId: entry.opts.eventScope?.teamId
3639
+ });
3640
+ if (!replayKey || claimedKeys.has(replayKey)) {
3641
+ surviving.push(entry);
3642
+ continue;
3643
+ }
3644
+ const claim = await claimSlackMessageDispatchReplay({
3645
+ guard: dispatchReplayGuard,
3646
+ key: replayKey
3647
+ });
3648
+ if (claim.kind === "claimed") {
3649
+ claims.push(claim.handle);
3650
+ claimedKeys.add(replayKey);
3651
+ surviving.push(entry);
3652
+ }
3653
+ }
3654
+ const releaseClaims = (error) => {
3655
+ for (const handle of claims) handle.release(error === void 0 ? {} : { error });
3656
+ };
3657
+ const commitClaims = async () => {
3658
+ for (const handle of claims) await handle.commit();
3659
+ };
3660
+ const last = surviving.at(-1);
3661
+ if (!last) {
3662
+ releaseClaims();
3663
+ return;
3664
+ }
3250
3665
  const teamId = last.opts.eventScope?.teamId;
3251
3666
  const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
3252
3667
  const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
@@ -3257,75 +3672,60 @@ function createSlackMessageHandler(params) {
3257
3672
  if (pendingKeys.size === 0) pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
3258
3673
  }
3259
3674
  }
3260
- const combinedText = entries.length === 1 ? last.message.text ?? "" : entries.map((entry) => entry.message.text ?? "").filter(Boolean).join("\n");
3261
- const combinedMentioned = entries.some((entry) => Boolean(entry.opts.wasMentioned));
3675
+ const combinedText = surviving.length === 1 ? last.message.text ?? "" : surviving.map((entry) => entry.message.text ?? "").filter(Boolean).join("\n");
3676
+ const combinedMentioned = surviving.some((entry) => Boolean(entry.opts.wasMentioned));
3262
3677
  const syntheticMessage = {
3263
3678
  ...last.message,
3264
3679
  text: combinedText
3265
3680
  };
3266
- const seenMessageKey = buildSeenMessageKey(last.message.channel, last.message.ts, last.opts.eventScope?.teamId);
3681
+ const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
3682
+ const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, turnAdoptionLifecycle, ...lastOpts } = last.opts;
3683
+ let prepared;
3684
+ let settlementHandedOff = false;
3267
3685
  try {
3268
- const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
3269
- const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, ...lastOpts } = last.opts;
3270
- const appMentionRetryKey = seenMessageKey && lastOpts.source === "app_mention" && !ctx.botUserId ? seenMessageKey : void 0;
3271
- if (appMentionRetryKey) appMentionPreparingKeys.add(appMentionRetryKey);
3272
- const prepared = await (async () => {
3273
- try {
3274
- const result = await prepareSlackMessage({
3275
- ctx,
3276
- account,
3277
- message: syntheticMessage,
3278
- opts: {
3279
- ...lastOpts,
3280
- wasMentioned: combinedMentioned || last.opts.wasMentioned,
3281
- ...seenMessageKey && lastOpts.source === "message" ? { shouldRecordDroppedHistory: () => !appMentionPreparingKeys.has(seenMessageKey) && !appMentionDispatchedKeys.has(seenMessageKey) } : {}
3282
- }
3283
- });
3284
- if (result && seenMessageKey) {
3285
- pruneAppMentionRetryKeys(Date.now());
3286
- if (last.opts.source === "app_mention") rememberExpiringAppMentionKey(appMentionDispatchedKeys, seenMessageKey);
3287
- else if (last.opts.source === "message" && appMentionDispatchedKeys.has(seenMessageKey)) {
3288
- appMentionDispatchedKeys.delete(seenMessageKey);
3289
- appMentionRetryKeys.delete(seenMessageKey);
3290
- return null;
3291
- }
3292
- appMentionRetryKeys.delete(seenMessageKey);
3293
- }
3294
- return result;
3295
- } finally {
3296
- if (appMentionRetryKey) appMentionPreparingKeys.delete(appMentionRetryKey);
3686
+ prepared = await prepareSlackMessage({
3687
+ ctx,
3688
+ account,
3689
+ message: syntheticMessage,
3690
+ opts: {
3691
+ ...lastOpts,
3692
+ wasMentioned: combinedMentioned || last.opts.wasMentioned
3297
3693
  }
3298
- })();
3299
- if (!prepared) return;
3300
- if (entries.length > 1) {
3301
- const ids = entries.map((entry) => entry.message.ts).filter(Boolean);
3694
+ });
3695
+ if (!prepared) {
3696
+ releaseClaims();
3697
+ return;
3698
+ }
3699
+ prepared.turnAdoptionLifecycle = turnAdoptionLifecycle ? {
3700
+ ...turnAdoptionLifecycle,
3701
+ onAdopted: async () => {
3702
+ settlementHandedOff = true;
3703
+ await commitClaims();
3704
+ await turnAdoptionLifecycle.onAdopted();
3705
+ },
3706
+ onDeferred: () => {
3707
+ settlementHandedOff = true;
3708
+ turnAdoptionLifecycle.onDeferred();
3709
+ },
3710
+ onAbandoned: () => {
3711
+ releaseClaims();
3712
+ turnAdoptionLifecycle.onAbandoned();
3713
+ }
3714
+ } : turnAdoptionLifecycle;
3715
+ onPrepared?.(prepared);
3716
+ if (surviving.length > 1) {
3717
+ const ids = surviving.map((entry) => entry.message.ts).filter(Boolean);
3302
3718
  if (ids.length > 0) {
3303
3719
  prepared.ctxPayload.MessageSids = ids;
3304
3720
  prepared.ctxPayload.MessageSidFirst = ids[0];
3305
3721
  prepared.ctxPayload.MessageSidLast = ids[ids.length - 1];
3306
3722
  }
3307
3723
  }
3308
- try {
3309
- await dispatchPreparedSlackMessage(prepared);
3310
- await recordSlackInboundMessageDeliveries({
3311
- accountId: ctx.accountId,
3312
- ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
3313
- messages: entries.map((entry) => entry.message)
3314
- });
3315
- } catch (error) {
3316
- if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
3317
- accountId: ctx.accountId,
3318
- ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
3319
- messages: entries.map((entry) => entry.message)
3320
- });
3321
- throw error;
3322
- }
3724
+ await dispatchPreparedSlackMessage(prepared);
3725
+ if (!turnAdoptionLifecycle) await commitClaims();
3726
+ else if (!settlementHandedOff) releaseClaims();
3323
3727
  } catch (error) {
3324
- if (isRetryableSlackInboundError(error)) for (const entry of entries) {
3325
- const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
3326
- if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
3327
- ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
3328
- }
3728
+ releaseClaims(error);
3329
3729
  throw error;
3330
3730
  }
3331
3731
  })();
@@ -3342,51 +3742,10 @@ function createSlackMessageHandler(params) {
3342
3742
  });
3343
3743
  const threadTsResolver = createSlackThreadTsResolver({ client: ctx.app.client });
3344
3744
  const pendingTopLevelDebounceKeys = /* @__PURE__ */ new Map();
3345
- const appMentionRetryKeys = /* @__PURE__ */ new Map();
3346
- const appMentionPreparingKeys = /* @__PURE__ */ new Set();
3347
- const appMentionDispatchedKeys = /* @__PURE__ */ new Map();
3348
- const pruneAppMentionRetryKeys = (rawNow) => {
3349
- const now = asDateTimestampMs(rawNow);
3350
- if (now === void 0) {
3351
- appMentionRetryKeys.clear();
3352
- appMentionDispatchedKeys.clear();
3353
- return false;
3354
- }
3355
- for (const [key, expiresAt] of appMentionRetryKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionRetryKeys.delete(key);
3356
- for (const [key, expiresAt] of appMentionDispatchedKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionDispatchedKeys.delete(key);
3357
- return true;
3358
- };
3359
- const rememberExpiringAppMentionKey = (map, key) => {
3360
- const now = Date.now();
3361
- if (!pruneAppMentionRetryKeys(now)) return;
3362
- const expiresAt = resolveExpiresAtMsFromDurationMs(APP_MENTION_RETRY_TTL_MS, { nowMs: now });
3363
- if (expiresAt !== void 0) map.set(key, expiresAt);
3364
- };
3365
- const rememberAppMentionRetryKey = (key) => {
3366
- rememberExpiringAppMentionKey(appMentionRetryKeys, key);
3367
- };
3368
- const consumeAppMentionRetryKey = (key) => {
3369
- if (!pruneAppMentionRetryKeys(Date.now())) return false;
3370
- if (!appMentionRetryKeys.has(key)) return false;
3371
- appMentionRetryKeys.delete(key);
3372
- return true;
3373
- };
3374
3745
  async function enqueueSlackMessage(message, opts) {
3375
3746
  if (opts.source === "message" && message.type !== "message") return;
3376
3747
  if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
3377
3748
  ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
3378
- const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
3379
- if (seenMessageKey && await hasSlackInboundMessageDelivery({
3380
- accountId: ctx.accountId,
3381
- channelId: message.channel,
3382
- ts: message.ts,
3383
- ...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
3384
- })) return;
3385
- const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
3386
- if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
3387
- if (seenMessageKey && wasSeen) {
3388
- if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
3389
- }
3390
3749
  trackEvent?.();
3391
3750
  const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
3392
3751
  message,
@@ -3426,110 +3785,201 @@ function createSlackMessageHandler(params) {
3426
3785
  };
3427
3786
  }
3428
3787
  //#endregion
3429
- //#region extensions/slack/src/monitor/reconnect-policy.ts
3430
- 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;
3431
- const NO_ERROR_DETAIL = "no error detail";
3432
- const SLACK_SOCKET_RECONNECT_POLICY = {
3433
- initialMs: 2e3,
3434
- maxMs: 3e4,
3435
- factor: 1.8,
3436
- jitter: .25
3437
- };
3438
- const SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL = "https://docs.slack.dev/apis/events-api/using-socket-mode#using-multiple-connections";
3439
- const SLACK_SOCKET_HELLO_MARKER = Buffer.from("\"hello\"");
3440
- function getSocketEmitter(app) {
3441
- const receiver = app.receiver;
3442
- const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
3443
- if (!client || typeof client !== "object") return null;
3444
- const on = client.on;
3445
- const off = client.off;
3446
- if (typeof on !== "function" || typeof off !== "function") return null;
3788
+ //#region extensions/slack/src/monitor/presence-monitor.ts
3789
+ const SLACK_PRESENCE_GREETING_COOLDOWN_MS = 480 * 60 * 1e3;
3790
+ const SLACK_PRESENCE_POLL_INTERVAL_MS = 6e4;
3791
+ const SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS = 8;
3792
+ const SLACK_PRESENCE_TARGET_TTL_MS = 1440 * 60 * 1e3;
3793
+ const SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL = 45;
3794
+ const SLACK_PRESENCE_MAX_TARGETS = 2e3;
3795
+ function resolveMode(channelConfig, accountConfig) {
3796
+ return channelConfig?.mode ?? accountConfig?.mode ?? "off";
3797
+ }
3798
+ function hasSlackPresenceEventsEnabled(params) {
3799
+ if (resolveMode(void 0, params.account) !== "off") return true;
3800
+ return Object.values(params.channels ?? {}).some((entry) => resolveMode(entry?.presenceEvents, void 0) !== "off");
3801
+ }
3802
+ function isTargetEligible(target) {
3803
+ if (target.mode === "on") return true;
3804
+ if (target.autoEligibleKind === "channel") return false;
3805
+ return target.participants.size <= SLACK_PRESENCE_AUTO_MAX_PARTICIPANTS;
3806
+ }
3807
+ function formatSlackPresenceEvent(target, userId) {
3808
+ return [
3809
+ "Slack presence event:",
3810
+ `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)}` : ""}.`,
3811
+ "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.",
3812
+ "Send at most one short, natural greeting in this Slack conversation. Do not reveal private memory. If no greeting is appropriate, stay silent."
3813
+ ].join("\n");
3814
+ }
3815
+ function resolveObservedTarget(params) {
3816
+ const { prepared } = params;
3817
+ const userId = prepared.message.user?.trim();
3818
+ if (!userId || prepared.message.bot_id || prepared.message.subtype === "bot_message") return null;
3819
+ const mode = resolveMode(prepared.channelConfig?.presenceEvents, params.accountConfig);
3820
+ if (mode === "off") return null;
3821
+ const channelId = prepared.message.channel;
3822
+ const rawThreadId = prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId ?? void 0;
3823
+ const threadId = rawThreadId === void 0 ? void 0 : String(rawThreadId);
3824
+ const channelType = prepared.message.channel_type;
3825
+ const autoEligibleKind = prepared.isDirectMessage ? "direct" : channelType === "mpim" ? "group" : threadId ? "thread" : "channel";
3826
+ if (mode === "auto" && autoEligibleKind === "channel") return null;
3447
3827
  return {
3448
- on: (event, listener) => on.call(client, event, listener),
3449
- off: (event, listener) => off.call(client, event, listener)
3828
+ key: `${channelId}${threadId ? `:thread:${threadId}` : ":top"}`,
3829
+ mode,
3830
+ channelId,
3831
+ threadId,
3832
+ to: prepared.isDirectMessage ? `user:${userId}` : `channel:${channelId}`,
3833
+ sessionKey: prepared.route.sessionKey,
3834
+ agentId: prepared.route.agentId,
3835
+ participants: /* @__PURE__ */ new Map([[userId, params.nowMs]]),
3836
+ lastActivityAtMs: params.nowMs,
3837
+ autoEligibleKind
3450
3838
  };
3451
3839
  }
3452
- function isBufferArray(value) {
3453
- return Array.isArray(value) && value.every((entry) => Buffer.isBuffer(entry));
3454
- }
3455
- function resolveSlackSocketModeConnectionCount(message) {
3456
- const buffer = Buffer.isBuffer(message) ? message : message instanceof ArrayBuffer ? Buffer.from(message) : isBufferArray(message) ? Buffer.concat(message) : void 0;
3457
- if (!buffer?.includes(SLACK_SOCKET_HELLO_MARKER)) return;
3458
- let payload;
3459
- try {
3460
- payload = JSON.parse(buffer.toString("utf8"));
3461
- } catch {
3462
- return;
3463
- }
3464
- const count = isRecord(payload) && payload.type === "hello" ? payload.num_connections : void 0;
3465
- return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
3466
- }
3467
- function formatSlackSocketModeSharedConnectionWarning(activeConnections) {
3468
- return [
3469
- `slack socket mode reports ${activeConnections} active connections for this Slack app`,
3470
- "Slack may deliver each event to any one connection",
3471
- "ensure every OpenClaw gateway sharing this app has equivalent routing and authorization, or use a separate Slack app per gateway, one relay ingress, or HTTP Request URLs behind a load balancer",
3472
- `See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
3473
- ].join("; ");
3474
- }
3475
- function registerSlackSocketModeConnectionDiagnostics(params) {
3476
- const emitter = getSocketEmitter(params.app);
3477
- if (!emitter) return () => {};
3478
- let hasWarned = false;
3479
- const listener = (message, isBinary) => {
3480
- if (isBinary === true || hasWarned) return;
3481
- const activeConnections = resolveSlackSocketModeConnectionCount(message);
3482
- if (activeConnections === void 0 || activeConnections <= 1) return;
3483
- hasWarned = true;
3484
- params.onSharedConnection(activeConnections);
3840
+ function createSlackPresenceMonitor(params) {
3841
+ const targets = /* @__PURE__ */ new Map();
3842
+ const presenceByUser = /* @__PURE__ */ new Map();
3843
+ const nowMs = params.nowMs ?? Date.now;
3844
+ const enqueue = params.enqueue ?? enqueueSystemEvent;
3845
+ const wake = params.wake ?? requestHeartbeat;
3846
+ let pollOffset = 0;
3847
+ let timer;
3848
+ let activePoll;
3849
+ let stopped = false;
3850
+ const pruneTargets = (now) => {
3851
+ for (const [key, target] of targets) if (now - target.lastActivityAtMs >= SLACK_PRESENCE_TARGET_TTL_MS) targets.delete(key);
3852
+ while (targets.size > SLACK_PRESENCE_MAX_TARGETS) {
3853
+ const oldestKey = targets.keys().next().value;
3854
+ if (typeof oldestKey !== "string") break;
3855
+ targets.delete(oldestKey);
3856
+ }
3857
+ const eligibleUsers = new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())));
3858
+ for (const userId of presenceByUser.keys()) if (!eligibleUsers.has(userId)) presenceByUser.delete(userId);
3485
3859
  };
3486
- emitter.on("ws_message", listener);
3487
- return () => {
3488
- emitter.off("ws_message", listener);
3860
+ const observe = (prepared) => {
3861
+ const now = nowMs();
3862
+ pruneTargets(now);
3863
+ const observed = resolveObservedTarget({
3864
+ prepared,
3865
+ accountConfig: params.accountConfig,
3866
+ nowMs: now
3867
+ });
3868
+ if (!observed) return;
3869
+ const current = targets.get(observed.key);
3870
+ if (current) {
3871
+ current.mode = observed.mode;
3872
+ current.sessionKey = observed.sessionKey;
3873
+ current.agentId = observed.agentId;
3874
+ current.to = observed.to;
3875
+ current.lastActivityAtMs = now;
3876
+ for (const [participant, observedAt] of observed.participants) current.participants.set(participant, observedAt);
3877
+ targets.delete(observed.key);
3878
+ targets.set(observed.key, current);
3879
+ } else targets.set(observed.key, observed);
3880
+ pruneTargets(now);
3489
3881
  };
3490
- }
3491
- function waitForSlackSocketDisconnect(app, abortSignal) {
3492
- return new Promise((resolve) => {
3493
- const emitter = getSocketEmitter(app);
3494
- if (!emitter) {
3495
- abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
3882
+ const emitTransition = (userId, now) => {
3883
+ 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];
3884
+ if (!target) return;
3885
+ const cooldownKey = `${params.accountId}:${userId}`;
3886
+ let reserved;
3887
+ try {
3888
+ reserved = params.cooldownStore.registerIfAbsent(cooldownKey, now, { ttlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS });
3889
+ } catch (err) {
3890
+ params.error?.(`slack presence cooldown persistence failed: ${String(err)}`);
3496
3891
  return;
3497
3892
  }
3498
- const disconnectListener = () => resolveOnce({ event: "disconnect" });
3499
- const startFailListener = (error) => resolveOnce({
3500
- event: "unable_to_socket_mode_start",
3501
- error
3893
+ if (!reserved) return;
3894
+ if (!enqueue(formatSlackPresenceEvent(target, userId), {
3895
+ sessionKey: target.sessionKey,
3896
+ contextKey: `slack:presence-active:${params.accountId}:${userId}`,
3897
+ deliveryContext: {
3898
+ channel: "slack",
3899
+ to: target.to,
3900
+ accountId: params.accountId,
3901
+ threadId: target.threadId
3902
+ }
3903
+ })) {
3904
+ params.cooldownStore.delete(cooldownKey);
3905
+ return;
3906
+ }
3907
+ wake({
3908
+ source: "notifications-event",
3909
+ intent: "immediate",
3910
+ reason: "wake",
3911
+ agentId: target.agentId,
3912
+ sessionKey: target.sessionKey,
3913
+ heartbeat: {
3914
+ target: "slack",
3915
+ to: target.to,
3916
+ accountId: params.accountId
3917
+ }
3502
3918
  });
3503
- const errorListener = (error) => resolveOnce({
3504
- event: "error",
3505
- error
3919
+ };
3920
+ const performPoll = async () => {
3921
+ const now = nowMs();
3922
+ pruneTargets(now);
3923
+ const candidates = Array.from(new Set(Array.from(targets.values()).filter(isTargetEligible).flatMap((target) => Array.from(target.participants.keys())))).toSorted();
3924
+ if (candidates.length === 0) return;
3925
+ const count = Math.min(candidates.length, SLACK_PRESENCE_MAX_POLLS_PER_INTERVAL);
3926
+ const selected = Array.from({ length: count }, (_, index) => candidates[(pollOffset + index) % candidates.length]).filter((userId) => Boolean(userId));
3927
+ pollOffset = (pollOffset + count) % candidates.length;
3928
+ for (const userId of selected) {
3929
+ if (stopped) return;
3930
+ try {
3931
+ const response = await params.client.getPresence({ user: userId });
3932
+ if (stopped) return;
3933
+ const next = response.presence === "active" || response.presence === "away" ? response.presence : void 0;
3934
+ if (!next) continue;
3935
+ const previous = presenceByUser.get(userId);
3936
+ presenceByUser.set(userId, next);
3937
+ if (previous === "away" && next === "active") emitTransition(userId, now);
3938
+ } catch (err) {
3939
+ if (stopped) return;
3940
+ params.error?.(`slack presence poll failed for user ${userId}: ${String(err)}`);
3941
+ }
3942
+ }
3943
+ };
3944
+ const pollOnce = () => {
3945
+ if (stopped) return Promise.resolve();
3946
+ if (activePoll) return activePoll;
3947
+ const run = performPoll().finally(() => {
3948
+ if (activePoll === run) activePoll = void 0;
3506
3949
  });
3507
- const abortListener = () => resolveOnce({ event: "disconnect" });
3508
- const cleanup = () => {
3509
- emitter.off("disconnected", disconnectListener);
3510
- emitter.off("unable_to_socket_mode_start", startFailListener);
3511
- emitter.off("error", errorListener);
3512
- abortSignal?.removeEventListener("abort", abortListener);
3513
- };
3514
- const resolveOnce = (value) => {
3515
- cleanup();
3516
- resolve(value);
3517
- };
3518
- emitter.on("disconnected", disconnectListener);
3519
- emitter.on("unable_to_socket_mode_start", startFailListener);
3520
- emitter.on("error", errorListener);
3521
- abortSignal?.addEventListener("abort", abortListener, { once: true });
3522
- });
3523
- }
3524
- /**
3525
- * Detect permanent Slack account and credential failures.
3526
- * Transient request and HTTP failures stay in OpenClaw's reconnect loop.
3527
- */
3528
- function isNonRecoverableSlackAuthError(error) {
3529
- return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
3950
+ activePoll = run;
3951
+ return run;
3952
+ };
3953
+ return {
3954
+ observe,
3955
+ pollOnce,
3956
+ start: () => {
3957
+ if (timer) return;
3958
+ stopped = false;
3959
+ params.log?.(`slack presence polling enabled for account ${params.accountId}`);
3960
+ timer = setInterval(() => void pollOnce(), SLACK_PRESENCE_POLL_INTERVAL_MS);
3961
+ timer.unref?.();
3962
+ },
3963
+ stop: async () => {
3964
+ stopped = true;
3965
+ if (timer) {
3966
+ clearInterval(timer);
3967
+ timer = void 0;
3968
+ }
3969
+ await activePoll;
3970
+ }
3971
+ };
3530
3972
  }
3531
- function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
3532
- return formatSlackError(error, fallback);
3973
+ //#endregion
3974
+ //#region extensions/slack/src/monitor/presence-cooldown-store.ts
3975
+ const SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES = 25e3;
3976
+ function openSlackPresenceCooldownStore() {
3977
+ return getSlackRuntime().state.openSyncKeyedStore({
3978
+ namespace: "presence-greeting-cooldowns",
3979
+ maxEntries: SLACK_PRESENCE_COOLDOWN_MAX_ENTRIES,
3980
+ overflowPolicy: "reject-new",
3981
+ defaultTtlMs: SLACK_PRESENCE_GREETING_COOLDOWN_MS
3982
+ });
3533
3983
  }
3534
3984
  //#endregion
3535
3985
  //#region extensions/slack/src/monitor/provider-support.ts
@@ -3614,13 +4064,15 @@ function resolveSlackBoltInterop(params) {
3614
4064
  };
3615
4065
  throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
3616
4066
  }
3617
- function publishSlackConnectedStatus(setStatus) {
4067
+ function publishSlackConnectedStatus(setStatus, identityHealth = {
4068
+ healthState: "healthy",
4069
+ lastError: null
4070
+ }) {
3618
4071
  if (!setStatus) return;
3619
4072
  setStatus({
3620
4073
  connected: true,
3621
4074
  lastConnectedAt: Date.now(),
3622
- healthState: "healthy",
3623
- lastError: null
4075
+ ...identityHealth
3624
4076
  });
3625
4077
  }
3626
4078
  function publishSlackDisconnectedStatus(setStatus, error) {
@@ -3694,7 +4146,8 @@ function createSlackBoltApp(params) {
3694
4146
  autoReconnectEnabled: true,
3695
4147
  clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
3696
4148
  logger: socketModeLogger,
3697
- installerOptions: { clientOptions: params.clientOptions }
4149
+ installerOptions: { clientOptions: params.clientOptions },
4150
+ ...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
3698
4151
  };
3699
4152
  if (params.socketMode?.serverPingTimeout !== void 0) socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
3700
4153
  if (params.socketMode?.pingPongLoggingEnabled !== void 0) socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
@@ -3704,15 +4157,17 @@ function createSlackBoltApp(params) {
3704
4157
  installSlackNativeReconnectFailureObserver(receiver);
3705
4158
  } else if (params.slackMode === "http") receiver = new params.interop.HTTPReceiver({
3706
4159
  signingSecret: params.signingSecret ?? "",
3707
- endpoints: params.slackWebhookPath
4160
+ endpoints: params.slackWebhookPath,
4161
+ ...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
3708
4162
  });
3709
4163
  else receiver = createSlackRelayReceiver();
4164
+ const appReceiver = receiver && params.wrapReceiver ? params.wrapReceiver(receiver) : receiver;
3710
4165
  const app = new params.interop.App({
3711
- token: params.botToken,
4166
+ token: params.token,
3712
4167
  clientOptions: params.clientOptions,
3713
4168
  ignoreSelf: false,
3714
4169
  tokenVerificationEnabled: false,
3715
- ...receiver ? { receiver } : {}
4170
+ ...appReceiver ? { receiver: appReceiver } : {}
3716
4171
  });
3717
4172
  app.use(async (args) => {
3718
4173
  if (shouldSkipOpenClawSlackSelfEvent(args)) return;
@@ -3960,7 +4415,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
3960
4415
  const SLACK_HEADER_TEXT_MAX = 150;
3961
4416
  const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
3962
4417
  const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
3963
- const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-C0Otuf_T.js"));
4418
+ const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-BT-mMm-f.js"));
3964
4419
  const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
3965
4420
  const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
3966
4421
  function resolveSlackCommandMenuModelContext(params) {
@@ -4700,7 +5155,7 @@ async function getSlackBoltInterop() {
4700
5155
  }
4701
5156
  return slackBoltInterop;
4702
5157
  }
4703
- const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-B-6LFUdz.js"));
5158
+ const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-Ea_P9VcO.js"));
4704
5159
  const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
4705
5160
  const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
4706
5161
  function resolveStableSlackUserIdEntry(raw) {
@@ -4793,16 +5248,26 @@ async function monitorSlackProvider(opts = {}) {
4793
5248
  path: `channels.slack.accounts.${account.accountId}.signingSecret`
4794
5249
  });
4795
5250
  const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken);
5251
+ const userToken = account.userToken;
4796
5252
  const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken);
4797
5253
  const relayConfig = slackMode === "relay" ? resolveSlackRelayConfig({
4798
5254
  relay: account.config.relay,
4799
5255
  accountId: account.accountId
4800
5256
  }) : void 0;
4801
- if (!botToken || slackMode === "socket" && !appToken) {
4802
- const missing = slackMode === "http" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : slackMode === "relay" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : `Slack bot + app tokens missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken/appToken or SLACK_BOT_TOKEN/SLACK_APP_TOKEN for default).`;
4803
- throw new Error(missing);
5257
+ let token;
5258
+ if (account.identity === "user") {
5259
+ if (!userToken) throw new Error(`Slack user token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.userToken or SLACK_USER_TOKEN for default).`);
5260
+ if (slackMode === "socket" && !appToken) throw new Error(`Slack app token missing for user-identity socket mode account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.appToken or SLACK_APP_TOKEN for default).`);
5261
+ if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for user-identity HTTP mode account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
5262
+ token = userToken;
5263
+ } else {
5264
+ if (!botToken || slackMode === "socket" && !appToken) {
5265
+ const missing = slackMode === "http" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : slackMode === "relay" ? `Slack bot token missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken or SLACK_BOT_TOKEN for default).` : `Slack bot + app tokens missing for account "${account.accountId}" (set channels.slack.accounts.${account.accountId}.botToken/appToken or SLACK_BOT_TOKEN/SLACK_APP_TOKEN for default).`;
5266
+ throw new Error(missing);
5267
+ }
5268
+ if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
5269
+ token = botToken;
4804
5270
  }
4805
- if (slackMode === "http" && !signingSecret) throw new Error(`Slack signing secret missing for account "${account.accountId}" (set channels.slack.signingSecret or channels.slack.accounts.${account.accountId}.signingSecret).`);
4806
5271
  const slackCfg = account.config;
4807
5272
  const dmConfig = slackCfg.dm;
4808
5273
  const dmEnabled = dmConfig?.enabled ?? true;
@@ -4842,7 +5307,6 @@ async function monitorSlackProvider(opts = {}) {
4842
5307
  const replyToMode = slackCfg.replyToMode ?? "off";
4843
5308
  const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
4844
5309
  const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
4845
- const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
4846
5310
  const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
4847
5311
  const allowNameMatching = isDangerousNameMatchingEnabled(slackCfg);
4848
5312
  const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
@@ -4851,15 +5315,21 @@ async function monitorSlackProvider(opts = {}) {
4851
5315
  const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024;
4852
5316
  const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
4853
5317
  const clientOptions = resolveSlackWebClientOptions();
5318
+ const durableIngress = createSlackDurableIngress({
5319
+ accountId: account.accountId,
5320
+ ...runtime.log ? { onLog: runtime.log } : {},
5321
+ ...opts.abortSignal ? { abortSignal: opts.abortSignal } : {}
5322
+ });
4854
5323
  const { app, receiver, socketModeLogger } = createSlackBoltApp({
4855
5324
  interop: await getSlackBoltInterop(),
4856
5325
  slackMode,
4857
- botToken,
5326
+ token,
4858
5327
  appToken: slackMode === "socket" ? appToken ?? void 0 : void 0,
4859
5328
  signingSecret: slackMode === "http" ? signingSecret ?? void 0 : void 0,
4860
5329
  slackWebhookPath,
4861
5330
  clientOptions,
4862
- ...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}
5331
+ ...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {},
5332
+ wrapReceiver: durableIngress.wrapReceiver
4863
5333
  });
4864
5334
  const gracefulStop = async () => {
4865
5335
  await gracefulStopSlackApp(app);
@@ -4890,43 +5360,50 @@ async function monitorSlackProvider(opts = {}) {
4890
5360
  let botUserId = "";
4891
5361
  let botId = "";
4892
5362
  const expectedApiAppIdFromAppToken = slackMode === "socket" ? parseApiAppIdFromAppToken(appToken) : void 0;
4893
- let authTestFailed = false;
4894
5363
  let authTestError;
4895
5364
  let authIdentityWarning;
4896
5365
  let authTestIdentity;
4897
5366
  try {
4898
- const auth = await app.client.auth.test();
5367
+ const auth = await createSlackStartupAuthClient(token, clientOptions).auth.test();
4899
5368
  const authUserId = normalizeOptionalString(auth.user_id) ?? "";
4900
5369
  botId = normalizeOptionalString(auth.bot_id) ?? "";
4901
- botUserId = botId ? authUserId : "";
5370
+ botUserId = account.identity === "user" ? authUserId : botId ? authUserId : "";
4902
5371
  authTestIdentity = auth;
4903
- authIdentityWarning = formatSlackBotTokenIdentityWarning({
5372
+ if (account.identity === "bot") authIdentityWarning = formatSlackBotTokenIdentityWarning({
4904
5373
  auth,
4905
5374
  accountId: account.accountId
4906
5375
  });
4907
- if (!authUserId && !enterpriseOrgInstall) {
4908
- authTestFailed = true;
4909
- authTestError = "auth.test returned no user_id";
4910
- }
5376
+ if (!authUserId && !enterpriseOrgInstall) authTestError = "auth.test returned no user_id";
4911
5377
  } catch (err) {
4912
- authTestFailed = true;
4913
5378
  authTestError = err instanceof Error ? err.message : String(err);
4914
5379
  }
4915
5380
  const installationIdentity = resolveSlackInstallationIdentity({
4916
5381
  enterpriseOrgInstall,
4917
- auth: authTestFailed ? void 0 : authTestIdentity,
5382
+ auth: authTestError === void 0 ? authTestIdentity : void 0,
4918
5383
  authError: authTestError,
4919
5384
  transportApiAppId: expectedApiAppIdFromAppToken
4920
5385
  });
4921
5386
  const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
4922
5387
  const apiAppId = installationIdentity.kind === "degraded" ? "" : installationIdentity.apiAppId ?? "";
4923
- 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`));
5388
+ if (authTestError !== void 0) {
5389
+ const identityFailureDetail = account.identity === "user" ? "explicit self-mention detection will be disabled until restart with a valid user token" : "explicit bot-mention detection will be disabled until restart with a valid bot token";
5390
+ runtime.log?.(warn(`[${account.accountId}] slack auth.test failed at boot (${authTestError}); ${identityFailureDetail}; required-mention channels will fail closed without another trusted activation signal`));
5391
+ }
4924
5392
  if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
4925
- if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
5393
+ const identityHealth = resolveSlackIdentityHealth({
5394
+ installationIdentity,
5395
+ botUserId,
5396
+ authTestError,
5397
+ authIdentityWarning
5398
+ });
5399
+ if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) {
5400
+ const identityTokenLabel = account.identity === "user" ? "user token" : "bot token";
5401
+ runtime.error?.(`slack token mismatch: ${identityTokenLabel} app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
5402
+ }
4926
5403
  const ctx = createSlackMonitorContext({
4927
5404
  cfg,
4928
5405
  accountId: account.accountId,
4929
- botToken,
5406
+ botToken: token,
4930
5407
  app,
4931
5408
  runtime,
4932
5409
  channelRuntime: opts.channelRuntime,
@@ -4954,7 +5431,6 @@ async function monitorSlackProvider(opts = {}) {
4954
5431
  replyToMode,
4955
5432
  threadHistoryScope,
4956
5433
  threadInheritParent,
4957
- threadRequireExplicitMention,
4958
5434
  slashCommand,
4959
5435
  textLimit,
4960
5436
  ackReactionScope,
@@ -4968,10 +5444,24 @@ async function monitorSlackProvider(opts = {}) {
4968
5444
  lastInboundAt: Date.now()
4969
5445
  });
4970
5446
  } : void 0;
5447
+ const presenceEventsEnabled = hasSlackPresenceEventsEnabled({
5448
+ account: slackCfg.presenceEvents,
5449
+ channels: slackCfg.channels
5450
+ });
5451
+ const presenceMonitor = installationIdentity.kind !== "enterprise" && presenceEventsEnabled ? createSlackPresenceMonitor({
5452
+ accountId: account.accountId,
5453
+ accountConfig: slackCfg.presenceEvents,
5454
+ client: app.client.users,
5455
+ cooldownStore: openSlackPresenceCooldownStore(),
5456
+ log: runtime.log,
5457
+ error: runtime.error
5458
+ }) : void 0;
5459
+ if (installationIdentity.kind === "enterprise" && presenceEventsEnabled) runtime.log?.(warn("slack presence events are unavailable for Enterprise Grid org installs"));
4971
5460
  const handleSlackMessage = createSlackMessageHandler({
4972
5461
  ctx,
4973
5462
  account,
4974
- trackEvent
5463
+ trackEvent,
5464
+ onPrepared: presenceMonitor?.observe
4975
5465
  });
4976
5466
  if (installationIdentity.kind !== "enterprise" && isSlackAnyNativeApprovalClientEnabled({
4977
5467
  cfg,
@@ -4999,6 +5489,8 @@ async function monitorSlackProvider(opts = {}) {
4999
5489
  appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
5000
5490
  trackEvent
5001
5491
  });
5492
+ durableIngress.start();
5493
+ presenceMonitor?.start();
5002
5494
  if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
5003
5495
  path: slackWebhookPath,
5004
5496
  handler: slackHttpHandler,
@@ -5113,10 +5605,10 @@ async function monitorSlackProvider(opts = {}) {
5113
5605
  abortSignal: opts.abortSignal,
5114
5606
  onStarted: () => {
5115
5607
  reconnectAttempts = 0;
5116
- publishSlackConnectedStatus(opts.setStatus);
5608
+ publishSlackConnectedStatus(opts.setStatus, identityHealth);
5117
5609
  if (!hasLoggedSocketConnected) {
5118
5610
  hasLoggedSocketConnected = true;
5119
- runtime.log?.("slack socket mode connected");
5611
+ runtime.log?.(identityHealth.healthState === "degraded" ? "slack socket mode connected (degraded identity)" : "slack socket mode connected");
5120
5612
  }
5121
5613
  }
5122
5614
  });
@@ -5163,9 +5655,17 @@ async function monitorSlackProvider(opts = {}) {
5163
5655
  }
5164
5656
  } else if (slackMode === "relay" && relayConfig) {
5165
5657
  runtime.log?.(`slack relay mode connecting to ${relayConfig.url} gateway_id:${relayConfig.gatewayId}`);
5658
+ durableIngress.attachRelayDispatch(async (message, turnAdoptionLifecycle) => {
5659
+ await handleSlackMessage(message, {
5660
+ source: "message",
5661
+ wasMentioned: true,
5662
+ awaitDispatch: true,
5663
+ turnAdoptionLifecycle
5664
+ });
5665
+ });
5166
5666
  await (await loadSlackRelaySource()).monitorSlackRelaySource({
5167
5667
  config: relayConfig,
5168
- handleSlackMessage,
5668
+ acceptRelayEvent: durableIngress.acceptRelayEvent,
5169
5669
  runtime,
5170
5670
  abortSignal: opts.abortSignal,
5171
5671
  setStatus: opts.setStatus,
@@ -5178,13 +5678,15 @@ async function monitorSlackProvider(opts = {}) {
5178
5678
  });
5179
5679
  }
5180
5680
  } finally {
5681
+ await presenceMonitor?.stop();
5181
5682
  if (slackMode === "relay") setSlackDefaultSendIdentity(account.accountId, void 0);
5182
5683
  opts.abortSignal?.removeEventListener("abort", stopOnAbort);
5183
5684
  unregisterSocketModeConnectionDiagnostics();
5184
5685
  unregisterHttpHandler?.();
5686
+ await durableIngress.stop();
5185
5687
  await gracefulStop();
5186
5688
  }
5187
5689
  }
5188
5690
  const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
5189
5691
  //#endregion
5190
- 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 };
5692
+ export { stripSlackMentionsForCommandDetection as C, buildSlackSlashCommandMatcher as S, readSessionUpdatedAt as _, createSlackResponseUrlBudget as a, resolveStorePath$1 as b, formatUnknownError as c, resolveSlackCommandIngress as d, resolveSlackEffectiveAllowFrom as f, resolveSlackChatType as g, normalizeSlackChannelType as h, SlackResponseAlreadyReportedError as i, resolveConversationLabel$1 as l, parseSlackAssistantThreadMetadata as m, resolveSlackRuntimeGroupPolicy as n, authorizeSlackDirectMessage as o, buildSlackAssistantThreadMetadata as p, resolveSlackRoomContextHints as r, SLACK_SOCKET_RECONNECT_POLICY as s, monitorSlackProvider as t, authorizeSlackBotRoomMessage as u, resolveChannelContextVisibilityMode as v, updateLastRoute as x, resolveChannelResetConfig as y };