@openclaw/slack 2026.7.2-beta.2 → 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 (57) 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-Cee2ih0s.js → action-runtime-DqkRwO3Z.js} +10 -16
  7. package/dist/action-runtime.runtime-DEWOcFM4.js +2 -0
  8. package/dist/{actions-BZiuSTIq.js → actions-CF0tUY5q.js} +4 -4
  9. package/dist/{actions.runtime-DoD0-gxw.js → actions.runtime-hCbx4RXg.js} +1 -1
  10. package/dist/api.js +13 -14
  11. package/dist/{approval-handler.runtime-CUPtIM4k.js → approval-handler.runtime-Em8I1-8-.js} +2 -2
  12. package/dist/{channel-DaIiGUOp.js → channel-4-TRi1D6.js} +72 -40
  13. package/dist/channel-config-api.js +1 -1
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/{channel-type-Cfm6_6n1.js → channel-type-Cu1yjNg2.js} +3 -3
  16. package/dist/{channel.setup-C-mLz50O.js → channel.setup-CgkADtZg.js} +6 -4
  17. package/dist/{config-schema-xPSnK2VK.js → config-schema-XjJGfwQC.js} +6 -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-D_yZhjgp.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-85UWiriS.js → group-policy-Bw-65m3V.js} +67 -121
  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-CalGciTs.js → monitor-CNFeF30h.js} +3 -3
  29. package/dist/{outbound-adapter-BiLKcnyH.js → outbound-adapter-CVi2uok0.js} +49 -5
  30. package/dist/{pipeline.runtime-DofAvTtb.js → pipeline.runtime-CTvokbty.js} +391 -279
  31. package/dist/{policy-BVs4LiSa.js → policy-b_dOPDCq.js} +1 -1
  32. package/dist/probe-Borhn9PQ.js +340 -0
  33. package/dist/{provider-CmeoUiIM.js → provider-B-HVL5ZE.js} +573 -306
  34. package/dist/{relay-source-DMmgtvg4.js → relay-source-Ea_P9VcO.js} +6 -11
  35. package/dist/{replies-CJJCp6qz.js → replies-B4bOdrYb.js} +3 -3
  36. package/dist/{resolve-channels-Rfts6POK.js → resolve-channels-KTc568SU.js} +1 -1
  37. package/dist/{resolve-users-y6wAxR8E.js → resolve-users-DeGJf1yp.js} +1 -1
  38. package/dist/{runtime-api-BGDyOt1D.js → runtime-api-BFfNpzDe.js} +1 -1
  39. package/dist/runtime-api.js +11 -11
  40. package/dist/{scopes-CuxNQZ6v.js → scopes-lx08L56K.js} +1 -2
  41. package/dist/{secret-contract-CMohXFWS.js → secret-contract-DxmvbxBF.js} +1 -0
  42. package/dist/secret-contract-api.js +1 -1
  43. package/dist/{send-DYp8ca-l.js → send-KuTDky7i.js} +28 -11
  44. package/dist/send.runtime-CXyxws1D.js +2 -0
  45. package/dist/{setup-core-CkwVLzFO.js → setup-core-CeiAVwQ3.js} +218 -53
  46. package/dist/setup-plugin-api.js +1 -1
  47. package/dist/{setup-surface-oSd3NX9J.js → setup-surface-DLA0vHXp.js} +23 -13
  48. package/dist/{shared-D9yz5Otg.js → shared-C4yVB0Q-.js} +55 -9
  49. package/dist/{slash-dispatch.runtime-CSja9VuD.js → slash-dispatch.runtime-BT-mMm-f.js} +1 -1
  50. package/npm-shrinkwrap.json +3 -3
  51. package/openclaw.plugin.json +66 -10
  52. package/package.json +4 -4
  53. package/dist/accounts.runtime-KvftRRek.js +0 -2
  54. package/dist/action-runtime.runtime-DZfd7IFr.js +0 -2
  55. package/dist/client-WIPAS3Yr.js +0 -160
  56. package/dist/probe-CN0ssfLN.js +0 -48
  57. package/dist/send.runtime-CJLxAt26.js +0 -2
@@ -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 { 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";
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
  /**
@@ -150,10 +151,6 @@ function createSlackMonitorContext(params) {
150
151
  const logger = getChildLogger({ module: "slack-auto-reply" });
151
152
  const channelCache = /* @__PURE__ */ new Map();
152
153
  const userCache = /* @__PURE__ */ new Map();
153
- const seenMessages = createDedupeCache({
154
- ttlMs: 6e4,
155
- maxSize: 500
156
- });
157
154
  const channelDenialWarnings = createDedupeCache({
158
155
  ttlMs: SLACK_CHANNEL_DENIAL_WARNING_TTL_MS,
159
156
  maxSize: SLACK_CHANNEL_DENIAL_WARNING_MAX_ENTRIES
@@ -187,14 +184,6 @@ function createSlackMonitorContext(params) {
187
184
  const id = normalizeOptionalString(channelId);
188
185
  return id ? readLruMapEntry(channelCache, scopedKey(id, eventScope))?.info.type : void 0;
189
186
  };
190
- const markMessageSeen = (channelId, ts, eventScope) => {
191
- if (!channelId || !ts) return false;
192
- return seenMessages.check(scopedKey(`${channelId}:${ts}`, eventScope));
193
- };
194
- const releaseSeenMessage = (channelId, ts, eventScope) => {
195
- if (!channelId || !ts) return;
196
- seenMessages.delete(scopedKey(`${channelId}:${ts}`, eventScope));
197
- };
198
187
  const assistantContextKey = (channelId, threadTs, eventScope) => scopedKey(`${channelId}:${threadTs}`, eventScope);
199
188
  const cleanupAssistantThreadContexts = () => {
200
189
  const now = Date.now();
@@ -472,7 +461,6 @@ function createSlackMonitorContext(params) {
472
461
  replyToMode: params.replyToMode,
473
462
  threadHistoryScope: params.threadHistoryScope,
474
463
  threadInheritParent: params.threadInheritParent,
475
- threadRequireExplicitMention: params.threadRequireExplicitMention,
476
464
  slashCommand: params.slashCommand,
477
465
  textLimit: params.textLimit,
478
466
  ackReactionScope: params.ackReactionScope,
@@ -480,8 +468,6 @@ function createSlackMonitorContext(params) {
480
468
  mediaMaxBytes: params.mediaMaxBytes,
481
469
  removeAckAfterReply: params.removeAckAfterReply,
482
470
  logger,
483
- markMessageSeen,
484
- releaseSeenMessage,
485
471
  shouldDropMismatchedSlackEvent,
486
472
  resolveSlackSystemEventSessionKey,
487
473
  isChannelAllowed,
@@ -1927,6 +1913,26 @@ async function handleSlackBlockAction(params) {
1927
1913
  });
1928
1914
  return;
1929
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
+ }
1930
1936
  const pluginInteractionData = buildSlackPluginInteractionData({
1931
1937
  actionId: parsed.actionId,
1932
1938
  summary: parsed.actionSummary
@@ -2649,6 +2655,350 @@ function resolveSlackEventScope(params) {
2649
2655
  };
2650
2656
  }
2651
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
2652
3002
  //#region extensions/slack/src/monitor/events/message-subtype-handlers.ts
2653
3003
  const SUBTYPE_HANDLER_REGISTRY = {
2654
3004
  message_changed: {
@@ -2776,6 +3126,7 @@ function registerSlackMessageEvents(params) {
2776
3126
  return resolved.scope;
2777
3127
  };
2778
3128
  const handleIncomingMessageEvent = async ({ event, body, context, client }) => {
3129
+ const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
2779
3130
  try {
2780
3131
  const eventScope = resolveEventScope({
2781
3132
  body,
@@ -2801,10 +3152,9 @@ function registerSlackMessageEvents(params) {
2801
3152
  if (assistantChangedInbound) {
2802
3153
  await handleSlackMessage(assistantChangedInbound, {
2803
3154
  source: "message",
2804
- ...eventScope ? {
2805
- eventScope,
2806
- awaitDispatch: true
2807
- } : {}
3155
+ ...eventScope ? { eventScope } : {},
3156
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3157
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2808
3158
  });
2809
3159
  return;
2810
3160
  }
@@ -2832,12 +3182,12 @@ function registerSlackMessageEvents(params) {
2832
3182
  }
2833
3183
  await handleSlackMessage(message, {
2834
3184
  source: "message",
2835
- ...eventScope ? {
2836
- eventScope,
2837
- awaitDispatch: true
2838
- } : {}
3185
+ ...eventScope ? { eventScope } : {},
3186
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3187
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2839
3188
  });
2840
3189
  } catch (err) {
3190
+ if (turnAdoptionLifecycle) throw err;
2841
3191
  ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
2842
3192
  }
2843
3193
  };
@@ -2846,6 +3196,7 @@ function registerSlackMessageEvents(params) {
2846
3196
  });
2847
3197
  ctx.app.event("app_mention", async (args) => {
2848
3198
  const { event, body, context, client } = args;
3199
+ const turnAdoptionLifecycle = resolveSlackIngressTurnLifecycle(context);
2849
3200
  try {
2850
3201
  const eventScope = resolveEventScope({
2851
3202
  body,
@@ -2872,12 +3223,12 @@ function registerSlackMessageEvents(params) {
2872
3223
  await handleSlackMessage(mention, {
2873
3224
  source: "app_mention",
2874
3225
  wasMentioned: true,
2875
- ...eventScope ? {
2876
- eventScope,
2877
- awaitDispatch: true
2878
- } : {}
3226
+ ...eventScope ? { eventScope } : {},
3227
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
3228
+ ...eventScope || turnAdoptionLifecycle ? { awaitDispatch: true } : {}
2879
3229
  });
2880
3230
  } catch (err) {
3231
+ if (turnAdoptionLifecycle) throw err;
2881
3232
  ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
2882
3233
  }
2883
3234
  });
@@ -3036,41 +3387,58 @@ function registerSlackMonitorEvents(params) {
3036
3387
  });
3037
3388
  }
3038
3389
  //#endregion
3039
- //#region extensions/slack/src/monitor/inbound-delivery-state.ts
3040
- const deliveredMessages = createPersistentDedupeCache({
3041
- globalKey: Symbol.for("openclaw.slackInboundDeliveries"),
3042
- ttlMs: 1440 * 60 * 1e3,
3043
- maxSize: 2e4,
3044
- persistent: {
3045
- namespace: "slack.inbound-deliveries",
3046
- maxEntries: 2e4,
3047
- openStore: (options) => getOptionalSlackRuntime()?.state.openKeyedStore(options),
3048
- logError: (error) => {
3049
- try {
3050
- getOptionalSlackRuntime()?.logging.getChildLogger({
3051
- plugin: "slack",
3052
- feature: "inbound-delivery-state"
3053
- }).warn("Slack persistent inbound delivery state failed", { error: String(error) });
3054
- } 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" };
3055
3440
  }
3056
3441
  }
3057
- });
3058
- function makeKey(accountId, channelId, ts, teamId) {
3059
- return `${accountId}:${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
3060
- }
3061
- async function hasSlackInboundMessageDelivery(params) {
3062
- if (!params.accountId || !params.channelId || !params.ts) return false;
3063
- return await deliveredMessages.lookup(makeKey(params.accountId, params.channelId, params.ts, params.teamId));
3064
- }
3065
- async function recordSlackInboundMessageDeliveries(params) {
3066
- if (!params.accountId || params.messages.length === 0) return;
3067
- const deliveredAt = Date.now();
3068
- const keys = /* @__PURE__ */ new Set();
3069
- for (const message of params.messages) {
3070
- if (!message.channel || !message.ts) continue;
3071
- keys.add(makeKey(params.accountId, message.channel, message.ts, params.teamId));
3072
- }
3073
- await Promise.all(Array.from(keys, (key) => deliveredMessages.register(key, { deliveredAt }, { at: deliveredAt })));
3074
3442
  }
3075
3443
  //#endregion
3076
3444
  //#region extensions/slack/src/monitor/message-handler/debounce-key.ts
@@ -3198,7 +3566,7 @@ function createSlackThreadTsResolver(params) {
3198
3566
  }
3199
3567
  //#endregion
3200
3568
  //#region extensions/slack/src/monitor/message-handler.ts
3201
- const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-DofAvTtb.js"));
3569
+ const loadSlackMessagePipeline = createLazyRuntimeModule(() => import("./pipeline.runtime-CTvokbty.js"));
3202
3570
  function createSlackDispatchCompletion() {
3203
3571
  let resolve;
3204
3572
  let reject;
@@ -3211,7 +3579,6 @@ function createSlackDispatchCompletion() {
3211
3579
  reject
3212
3580
  };
3213
3581
  }
3214
- const APP_MENTION_RETRY_TTL_MS = 6e4;
3215
3582
  const RETRYABLE_FLUSH_MAX_ATTEMPTS = 3;
3216
3583
  const RETRYABLE_FLUSH_RETRY_DELAY_MS = 1e3;
3217
3584
  const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
@@ -3225,12 +3592,9 @@ function shouldDebounceSlackMessage(message, cfg) {
3225
3592
  hasMedia: Boolean(message.files && message.files.length > 0)
3226
3593
  });
3227
3594
  }
3228
- function buildSeenMessageKey(channelId, ts, teamId) {
3229
- if (!channelId || !ts) return null;
3230
- return `${teamId ? `${teamId}:` : ""}${channelId}:${ts}`;
3231
- }
3232
3595
  function createSlackMessageHandler(params) {
3233
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)}`) });
3234
3598
  const { debounceMs, debouncer } = createChannelInboundDebouncer({
3235
3599
  cfg: ctx.cfg,
3236
3600
  channel: "slack",
@@ -3263,8 +3627,41 @@ function createSlackMessageHandler(params) {
3263
3627
  const completions = entries.map((entry) => entry.opts.dispatchCompletion).filter((completion) => completion !== void 0);
3264
3628
  try {
3265
3629
  await (async () => {
3266
- const last = entries.at(-1);
3267
- 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
+ }
3268
3665
  const teamId = last.opts.eventScope?.teamId;
3269
3666
  const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
3270
3667
  const topLevelConversationKey = buildTopLevelSlackConversationKey(last.message, ctx.accountId, teamId);
@@ -3275,76 +3672,60 @@ function createSlackMessageHandler(params) {
3275
3672
  if (pendingKeys.size === 0) pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
3276
3673
  }
3277
3674
  }
3278
- const combinedText = entries.length === 1 ? last.message.text ?? "" : entries.map((entry) => entry.message.text ?? "").filter(Boolean).join("\n");
3279
- 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));
3280
3677
  const syntheticMessage = {
3281
3678
  ...last.message,
3282
3679
  text: combinedText
3283
3680
  };
3284
- 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;
3285
3685
  try {
3286
- const { prepareSlackMessage, dispatchPreparedSlackMessage } = await loadSlackMessagePipeline();
3287
- const { dispatchCompletion: _completion, awaitDispatch: _awaitDispatch, ...lastOpts } = last.opts;
3288
- const appMentionRetryKey = seenMessageKey && lastOpts.source === "app_mention" && !ctx.botUserId ? seenMessageKey : void 0;
3289
- if (appMentionRetryKey) appMentionPreparingKeys.add(appMentionRetryKey);
3290
- const prepared = await (async () => {
3291
- try {
3292
- const result = await prepareSlackMessage({
3293
- ctx,
3294
- account,
3295
- message: syntheticMessage,
3296
- opts: {
3297
- ...lastOpts,
3298
- wasMentioned: combinedMentioned || last.opts.wasMentioned,
3299
- ...seenMessageKey && lastOpts.source === "message" ? { shouldRecordDroppedHistory: () => !appMentionPreparingKeys.has(seenMessageKey) && !appMentionDispatchedKeys.has(seenMessageKey) } : {}
3300
- }
3301
- });
3302
- if (result && seenMessageKey) {
3303
- pruneAppMentionRetryKeys(Date.now());
3304
- if (last.opts.source === "app_mention") rememberExpiringAppMentionKey(appMentionDispatchedKeys, seenMessageKey);
3305
- else if (last.opts.source === "message" && appMentionDispatchedKeys.has(seenMessageKey)) {
3306
- appMentionDispatchedKeys.delete(seenMessageKey);
3307
- appMentionRetryKeys.delete(seenMessageKey);
3308
- return null;
3309
- }
3310
- appMentionRetryKeys.delete(seenMessageKey);
3311
- }
3312
- return result;
3313
- } finally {
3314
- 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
3693
+ }
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();
3315
3713
  }
3316
- })();
3317
- if (!prepared) return;
3714
+ } : turnAdoptionLifecycle;
3318
3715
  onPrepared?.(prepared);
3319
- if (entries.length > 1) {
3320
- const ids = entries.map((entry) => entry.message.ts).filter(Boolean);
3716
+ if (surviving.length > 1) {
3717
+ const ids = surviving.map((entry) => entry.message.ts).filter(Boolean);
3321
3718
  if (ids.length > 0) {
3322
3719
  prepared.ctxPayload.MessageSids = ids;
3323
3720
  prepared.ctxPayload.MessageSidFirst = ids[0];
3324
3721
  prepared.ctxPayload.MessageSidLast = ids[ids.length - 1];
3325
3722
  }
3326
3723
  }
3327
- try {
3328
- await dispatchPreparedSlackMessage(prepared);
3329
- await recordSlackInboundMessageDeliveries({
3330
- accountId: ctx.accountId,
3331
- ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
3332
- messages: entries.map((entry) => entry.message)
3333
- });
3334
- } catch (error) {
3335
- if (!isRetryableSlackInboundError(error)) await recordSlackInboundMessageDeliveries({
3336
- accountId: ctx.accountId,
3337
- ...last.opts.eventScope ? { teamId: last.opts.eventScope.teamId } : {},
3338
- messages: entries.map((entry) => entry.message)
3339
- });
3340
- throw error;
3341
- }
3724
+ await dispatchPreparedSlackMessage(prepared);
3725
+ if (!turnAdoptionLifecycle) await commitClaims();
3726
+ else if (!settlementHandedOff) releaseClaims();
3342
3727
  } catch (error) {
3343
- if (isRetryableSlackInboundError(error)) for (const entry of entries) {
3344
- const entrySeenKey = buildSeenMessageKey(entry.message.channel, entry.message.ts, entry.opts.eventScope?.teamId);
3345
- if (entrySeenKey) appMentionDispatchedKeys.delete(entrySeenKey);
3346
- ctx.releaseSeenMessage(entry.message.channel, entry.message.ts, entry.opts.eventScope);
3347
- }
3728
+ releaseClaims(error);
3348
3729
  throw error;
3349
3730
  }
3350
3731
  })();
@@ -3361,51 +3742,10 @@ function createSlackMessageHandler(params) {
3361
3742
  });
3362
3743
  const threadTsResolver = createSlackThreadTsResolver({ client: ctx.app.client });
3363
3744
  const pendingTopLevelDebounceKeys = /* @__PURE__ */ new Map();
3364
- const appMentionRetryKeys = /* @__PURE__ */ new Map();
3365
- const appMentionPreparingKeys = /* @__PURE__ */ new Set();
3366
- const appMentionDispatchedKeys = /* @__PURE__ */ new Map();
3367
- const pruneAppMentionRetryKeys = (rawNow) => {
3368
- const now = asDateTimestampMs(rawNow);
3369
- if (now === void 0) {
3370
- appMentionRetryKeys.clear();
3371
- appMentionDispatchedKeys.clear();
3372
- return false;
3373
- }
3374
- for (const [key, expiresAt] of appMentionRetryKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionRetryKeys.delete(key);
3375
- for (const [key, expiresAt] of appMentionDispatchedKeys) if (asDateTimestampMs(expiresAt) === void 0 || expiresAt <= now) appMentionDispatchedKeys.delete(key);
3376
- return true;
3377
- };
3378
- const rememberExpiringAppMentionKey = (map, key) => {
3379
- const now = Date.now();
3380
- if (!pruneAppMentionRetryKeys(now)) return;
3381
- const expiresAt = resolveExpiresAtMsFromDurationMs(APP_MENTION_RETRY_TTL_MS, { nowMs: now });
3382
- if (expiresAt !== void 0) map.set(key, expiresAt);
3383
- };
3384
- const rememberAppMentionRetryKey = (key) => {
3385
- rememberExpiringAppMentionKey(appMentionRetryKeys, key);
3386
- };
3387
- const consumeAppMentionRetryKey = (key) => {
3388
- if (!pruneAppMentionRetryKeys(Date.now())) return false;
3389
- if (!appMentionRetryKeys.has(key)) return false;
3390
- appMentionRetryKeys.delete(key);
3391
- return true;
3392
- };
3393
3745
  async function enqueueSlackMessage(message, opts) {
3394
3746
  if (opts.source === "message" && message.type !== "message") return;
3395
3747
  if (opts.source === "message" && message.subtype && message.subtype !== "file_share" && message.subtype !== "bot_message" && message.subtype !== "thread_broadcast") return;
3396
3748
  ctx.rememberSlackChannelType(message.channel, message.channel_type, opts.eventScope);
3397
- const seenMessageKey = buildSeenMessageKey(message.channel, message.ts, opts.eventScope?.teamId);
3398
- if (seenMessageKey && await hasSlackInboundMessageDelivery({
3399
- accountId: ctx.accountId,
3400
- channelId: message.channel,
3401
- ts: message.ts,
3402
- ...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
3403
- })) return;
3404
- const wasSeen = seenMessageKey ? ctx.markMessageSeen(message.channel, message.ts, opts.eventScope) : false;
3405
- if (seenMessageKey && opts.source === "message" && !wasSeen) rememberAppMentionRetryKey(seenMessageKey);
3406
- if (seenMessageKey && wasSeen) {
3407
- if (opts.source !== "app_mention" || !consumeAppMentionRetryKey(seenMessageKey)) return;
3408
- }
3409
3749
  trackEvent?.();
3410
3750
  const resolvedMessage = await (opts.eventScope ? createSlackThreadTsResolver({ client: opts.eventScope.client }) : threadTsResolver).resolve({
3411
3751
  message,
@@ -3642,112 +3982,6 @@ function openSlackPresenceCooldownStore() {
3642
3982
  });
3643
3983
  }
3644
3984
  //#endregion
3645
- //#region extensions/slack/src/monitor/reconnect-policy.ts
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;
3647
- const NO_ERROR_DETAIL = "no error detail";
3648
- const SLACK_SOCKET_RECONNECT_POLICY = {
3649
- initialMs: 2e3,
3650
- maxMs: 3e4,
3651
- factor: 1.8,
3652
- jitter: .25
3653
- };
3654
- const SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL = "https://docs.slack.dev/apis/events-api/using-socket-mode#using-multiple-connections";
3655
- const SLACK_SOCKET_HELLO_MARKER = Buffer.from("\"hello\"");
3656
- function getSocketEmitter(app) {
3657
- const receiver = app.receiver;
3658
- const client = receiver && typeof receiver === "object" ? receiver.client : void 0;
3659
- if (!client || typeof client !== "object") return null;
3660
- const on = client.on;
3661
- const off = client.off;
3662
- if (typeof on !== "function" || typeof off !== "function") return null;
3663
- return {
3664
- on: (event, listener) => on.call(client, event, listener),
3665
- off: (event, listener) => off.call(client, event, listener)
3666
- };
3667
- }
3668
- function isBufferArray(value) {
3669
- return Array.isArray(value) && value.every((entry) => Buffer.isBuffer(entry));
3670
- }
3671
- function resolveSlackSocketModeConnectionCount(message) {
3672
- const buffer = Buffer.isBuffer(message) ? message : message instanceof ArrayBuffer ? Buffer.from(message) : isBufferArray(message) ? Buffer.concat(message) : void 0;
3673
- if (!buffer?.includes(SLACK_SOCKET_HELLO_MARKER)) return;
3674
- let payload;
3675
- try {
3676
- payload = JSON.parse(buffer.toString("utf8"));
3677
- } catch {
3678
- return;
3679
- }
3680
- const count = isRecord(payload) && payload.type === "hello" ? payload.num_connections : void 0;
3681
- return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
3682
- }
3683
- function formatSlackSocketModeSharedConnectionWarning(activeConnections) {
3684
- return [
3685
- `slack socket mode reports ${activeConnections} active connections for this Slack app`,
3686
- "Slack may deliver each event to any one connection",
3687
- "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",
3688
- `See ${SLACK_SOCKET_SHARED_CONNECTION_DOCS_URL}`
3689
- ].join("; ");
3690
- }
3691
- function registerSlackSocketModeConnectionDiagnostics(params) {
3692
- const emitter = getSocketEmitter(params.app);
3693
- if (!emitter) return () => {};
3694
- let hasWarned = false;
3695
- const listener = (message, isBinary) => {
3696
- if (isBinary === true || hasWarned) return;
3697
- const activeConnections = resolveSlackSocketModeConnectionCount(message);
3698
- if (activeConnections === void 0 || activeConnections <= 1) return;
3699
- hasWarned = true;
3700
- params.onSharedConnection(activeConnections);
3701
- };
3702
- emitter.on("ws_message", listener);
3703
- return () => {
3704
- emitter.off("ws_message", listener);
3705
- };
3706
- }
3707
- function waitForSlackSocketDisconnect(app, abortSignal) {
3708
- return new Promise((resolve) => {
3709
- const emitter = getSocketEmitter(app);
3710
- if (!emitter) {
3711
- abortSignal?.addEventListener("abort", () => resolve({ event: "disconnect" }), { once: true });
3712
- return;
3713
- }
3714
- const disconnectListener = () => resolveOnce({ event: "disconnect" });
3715
- const startFailListener = (error) => resolveOnce({
3716
- event: "unable_to_socket_mode_start",
3717
- error
3718
- });
3719
- const errorListener = (error) => resolveOnce({
3720
- event: "error",
3721
- error
3722
- });
3723
- const abortListener = () => resolveOnce({ event: "disconnect" });
3724
- const cleanup = () => {
3725
- emitter.off("disconnected", disconnectListener);
3726
- emitter.off("unable_to_socket_mode_start", startFailListener);
3727
- emitter.off("error", errorListener);
3728
- abortSignal?.removeEventListener("abort", abortListener);
3729
- };
3730
- const resolveOnce = (value) => {
3731
- cleanup();
3732
- resolve(value);
3733
- };
3734
- emitter.on("disconnected", disconnectListener);
3735
- emitter.on("unable_to_socket_mode_start", startFailListener);
3736
- emitter.on("error", errorListener);
3737
- abortSignal?.addEventListener("abort", abortListener, { once: true });
3738
- });
3739
- }
3740
- /**
3741
- * Detect permanent Slack account and credential failures.
3742
- * Transient request and HTTP failures stay in OpenClaw's reconnect loop.
3743
- */
3744
- function isNonRecoverableSlackAuthError(error) {
3745
- return SLACK_AUTH_ERROR_RE.test(formatUnknownError(error, ""));
3746
- }
3747
- function formatUnknownError(error, fallback = NO_ERROR_DETAIL) {
3748
- return formatSlackError(error, fallback);
3749
- }
3750
- //#endregion
3751
3985
  //#region extensions/slack/src/monitor/provider-support.ts
3752
3986
  const OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS = 15e3;
3753
3987
  const OPENCLAW_SLACK_SOCKET_START_FAILED_EVENT = "unable_to_socket_mode_start";
@@ -3912,7 +4146,8 @@ function createSlackBoltApp(params) {
3912
4146
  autoReconnectEnabled: true,
3913
4147
  clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
3914
4148
  logger: socketModeLogger,
3915
- installerOptions: { clientOptions: params.clientOptions }
4149
+ installerOptions: { clientOptions: params.clientOptions },
4150
+ ...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
3916
4151
  };
3917
4152
  if (params.socketMode?.serverPingTimeout !== void 0) socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
3918
4153
  if (params.socketMode?.pingPongLoggingEnabled !== void 0) socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
@@ -3922,15 +4157,17 @@ function createSlackBoltApp(params) {
3922
4157
  installSlackNativeReconnectFailureObserver(receiver);
3923
4158
  } else if (params.slackMode === "http") receiver = new params.interop.HTTPReceiver({
3924
4159
  signingSecret: params.signingSecret ?? "",
3925
- endpoints: params.slackWebhookPath
4160
+ endpoints: params.slackWebhookPath,
4161
+ ...params.wrapReceiver ? { processEventErrorHandler: async () => false } : {}
3926
4162
  });
3927
4163
  else receiver = createSlackRelayReceiver();
4164
+ const appReceiver = receiver && params.wrapReceiver ? params.wrapReceiver(receiver) : receiver;
3928
4165
  const app = new params.interop.App({
3929
- token: params.botToken,
4166
+ token: params.token,
3930
4167
  clientOptions: params.clientOptions,
3931
4168
  ignoreSelf: false,
3932
4169
  tokenVerificationEnabled: false,
3933
- ...receiver ? { receiver } : {}
4170
+ ...appReceiver ? { receiver: appReceiver } : {}
3934
4171
  });
3935
4172
  app.use(async (args) => {
3936
4173
  if (shouldSkipOpenClawSlackSelfEvent(args)) return;
@@ -4178,7 +4415,7 @@ const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300;
4178
4415
  const SLACK_HEADER_TEXT_MAX = 150;
4179
4416
  const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = 47;
4180
4417
  const loadSlashCommandsRuntime = createLazyRuntimeModule(() => import("./slash-commands.runtime-BTTMlmwy.js"));
4181
- const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-CSja9VuD.js"));
4418
+ const loadSlashDispatchRuntime = createLazyRuntimeModule(() => import("./slash-dispatch.runtime-BT-mMm-f.js"));
4182
4419
  const loadSlackPluginCommandsRuntime = createLazyRuntimeModule(() => import("./slash-plugin-commands.runtime-Dosfcx3X.js"));
4183
4420
  const loadSlashSkillCommandsRuntime = createLazyRuntimeModule(() => import("./slash-skill-commands.runtime-CD1J2ODY.js"));
4184
4421
  function resolveSlackCommandMenuModelContext(params) {
@@ -4918,7 +5155,7 @@ async function getSlackBoltInterop() {
4918
5155
  }
4919
5156
  return slackBoltInterop;
4920
5157
  }
4921
- const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-DMmgtvg4.js"));
5158
+ const loadSlackRelaySource = createLazyRuntimeModule(() => import("./relay-source-Ea_P9VcO.js"));
4922
5159
  const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
4923
5160
  const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 3e4;
4924
5161
  function resolveStableSlackUserIdEntry(raw) {
@@ -5011,16 +5248,26 @@ async function monitorSlackProvider(opts = {}) {
5011
5248
  path: `channels.slack.accounts.${account.accountId}.signingSecret`
5012
5249
  });
5013
5250
  const botToken = resolveSlackBotToken(opts.botToken ?? account.botToken);
5251
+ const userToken = account.userToken;
5014
5252
  const appToken = resolveSlackAppToken(opts.appToken ?? account.appToken);
5015
5253
  const relayConfig = slackMode === "relay" ? resolveSlackRelayConfig({
5016
5254
  relay: account.config.relay,
5017
5255
  accountId: account.accountId
5018
5256
  }) : void 0;
5019
- if (!botToken || slackMode === "socket" && !appToken) {
5020
- 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).`;
5021
- 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;
5022
5270
  }
5023
- 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).`);
5024
5271
  const slackCfg = account.config;
5025
5272
  const dmConfig = slackCfg.dm;
5026
5273
  const dmEnabled = dmConfig?.enabled ?? true;
@@ -5060,7 +5307,6 @@ async function monitorSlackProvider(opts = {}) {
5060
5307
  const replyToMode = slackCfg.replyToMode ?? "off";
5061
5308
  const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
5062
5309
  const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
5063
- const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
5064
5310
  const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
5065
5311
  const allowNameMatching = isDangerousNameMatchingEnabled(slackCfg);
5066
5312
  const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, { fallbackLimit: SLACK_TEXT_LIMIT });
@@ -5069,15 +5315,21 @@ async function monitorSlackProvider(opts = {}) {
5069
5315
  const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024;
5070
5316
  const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
5071
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
+ });
5072
5323
  const { app, receiver, socketModeLogger } = createSlackBoltApp({
5073
5324
  interop: await getSlackBoltInterop(),
5074
5325
  slackMode,
5075
- botToken,
5326
+ token,
5076
5327
  appToken: slackMode === "socket" ? appToken ?? void 0 : void 0,
5077
5328
  signingSecret: slackMode === "http" ? signingSecret ?? void 0 : void 0,
5078
5329
  slackWebhookPath,
5079
5330
  clientOptions,
5080
- ...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}
5331
+ ...slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {},
5332
+ wrapReceiver: durableIngress.wrapReceiver
5081
5333
  });
5082
5334
  const gracefulStop = async () => {
5083
5335
  await gracefulStopSlackApp(app);
@@ -5112,12 +5364,12 @@ async function monitorSlackProvider(opts = {}) {
5112
5364
  let authIdentityWarning;
5113
5365
  let authTestIdentity;
5114
5366
  try {
5115
- const auth = await createSlackStartupAuthClient(botToken, clientOptions).auth.test();
5367
+ const auth = await createSlackStartupAuthClient(token, clientOptions).auth.test();
5116
5368
  const authUserId = normalizeOptionalString(auth.user_id) ?? "";
5117
5369
  botId = normalizeOptionalString(auth.bot_id) ?? "";
5118
- botUserId = botId ? authUserId : "";
5370
+ botUserId = account.identity === "user" ? authUserId : botId ? authUserId : "";
5119
5371
  authTestIdentity = auth;
5120
- authIdentityWarning = formatSlackBotTokenIdentityWarning({
5372
+ if (account.identity === "bot") authIdentityWarning = formatSlackBotTokenIdentityWarning({
5121
5373
  auth,
5122
5374
  accountId: account.accountId
5123
5375
  });
@@ -5133,7 +5385,10 @@ async function monitorSlackProvider(opts = {}) {
5133
5385
  });
5134
5386
  const teamId = installationIdentity.kind === "workspace" ? installationIdentity.teamId : "";
5135
5387
  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`));
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
+ }
5137
5392
  if (authIdentityWarning) runtime.log?.(warn(authIdentityWarning));
5138
5393
  const identityHealth = resolveSlackIdentityHealth({
5139
5394
  installationIdentity,
@@ -5141,11 +5396,14 @@ async function monitorSlackProvider(opts = {}) {
5141
5396
  authTestError,
5142
5397
  authIdentityWarning
5143
5398
  });
5144
- if (apiAppId && expectedApiAppIdFromAppToken && apiAppId !== expectedApiAppIdFromAppToken) runtime.error?.(`slack token mismatch: bot token app_id=${apiAppId} but app token looks like app_id=${expectedApiAppIdFromAppToken}`);
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
+ }
5145
5403
  const ctx = createSlackMonitorContext({
5146
5404
  cfg,
5147
5405
  accountId: account.accountId,
5148
- botToken,
5406
+ botToken: token,
5149
5407
  app,
5150
5408
  runtime,
5151
5409
  channelRuntime: opts.channelRuntime,
@@ -5173,7 +5431,6 @@ async function monitorSlackProvider(opts = {}) {
5173
5431
  replyToMode,
5174
5432
  threadHistoryScope,
5175
5433
  threadInheritParent,
5176
- threadRequireExplicitMention,
5177
5434
  slashCommand,
5178
5435
  textLimit,
5179
5436
  ackReactionScope,
@@ -5232,6 +5489,7 @@ async function monitorSlackProvider(opts = {}) {
5232
5489
  appHomeSlashCommandName: commandRegistration.mode === "single" ? commandRegistration.name : void 0,
5233
5490
  trackEvent
5234
5491
  });
5492
+ durableIngress.start();
5235
5493
  presenceMonitor?.start();
5236
5494
  if (slackMode === "http" && slackHttpHandler) unregisterHttpHandler = registerSlackHttpHandler({
5237
5495
  path: slackWebhookPath,
@@ -5397,9 +5655,17 @@ async function monitorSlackProvider(opts = {}) {
5397
5655
  }
5398
5656
  } else if (slackMode === "relay" && relayConfig) {
5399
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
+ });
5400
5666
  await (await loadSlackRelaySource()).monitorSlackRelaySource({
5401
5667
  config: relayConfig,
5402
- handleSlackMessage,
5668
+ acceptRelayEvent: durableIngress.acceptRelayEvent,
5403
5669
  runtime,
5404
5670
  abortSignal: opts.abortSignal,
5405
5671
  setStatus: opts.setStatus,
@@ -5417,9 +5683,10 @@ async function monitorSlackProvider(opts = {}) {
5417
5683
  opts.abortSignal?.removeEventListener("abort", stopOnAbort);
5418
5684
  unregisterSocketModeConnectionDiagnostics();
5419
5685
  unregisterHttpHandler?.();
5686
+ await durableIngress.stop();
5420
5687
  await gracefulStop();
5421
5688
  }
5422
5689
  }
5423
5690
  const resolveSlackRuntimeGroupPolicy = resolveOpenProviderRuntimeGroupPolicy$1;
5424
5691
  //#endregion
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 };
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 };