@openclaw/slack 2026.9.1-beta.1 → 2026.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/{account-inspect-CN2hBsim.js → account-inspect-C3sePHWa.js} +3 -3
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/{action-runtime-BoSeX09X.js → action-runtime-DftQWpI4.js} +84 -66
  4. package/dist/action-runtime.runtime-rHI3UAh4.js +2 -0
  5. package/dist/{actions-XdQyjG_p.js → actions-CFkvEv1d.js} +198 -54
  6. package/dist/api.js +14 -14
  7. package/dist/{approval-handler.runtime-DxXfGX_Z.js → approval-handler.runtime-BeILKdw4.js} +47 -25
  8. package/dist/{channel-C_vnTgxo.js → channel-CmOuxbOD.js} +76 -133
  9. package/dist/channel-config-api.js +1 -1
  10. package/dist/channel-plugin-api.js +1 -1
  11. package/dist/{channel-type-j-2lalcC.js → channel-type-BWt2CRaI.js} +2 -2
  12. package/dist/{channel.setup-aal8tokM.js → channel.setup-D5z1ReaT.js} +29 -23
  13. package/dist/{config-schema-CN3XZRX5.js → config-schema-JDx_f9Zb.js} +26 -17
  14. package/dist/{cursor-pages-eX7p8Wwt.js → cursor-pages-ByFwo-Py.js} +7 -1
  15. package/dist/{directory-config-DqfQ42OV.js → directory-config-Bmw4ykSy.js} +4 -3
  16. package/dist/directory-contract-api.js +1 -1
  17. package/dist/{directory-live-lC6X_Ru-.js → directory-live-BuBPHmju.js} +3 -8
  18. package/dist/{doctor-contract-BAy7-EUQ.js → doctor-contract-BNjw9bxb.js} +5 -4
  19. package/dist/doctor-contract-api.js +1 -1
  20. package/dist/{message-tool-api-CWosHSi5.js → message-tool-api-uNEfQYGY.js} +19 -2
  21. package/dist/message-tool-api.js +1 -1
  22. package/dist/{monitor-DrNCGneD.js → monitor-DI07pfJl.js} +3 -3
  23. package/dist/{outbound-adapter-DkT2vUE9.js → outbound-adapter-B5QyTRpJ.js} +96 -91
  24. package/dist/{pipeline.runtime-CRRng_Nw.js → pipeline.runtime-BdIwZgm4.js} +596 -920
  25. package/dist/{policy-BoP6t88v.js → policy-Bh5TOqII.js} +7 -4
  26. package/dist/{probe-4_aHtVT3.js → probe-Biy--yhL.js} +251 -12
  27. package/dist/{provider-D4nDgxCh.js → provider-DoaUvAM4.js} +5621 -4944
  28. package/dist/{relay-source-CEmze11m.js → relay-source-D4uecp68.js} +6 -4
  29. package/dist/{replies-D9XMM_S0.js → replies-DENuTcPO.js} +21 -16
  30. package/dist/reply-blocks-PRkhyAN7.js +790 -0
  31. package/dist/{resolve-channels-DSamDgVs.js → resolve-channels-DQGV2VrE.js} +6 -10
  32. package/dist/{resolve-users-CIzwbaJR.js → resolve-users-DRGt_Q4I.js} +5 -4
  33. package/dist/runtime-api.js +10 -10
  34. package/dist/{scopes-BBrUzpBH.js → scopes-CZzeGdWV.js} +1 -1
  35. package/dist/{send-DtjEnlLh.js → send-DElUcgIs.js} +32 -24
  36. package/dist/send-results-eeOu_HYm.js +23 -0
  37. package/dist/{send.runtime-DQeNKa2J.js → send.runtime-C8D6cKGm.js} +1 -1
  38. package/dist/{group-policy-CU1tEGpS.js → session-status-BynwcZSO.js} +284 -1013
  39. package/dist/setup-plugin-api.js +1 -1
  40. package/dist/{setup-surface-CsBCM4rP.js → setup-surface-B1_gpGBl.js} +5 -5
  41. package/dist/{shared-D9GeeR9K.js → shared-CNQsWJT7.js} +7 -5
  42. package/dist/{slash-dispatch.runtime-Chr3YHnT.js → slash-dispatch.runtime-DhsXavSY.js} +2 -2
  43. package/dist/{target-parsing-BnMD2ZqZ.js → target-parsing-PbxGDbr9.js} +11 -6
  44. package/dist/{targets-Cx5W_n3W.js → targets-7FexXJ62.js} +1 -1
  45. package/dist/{thread-ts-DUGhaYKq.js → thread-ts-CukseRVL.js} +5 -1
  46. package/node_modules/@slack/bolt/node_modules/negotiator/lib/accept.js +54 -0
  47. package/node_modules/@slack/bolt/node_modules/negotiator/lib/charset.js +10 -34
  48. package/node_modules/@slack/bolt/node_modules/negotiator/lib/encoding.js +9 -30
  49. package/node_modules/@slack/bolt/node_modules/negotiator/lib/language.js +15 -36
  50. package/node_modules/@slack/bolt/node_modules/negotiator/lib/mediaType.js +16 -134
  51. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
  52. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/README.md +71 -0
  53. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
  54. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
  55. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
  56. package/node_modules/@slack/bolt/node_modules/negotiator/node_modules/content-type/package.json +52 -0
  57. package/node_modules/@slack/bolt/node_modules/negotiator/package.json +16 -10
  58. package/node_modules/typebox/build/format/email.mjs +1 -1
  59. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
  60. package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
  61. package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
  62. package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
  63. package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
  64. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  65. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  66. package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
  67. package/node_modules/typebox/build/system/memory/create.mjs +3 -4
  68. package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
  69. package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
  70. package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
  71. package/node_modules/typebox/build/system/memory/update.mjs +3 -2
  72. package/node_modules/typebox/package.json +1 -1
  73. package/node_modules/undici/lib/dispatcher/balanced-pool.js +4 -2
  74. package/node_modules/undici/lib/dispatcher/client-h1.js +12 -4
  75. package/node_modules/undici/lib/dispatcher/client-h2.js +70 -14
  76. package/node_modules/undici/lib/dispatcher/client.js +6 -2
  77. package/node_modules/undici/lib/handler/cache-handler.js +21 -5
  78. package/node_modules/undici/lib/handler/retry-handler.js +42 -7
  79. package/node_modules/undici/lib/interceptor/cache.js +20 -1
  80. package/node_modules/undici/lib/interceptor/decompress.js +146 -14
  81. package/node_modules/undici/lib/interceptor/dump.js +10 -23
  82. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +245 -150
  83. package/node_modules/undici/lib/web/websocket/connection.js +1 -1
  84. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
  85. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -10
  86. package/node_modules/undici/package.json +1 -1
  87. package/node_modules/undici/types/interceptors.d.ts +2 -0
  88. package/openclaw.plugin.json +17 -9
  89. package/package.json +6 -6
  90. package/skills/_vendor/slack-skills-plugin.LICENSE +21 -0
  91. package/skills/_vendor/slack-skills-plugin.json +31 -0
  92. package/skills/block-kit/SKILL.md +37 -0
  93. package/skills/block-kit/references/official-block-kit.md +257 -0
  94. package/skills/block-kit/references/official-common-patterns.md +385 -0
  95. package/skills/slack/SKILL.md +1 -1
  96. package/dist/action-runtime.runtime-Dpa16J87.js +0 -2
  97. package/dist/conversation-binding-route-DFVLUdYF.js +0 -115
  98. package/dist/slack-client-kind-hSfjXyUM.js +0 -94
  99. package/node_modules/@slack/bolt/node_modules/negotiator/HISTORY.md +0 -114
@@ -1,225 +1,46 @@
1
- import { n as resolveSlackConversationBindingRoute, r as resolveSlackReplyToMode, t as normalizeSlackRouteBindingConfig } from "./conversation-binding-route-DFVLUdYF.js";
2
- import { n as formatSlackTarget } from "./target-parsing-BnMD2ZqZ.js";
3
- import { B as SLACK_TEXT_LIMIT, G as truncateSlackText, K as truncateSlackTextByUtf8Bytes, L as SLACK_EDIT_TEXT_MAX_BYTES, N as buildSlackBlocksFallbackText, O as hasSlackNativeDataBlock, S as resolveSlackReplyRenderPlan, W as countSlackTextUtf8Bytes, Z as SLACK_SESSION_LINK_ACTION_ID, a as qualifySlackConversationId, ct as escapeSlackMrkdwn, d as normalizeSlackAllowOwnerEntry, o as qualifySlackRoutePeerId, ot as normalizeSlackOutboundText, p as resolveSlackAllowListMatch, s as resolveSlackEnterpriseMainDmSessionKey, u as normalizeAllowListLower } from "./group-policy-CU1tEGpS.js";
4
- import { r as formatSlackError } from "./probe-4_aHtVT3.js";
1
+ import { A as countSlackTextUtf8Bytes, C as buildSlackBlocksFallbackText, E as SLACK_EDIT_TEXT_MAX_BYTES, H as SLACK_SESSION_LINK_ACTION_ID, M as truncateSlackTextByUtf8Bytes, Nt as resolveSlackReplyToMode, Y as normalizeSlackOutboundText, at as escapeSlackMrkdwn, c as qualifySlackRoutePeerId, j as truncateSlackText, k as SLACK_TEXT_LIMIT, v as hasSlackNativeDataBlock } from "./session-status-BynwcZSO.js";
2
+ import { n as formatSlackTarget } from "./target-parsing-PbxGDbr9.js";
3
+ import { C as normalizeAllowListLower, E as resolveSlackAllowListMatch, o as formatSlackError, w as normalizeSlackAllowOwnerEntry } from "./probe-Biy--yhL.js";
4
+ import { o as resolveSlackReplyRenderPlan } from "./reply-blocks-PRkhyAN7.js";
5
+ import { n as resolveSlackReplyThreadTs } from "./thread-ts-CukseRVL.js";
5
6
  import { n as resolveSlackStreamingMode, t as resolveSlackNativeStreaming } from "./streaming-compat-B-e0mqM0.js";
6
- import { a as editSlackRenderedMessage, b as formatSlackFileReferenceList, d as reactSlackMessage, i as editSlackMessage, m as removeSlackReaction, n as deleteSlackMessage, v as logVerbose$1, x as buildSlackEditTextPayload, y as formatSlackFileReference } from "./actions-XdQyjG_p.js";
7
- import { a as hasSlackTableBlock, c as resolveSlackMessageText$1, o as isSlackUnfurlAttachment, s as resolveSlackBlocksText } from "./slack-client-kind-hSfjXyUM.js";
8
- import { c as hasSlackThreadFailureNotice, d as recordSlackThreadFailureNotice, f as recordSlackThreadParticipation, l as hasSlackThreadParticipation, m as buildSlackNativeDataDeliveryPlan, o as clearSlackThreadFailureNotice, r as sendMessageSlack, u as hasSlackThreadParticipationWithPersistence } from "./send-DtjEnlLh.js";
9
- import { C as resolveStorePath, E as stripSlackMentionsForCommandDetection, S as resolveChannelResetConfig, _ as resolveSlackChatType, b as readSessionUpdatedAt, c as resolveSlackThreadTargets, d as authorizeSlackBotRoomMessage, f as resolveSlackCommandIngress, g as normalizeSlackChannelType, l as trackSlackDraftMessage, o as authorizeSlackDirectMessage, p as resolveSlackEffectiveAllowFrom, r as resolveSlackRoomContextHints, s as resolveSlackThreadContext, u as resolveConversationLabel$1, v as buildSlackAssistantThreadMetadata, w as updateLastRoute, x as resolveChannelContextVisibilityMode, y as parseSlackAssistantThreadMetadata } from "./provider-D4nDgxCh.js";
10
- import { n as resolveSlackChannelConfig } from "./policy-BoP6t88v.js";
11
- import { a as resolveDeliveredSlackReplyThreadTs, c as emitSlackMessageSentHooks, i as readSlackReplyBlocks, n as deliverReplies, o as resolveSlackThreadTs, s as sanitizeSlackMonitorReplyPayload, t as createSlackReplyDeliveryPlan } from "./replies-D9XMM_S0.js";
7
+ import { D as resolveSlackMessageText$1, E as resolveSlackBlocksText, O as buildSlackEditTextPayload, S as formatSlackFileReferenceList, T as isSlackUnfurlAttachment, a as editSlackRenderedMessage, d as reactSlackMessage, i as editSlackMessage, m as removeSlackReaction, n as deleteSlackMessage, v as logVerbose$1, w as hasSlackTableBlock, x as formatSlackFileReference } from "./actions-CFkvEv1d.js";
8
+ import { c as hasSlackThreadFailureNotice, d as recordSlackThreadFailureNotice, f as recordSlackThreadParticipation, l as hasSlackThreadParticipation, m as buildSlackNativeDataDeliveryPlan, o as clearSlackThreadFailureNotice, r as sendMessageSlack, u as hasSlackThreadParticipationWithPersistence } from "./send-DElUcgIs.js";
9
+ import { A as resolveStorePath, C as resolveSlackChatType, D as readSessionUpdatedAt, E as getSessionEntry, N as stripSlackMentionsForCommandDetection, O as resolveChannelContextVisibilityMode, S as normalizeSlackChannelType, T as readSlackAssistantThreadContext, _ as SlackStreamNotDeliveredError, b as startSlackStream, d as resolveSlackRoutingContext, f as registerSlackSessionRun, g as resolveSlackEffectiveAllowFrom, h as resolveSlackCommandIngress, j as updateLastRoute, k as resolveChannelResetConfig, l as authorizeSlackDirectMessage, m as authorizeSlackBotRoomMessage, o as resolveSlackRoomContextHints, p as resolveSlackThreadTargets, r as trackSlackDraftMessage, u as resolveConversationLabel$1, v as appendSlackStream, w as buildSlackAssistantThreadMetadata, x as stopSlackStream, y as markSlackStreamFallbackDelivered } from "./provider-DoaUvAM4.js";
10
+ import { n as resolveSlackChannelConfig } from "./policy-Bh5TOqII.js";
11
+ import { a as resolveSlackThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as sanitizeSlackMonitorReplyPayload, s as emitSlackMessageSentHooks, t as createSlackReplyDeliveryPlan } from "./replies-DENuTcPO.js";
12
12
  import { asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
13
- import { resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
14
- import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createChannelProgressWorkCounter, createFinalizableDraftStreamControlsForState, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, formatPlanChecklistLines, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
13
+ import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
14
+ import { buildChannelProgressDraftLine, createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createChannelProgressWorkCounter, createDraftStreamLoop, createFinalizableDraftStreamControlsForState, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, formatPlanChecklistLines, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
15
15
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
16
16
  import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
17
17
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
18
+ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
19
+ import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, isReplyPayloadNonTerminalToolErrorWarning, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
18
20
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
19
21
  import { createHash } from "node:crypto";
20
22
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
21
23
  import { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversation-runtime";
24
+ import { resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
22
25
  import fs from "node:fs/promises";
23
26
  import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
24
27
  import { formatErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
25
- import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, isReplyPayloadNonTerminalToolErrorWarning, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
26
28
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist, shouldIncludeSupplementalContext } from "openclaw/plugin-sdk/security-runtime";
27
29
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
28
30
  import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
29
31
  import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
30
- import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
31
- import { resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
32
32
  import { asDateTimestampMs, asFiniteNumberInRange, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
33
- import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
34
- import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundTurn, formatInboundEnvelope, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, logInboundDrop, matchesMentionWithExplicit, readAgentRunTerminalOutcome, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundReplyDispatchCounts, resolveInboundSupplementalSenderAllowed, resolveUnmentionedGroupInboundPolicy, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
35
33
  import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
34
+ import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
35
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
36
+ import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundTurn, formatInboundEnvelope, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, matchesMentionWithExplicit, readAgentRunTerminalOutcome, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundReplyDispatchCounts, resolveInboundSupplementalSenderAllowed, resolveUnmentionedGroupInboundPolicy, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
36
37
  import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
37
38
  import { buildControlUiSessionPath } from "openclaw/plugin-sdk/session-discussion";
38
- import { DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
39
+ import { createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
39
40
  import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
40
41
  import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
41
42
  import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-surface";
42
43
  import { createChannelPreflightAudio, formatAudioTranscriptForAgent } from "openclaw/plugin-sdk/media-understanding-runtime";
43
- //#region extensions/slack/src/streaming.ts
44
- /**
45
- * Thrown when Slack definitively rejects a stream flush/finalize while text
46
- * remains buffered locally by the Slack SDK. Carries the pending text so the
47
- * caller can deliver it via the normal Slack reply path.
48
- */
49
- var SlackStreamNotDeliveredError = class extends Error {
50
- constructor(pendingText, slackCode) {
51
- super(`slack-stream: finalize failed with ${slackCode} before buffered text reached Slack (${pendingText.length} chars pending)`);
52
- this.name = "SlackStreamNotDeliveredError";
53
- this.pendingText = pendingText;
54
- this.slackCode = slackCode;
55
- }
56
- };
57
- /**
58
- * Start a new Slack text stream.
59
- *
60
- * Returns a {@link SlackStreamSession} that should be passed to
61
- * {@link appendSlackStream} and {@link stopSlackStream}.
62
- *
63
- * The first chunk of text can optionally be included via `text`.
64
- */
65
- async function startSlackStream(params) {
66
- const { client, channel, threadTs, text, chunks, taskDisplayMode, teamId, userId, identity } = params;
67
- const identityPayload = identity?.iconUrl ? {
68
- ...identity.username ? { username: identity.username } : {},
69
- icon_url: identity.iconUrl
70
- } : identity?.iconEmoji ? {
71
- ...identity.username ? { username: identity.username } : {},
72
- icon_emoji: identity.iconEmoji
73
- } : identity?.username ? { username: identity.username } : {};
74
- logVerbose(`slack-stream: starting stream in ${channel} thread=${threadTs}${teamId ? ` team=${teamId}` : ""}${userId ? ` user=${userId}` : ""}`);
75
- const streamer = client.chatStream({
76
- channel,
77
- thread_ts: threadTs,
78
- ...taskDisplayMode ? { task_display_mode: taskDisplayMode } : {},
79
- ...teamId ? { recipient_team_id: teamId } : {},
80
- ...userId ? { recipient_user_id: userId } : {},
81
- ...identityPayload
82
- });
83
- const session = {
84
- streamer,
85
- channel,
86
- threadTs,
87
- stopped: false,
88
- delivered: false,
89
- pendingText: ""
90
- };
91
- if (text || chunks?.length) {
92
- if (text) session.pendingText += text;
93
- try {
94
- const result = await streamer.append({
95
- ...text ? { markdown_text: text } : {},
96
- ...chunks?.length ? { chunks } : {}
97
- });
98
- if (result) {
99
- session.delivered = true;
100
- session.pendingText = "";
101
- }
102
- logVerbose(`slack-stream: appended initial payload (${text?.length ?? 0} chars, ${chunks?.length ?? 0} chunks, ${result ? "flushed" : "buffered"})`);
103
- } catch (err) {
104
- if (isBenignSlackFinalizeError(err) && session.pendingText) throw new SlackStreamNotDeliveredError(session.pendingText, extractSlackErrorCode(err) ?? "unknown");
105
- throw err;
106
- }
107
- }
108
- return session;
109
- }
110
- /**
111
- * Append markdown text to an active Slack stream.
112
- */
113
- async function appendSlackStream(params) {
114
- const { session, text, chunks } = params;
115
- if (session.stopped) {
116
- logVerbose("slack-stream: attempted to append to a stopped stream, ignoring");
117
- return;
118
- }
119
- if (!text && !chunks?.length) return;
120
- if (text) session.pendingText += text;
121
- try {
122
- const result = await session.streamer.append({
123
- ...text ? { markdown_text: text } : {},
124
- ...chunks?.length ? { chunks } : {}
125
- });
126
- if (result) {
127
- session.delivered = true;
128
- session.pendingText = "";
129
- }
130
- logVerbose(`slack-stream: appended ${text?.length ?? 0} chars, ${chunks?.length ?? 0} chunks (${result ? "flushed" : "buffered"})`);
131
- } catch (err) {
132
- if (isBenignSlackFinalizeError(err) && session.pendingText) throw new SlackStreamNotDeliveredError(session.pendingText, extractSlackErrorCode(err) ?? "unknown");
133
- throw err;
134
- }
135
- }
136
- /**
137
- * Stop (finalize) a Slack stream.
138
- *
139
- * After calling this the stream message becomes a normal Slack message.
140
- * Optionally include final chunks to append before stopping.
141
- *
142
- * If Slack's `chat.stopStream` responds with a definitive recipient/channel
143
- * rejection while text is still buffered locally, this function throws a
144
- * {@link SlackStreamNotDeliveredError} carrying that pending text so the caller
145
- * can deliver it through the normal Slack reply path. Ambiguous failures
146
- * propagate unchanged because Slack may have committed the request.
147
- *
148
- * If Slack responds with a known benign finalize error (see
149
- * {@link BENIGN_SLACK_FINALIZE_ERROR_CODES}) after prior `append` calls already
150
- * landed, the error is swallowed and the session is marked stopped - the
151
- * already-delivered text stays visible.
152
- *
153
- * Errors without buffered text propagate unchanged.
154
- *
155
- * On success, returns the finalized message's Slack `ts` (when reported) so the
156
- * caller can emit the `message_sent` hook with a populated `messageId`.
157
- */
158
- async function stopSlackStream(params) {
159
- const { session, chunks, metadata } = params;
160
- if (session.stopped) {
161
- logVerbose("slack-stream: stream already stopped, ignoring duplicate stop");
162
- return {};
163
- }
164
- session.stopped = true;
165
- logVerbose(`slack-stream: stopping stream in ${session.channel} thread=${session.threadTs}`);
166
- try {
167
- const stopResponse = await session.streamer.stop(chunks?.length || metadata ? {
168
- ...chunks?.length ? { chunks } : {},
169
- ...metadata ? { metadata } : {}
170
- } : void 0);
171
- session.delivered = true;
172
- session.pendingText = "";
173
- logVerbose("slack-stream: stream stopped");
174
- const messageId = stopResponse?.ts ?? stopResponse?.message?.ts;
175
- return messageId ? { messageId } : {};
176
- } catch (err) {
177
- const code = extractSlackErrorCode(err) ?? "unknown";
178
- const benignFinalizeError = isBenignSlackFinalizeError(err);
179
- if (session.pendingText && (benignFinalizeError || code === "missing_scope")) throw new SlackStreamNotDeliveredError(session.pendingText, code);
180
- if (benignFinalizeError) {
181
- if (session.delivered) {
182
- logVerbose(`slack-stream: finalize rejected by Slack (${code}); prior appends delivered, treating stream as stopped`);
183
- return {};
184
- }
185
- }
186
- throw err;
187
- }
188
- }
189
- /**
190
- * Slack API error codes that indicate `chat.stopStream` (or the
191
- * `chat.startStream` call the SDK issues inside `stop()` when the buffer
192
- * never flushed) cannot finalize the stream for the current recipient or
193
- * team. Either the caller falls back to a normal message (see
194
- * {@link SlackStreamNotDeliveredError}) or, if prior appends already
195
- * delivered text, the error is logged verbosely and swallowed.
196
- */
197
- const BENIGN_SLACK_FINALIZE_ERROR_CODES = /* @__PURE__ */ new Set([
198
- "user_not_found",
199
- "team_not_found",
200
- "missing_recipient_user_id",
201
- "method_not_supported_for_channel_type"
202
- ]);
203
- function isBenignSlackFinalizeError(err) {
204
- const code = extractSlackErrorCode(err);
205
- return code !== void 0 && BENIGN_SLACK_FINALIZE_ERROR_CODES.has(code);
206
- }
207
- function extractSlackErrorCode(err) {
208
- if (!err || typeof err !== "object") return;
209
- const record = err;
210
- if (record.data && typeof record.data === "object") {
211
- const inner = record.data.error;
212
- if (typeof inner === "string") return inner;
213
- }
214
- return (typeof record.message === "string" ? record.message : "").match(/An API error occurred:\s*([a-z_][a-z0-9_]*)/i)?.[1];
215
- }
216
- function markSlackStreamFallbackDelivered(session) {
217
- const nativeStreamWasStarted = session.delivered || Boolean(session.streamer.ts);
218
- session.pendingText = "";
219
- session.streamer.buffer = "";
220
- session.stopped = !nativeStreamWasStarted;
221
- }
222
- //#endregion
223
44
  //#region extensions/slack/src/monitor/message-handler/timestamp.ts
224
45
  const SLACK_TIMESTAMP_RE = /^\d+(?:\.\d+)?$/;
225
46
  const MAX_SAFE_SLACK_TIMESTAMP_SECONDS = Number.MAX_SAFE_INTEGER / 1e3;
@@ -269,7 +90,8 @@ function resolveExplicitSlackProgressTitle(entry) {
269
90
  return trimmed && trimmed.toLowerCase() !== "auto" ? trimmed : void 0;
270
91
  }
271
92
  function resolveSlackProgressStyle(entry) {
272
- return entry?.streaming?.progress?.style === "compact" ? "compact" : "card";
93
+ const progress = entry?.streaming?.progress;
94
+ return progress?.style ?? (progress?.toolProgress === false ? "compact" : "card");
273
95
  }
274
96
  function resolveSlackNativeProgressTaskCards(entry) {
275
97
  if (resolveSlackProgressStyle(entry) === "compact") return false;
@@ -479,6 +301,7 @@ function createSlackDraftStream(params) {
479
301
  };
480
302
  const { loop, update, discardPending, seal } = createFinalizableDraftStreamControlsForState({
481
303
  throttleMs,
304
+ coalesceInFlight: true,
482
305
  state: streamState,
483
306
  sendOrEditStreamMessage,
484
307
  emptyValue: "",
@@ -630,6 +453,7 @@ const SLACK_PROGRESS_CHUNK_TEXT_MAX = 256;
630
453
  const SLACK_PROGRESS_TASK_TITLE_MAX = 120;
631
454
  const SLACK_PROGRESS_PLAN_FALLBACK_TITLE = "Thinking";
632
455
  const SLACK_PROGRESS_LINE_DELTA_RE = /(?:^|\s)\+(\d+)\s+[−-](\d+)(?=\s|$)/u;
456
+ const SLACK_ATTENTION_TASK_PREFIX = "openclaw-attention-";
633
457
  function buildSessionSources(url) {
634
458
  return [{
635
459
  type: "url_source",
@@ -661,23 +485,23 @@ function compactTitle(value) {
661
485
  function compactChunkText(value) {
662
486
  return truncateSlackText(value.replace(/\s+/g, " ").trim(), SLACK_PROGRESS_CHUNK_TEXT_MAX);
663
487
  }
488
+ function renderProgressCardText(text, enclosingStyle) {
489
+ return normalizeSlackOutboundText(text, {
490
+ mentions: "escape",
491
+ enclosingStyle
492
+ });
493
+ }
664
494
  function lineDetailParts(line) {
665
495
  return [line.detail, line.status && line.status !== "completed" && !line.detail?.includes(line.status) ? line.status : void 0].map((part) => part?.trim()).filter((part) => Boolean(part));
666
496
  }
667
- function legacyLineTitle(line) {
668
- return `${line.icon ?? "•"} *${escapeSlackMrkdwn(line.label)}*`;
669
- }
670
- function isAuthoredProgressLine(line) {
671
- return line.id === "reasoning" || line.id?.startsWith("commentary:") === true;
672
- }
673
- function legacyLineDetail(line, maxChars) {
497
+ function activityLineDetail(line, maxChars) {
674
498
  const detail = lineDetailParts(line).join(" · ");
675
499
  if (detail) return escapeSlackMrkdwn(compactDetail(detail, maxChars));
676
- if (isAuthoredProgressLine(line)) return normalizeSlackOutboundText(compactDetail(line.text.replace(/^(?:🧠|💬)\s+/u, ""), maxChars));
677
- return "—";
500
+ const text = line.text.replace(/^(?:🧠|💬)\s+/u, "").trim();
501
+ return line.kind === "item" && !line.toolName && text && text !== line.label ? renderProgressCardText(compactDetail(text, maxChars)) : "—";
678
502
  }
679
503
  function lineTaskTitle(line) {
680
- const label = (line.kind === "command-output" ? line.toolName : void 0) || line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
504
+ const label = line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
681
505
  const fallback = line.text.replace(/\s+/g, " ").trim();
682
506
  if (fallback && fallback !== label) return compactTitle(lineDetailParts(line).length > 0 || line.status ? label : fallback);
683
507
  return compactTitle(label);
@@ -696,102 +520,109 @@ function lineTaskStatus(line) {
696
520
  const normalized = line.status?.replace(/\s+/g, " ").trim().toLowerCase();
697
521
  if (!normalized) return "in_progress";
698
522
  if (normalized === "complete" || normalized === "completed" || normalized === "done" || normalized === "ok" || normalized === "success" || normalized === "succeeded" || normalized === "successful" || normalized === "exit 0") return "complete";
699
- if (normalized === "error" || normalized === "failed" || normalized === "failure" || normalized.startsWith("exit ")) return normalized === "exit 0" ? "complete" : "error";
523
+ if (normalized === "error" || normalized === "failed" || normalized === "failure" || normalized.startsWith("exit ")) return "error";
700
524
  return "in_progress";
701
525
  }
702
- function slugTaskIdPart(value) {
703
- return value?.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "task";
704
- }
705
526
  function stableTaskIdPart(value, slugValue = value) {
527
+ const slug = slugValue.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
706
528
  const suffix = createHash("sha256").update(value).digest("hex").slice(0, 8);
707
- return `${slugTaskIdPart(slugValue).slice(0, 48)}_${suffix}`;
708
- }
709
- function resolveLineTaskIdentity(line) {
710
- if (line.id?.trim()) return {
711
- id: stableTaskIdPart(line.id),
712
- contentDerived: false
713
- };
714
- return {
715
- id: stableTaskIdPart([
716
- line.kind,
717
- line.toolName,
718
- line.label,
719
- line.text
720
- ].join("\0"), line.toolName ?? line.kind ?? line.label),
721
- contentDerived: true
722
- };
723
- }
724
- function buildPlanTasks(params) {
725
- if (params.plan) return params.plan.slice(-50).map((entry, index) => ({
529
+ return `${(slug || "task").slice(0, 48)}_${suffix}`;
530
+ }
531
+ function resolveLineTaskIdentity(line, contentIdOccurrences) {
532
+ if (line.id?.trim()) return stableTaskIdPart(line.id);
533
+ const id = stableTaskIdPart([
534
+ line.kind,
535
+ line.toolName,
536
+ line.label,
537
+ line.text
538
+ ].join("\0"), line.toolName ?? line.kind ?? line.label);
539
+ const occurrence = (contentIdOccurrences.get(id) ?? 0) + 1;
540
+ contentIdOccurrences.set(id, occurrence);
541
+ return `${id}_${occurrence}`;
542
+ }
543
+ function buildNativeTasks(params) {
544
+ const tasks = (params.plan ?? []).map((entry, index) => ({
726
545
  id: `plan_step_${index + 1}`,
727
546
  title: compactTitle(entry.step),
728
547
  status: entry.status === "completed" ? "complete" : entry.status
729
548
  }));
730
549
  const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS);
731
- const lines = params.lines.slice(-50);
732
- const identities = lines.map(resolveLineTaskIdentity);
733
550
  const contentIdOccurrences = /* @__PURE__ */ new Map();
734
- return lines.map((line, index) => {
735
- const identity = identities[index];
736
- let id = identity.id;
737
- if (identity.contentDerived) {
738
- const occurrence = (contentIdOccurrences.get(id) ?? 0) + 1;
739
- contentIdOccurrences.set(id, occurrence);
740
- id = `${id}_${occurrence}`;
741
- }
742
- const details = lineTaskDetails(line, maxLineChars);
743
- const output = lineTaskOutput(line);
551
+ for (const line of params.lines) {
744
552
  const task = {
745
- id,
553
+ id: resolveLineTaskIdentity(line, contentIdOccurrences),
746
554
  title: lineTaskTitle(line),
747
555
  status: lineTaskStatus(line)
748
556
  };
557
+ const details = lineTaskDetails(line, maxLineChars);
558
+ const output = lineTaskOutput(line);
749
559
  if (details) task.details = details;
750
560
  if (output) task.output = output;
751
- return task;
561
+ tasks.push(task);
562
+ }
563
+ return tasks;
564
+ }
565
+ function buildProgressAttentionTasks(lines, finalStatus) {
566
+ const contentIdOccurrences = /* @__PURE__ */ new Map();
567
+ return lines.flatMap((line) => {
568
+ const approval = line.kind === "approval";
569
+ if (approval ? line.status !== "requested" || finalStatus !== void 0 : lineTaskStatus(line) !== "error") return [];
570
+ const title = approval ? `Approval required: ${line.detail || line.label}` : [...new Set([
571
+ line.label,
572
+ line.detail,
573
+ line.status
574
+ ].filter(Boolean))].join(" — ");
575
+ const recovered = !approval && finalStatus === "complete";
576
+ return [{
577
+ id: `${SLACK_ATTENTION_TASK_PREFIX}${resolveLineTaskIdentity(line, contentIdOccurrences)}`,
578
+ title: compactTitle(recovered ? `Recovered: ${title}` : title),
579
+ status: approval ? "pending" : finalStatus ?? "error"
580
+ }];
752
581
  });
753
582
  }
754
- function resolvePlanTitle(params) {
755
- return compactChunkText(params.title?.trim() || params.label?.trim() || (params.tasks.at(-1)?.details ? `${params.tasks.at(-1)?.title} ${params.tasks.at(-1)?.details}` : params.tasks.at(-1)?.title) || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
583
+ function formatDiffStat(diffStat) {
584
+ if (!diffStat || diffStat.files === 0 && diffStat.added === 0 && diffStat.removed === 0) return;
585
+ return [
586
+ `📝 ${diffStat.files} files`,
587
+ ...diffStat.added > 0 ? [`+${diffStat.added}`] : [],
588
+ ...diffStat.removed > 0 ? [`−${diffStat.removed}`] : []
589
+ ].join(" ");
590
+ }
591
+ function formatTaskDiffOutput(diffStat) {
592
+ return diffStat && (diffStat.added > 0 || diffStat.removed > 0) ? `+${diffStat.added} −${diffStat.removed}` : void 0;
756
593
  }
757
594
  function buildSlackProgressStreamChunks(params) {
758
- const tasks = buildPlanTasks({
759
- lines: params.lines,
595
+ const approvals = params.lines.filter((line) => line.kind === "approval");
596
+ const tasks = buildNativeTasks({
597
+ lines: params.summaryRow ? [] : params.lines.filter((line) => line.kind !== "approval"),
760
598
  plan: params.plan,
761
599
  maxLineChars: params.maxLineChars
762
600
  });
763
- if (tasks.length === 0) {
764
- const title = params.title?.trim() || params.label?.trim();
765
- if (!title) return;
766
- if (!params.sessionUrl && !params.diffStat) return [{
767
- type: "plan_update",
768
- title: compactChunkText(title)
769
- }];
770
- return [{
771
- type: "plan_update",
772
- title: compactChunkText(title)
773
- }, {
774
- type: "task_update",
775
- id: "openclaw_summary",
776
- title: "Completed",
777
- status: "complete",
778
- ...formatTaskDiffOutput(params.diffStat) ? { output: formatTaskDiffOutput(params.diffStat) } : {},
779
- ...params.sessionUrl ? { sources: buildSessionSources(params.sessionUrl) } : {}
780
- }];
781
- }
782
- const title = resolvePlanTitle({
783
- label: params.label,
784
- title: params.title,
785
- tasks
601
+ const attention = buildProgressAttentionTasks(params.summaryRow ? params.lines : approvals, params.finalInProgressStatus);
602
+ const headline = params.title?.trim() || params.label?.trim();
603
+ const newest = tasks.at(-1);
604
+ const title = compactChunkText(headline || (newest?.details ? `${newest.title} ${newest.details}` : newest?.title) || (params.summaryRow ? "Working" : attention.at(-1)?.title) || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
605
+ const diffOutput = formatTaskDiffOutput(params.diffStat);
606
+ if (tasks.length === 0 && (params.summaryRow || params.sessionUrl || diffOutput)) tasks.push({
607
+ id: "openclaw_summary",
608
+ title: params.summaryRow ? compactTitle(title) : "Completed",
609
+ status: params.finalInProgressStatus ?? (params.summaryRow ? "in_progress" : "complete")
610
+ });
611
+ tasks.push(...attention);
612
+ if (params.finalInProgressStatus === "error" && !tasks.some((task) => task.status === "in_progress" || task.status === "error")) tasks.push({
613
+ id: "openclaw_attention",
614
+ title: "Failed",
615
+ status: "error"
786
616
  });
617
+ if (!headline && tasks.length === 0) return;
787
618
  const finalTaskIndex = tasks.length - 1;
788
- const diffOutput = formatTaskDiffOutput(params.diffStat);
789
619
  const taskChunks = tasks.map((task, index) => {
620
+ const recovered = params.finalInProgressStatus === "complete" && task.status === "error";
790
621
  const chunk = {
791
622
  type: "task_update",
792
623
  id: task.id,
793
- title: task.title,
794
- status: task.status === "in_progress" ? params.finalInProgressStatus ?? (params.completeInProgress ? "complete" : task.status) : task.status
624
+ title: recovered ? compactTitle(`Recovered: ${task.title}`) : task.title,
625
+ status: recovered ? "complete" : task.status === "in_progress" ? params.finalInProgressStatus ?? task.status : task.status
795
626
  };
796
627
  if (task.details) chunk.details = task.details;
797
628
  if (task.output) chunk.output = task.output;
@@ -804,22 +635,10 @@ function buildSlackProgressStreamChunks(params) {
804
635
  title
805
636
  }, ...taskChunks];
806
637
  }
807
- function formatDiffStat(diffStat) {
808
- if (!diffStat || diffStat.files === 0 && diffStat.added === 0 && diffStat.removed === 0) return;
809
- return [
810
- `📝 ${diffStat.files} files`,
811
- ...diffStat.added > 0 ? [`+${diffStat.added}`] : [],
812
- ...diffStat.removed > 0 ? [`−${diffStat.removed}`] : []
813
- ].join(" ");
814
- }
815
- function formatTaskDiffOutput(diffStat) {
816
- return diffStat && (diffStat.added > 0 || diffStat.removed > 0) ? `+${diffStat.added} −${diffStat.removed}` : void 0;
817
- }
818
- function buildActivityText(lines, maxLineChars) {
638
+ function joinRecentProgressRows(rows) {
819
639
  const rendered = [];
820
640
  let length = 0;
821
- for (const line of lines.slice(-50).toReversed()) {
822
- const row = `${legacyLineTitle(line)} — ${legacyLineDetail(line, maxLineChars)}`;
641
+ for (const row of rows.toReversed()) {
823
642
  const nextLength = length + row.length + (rendered.length > 0 ? 1 : 0);
824
643
  if (nextLength > SLACK_PROGRESS_FIELD_MAX) break;
825
644
  rendered.push(row);
@@ -827,6 +646,11 @@ function buildActivityText(lines, maxLineChars) {
827
646
  }
828
647
  return rendered.toReversed().join("\n");
829
648
  }
649
+ function buildActivityText(lines, maxLineChars) {
650
+ return joinRecentProgressRows(lines.slice(-50).map((line) => {
651
+ return `${`${line.icon ?? "•"} *${renderProgressCardText(line.label, "bold")}*`} — ${activityLineDetail(line, maxLineChars)}`;
652
+ }));
653
+ }
830
654
  function buildSlackProgressCardBlocks(params) {
831
655
  const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS);
832
656
  const planLines = formatPlanChecklistLines(params.plan ?? [], {
@@ -834,7 +658,6 @@ function buildSlackProgressCardBlocks(params) {
834
658
  maxLineChars
835
659
  });
836
660
  const narration = params.narration?.replace(/\s+/g, " ").trim();
837
- const activityText = buildActivityText(params.lines, maxLineChars);
838
661
  const diffStat = formatDiffStat(params.diffStat);
839
662
  const workingFooter = [
840
663
  ...params.toolCalls && params.toolCalls > 0 ? [`🛠️ ${params.toolCalls} tools`] : [],
@@ -842,40 +665,36 @@ function buildSlackProgressCardBlocks(params) {
842
665
  ...params.elapsedSeconds && params.elapsedSeconds > 0 ? [`⏱ ${params.elapsedSeconds}s`] : []
843
666
  ].join(" · ");
844
667
  const footer = params.state === "working" ? workingFooter : diffStat;
845
- return [
846
- {
847
- type: "section",
848
- text: field(`${params.state === "working" ? "🔄" : params.state === "success" ? "✅" : "❌"} *${escapeSlackMrkdwn(params.title.trim() || "Working")}*`)
849
- },
850
- ...narration ? [{
851
- type: "section",
852
- text: field(`_${escapeSlackMrkdwn(narration)}_`)
853
- }] : [],
854
- ...planLines.length > 0 ? [{
855
- type: "section",
856
- text: field(planLines.map((line) => escapeSlackMrkdwn(line)).join("\n"))
857
- }] : [],
858
- ...activityText ? [{
859
- type: "section",
860
- text: field(activityText)
861
- }] : [],
862
- ...footer ? [{
863
- type: "context",
864
- elements: [field(footer)]
865
- }] : [],
866
- ...params.state !== "working" && params.sessionUrl ? [{
867
- type: "actions",
868
- elements: [{
869
- type: "button",
870
- action_id: SLACK_SESSION_LINK_ACTION_ID,
871
- text: {
872
- type: "plain_text",
873
- text: "Open in OpenClaw"
874
- },
875
- url: params.sessionUrl
876
- }]
877
- }] : []
878
- ].slice(0, 50);
668
+ const icon = params.state === "working" ? "🔄" : params.state === "success" ? "✅" : "❌";
669
+ const finalStatus = params.state === "working" ? void 0 : params.state === "success" ? "complete" : "error";
670
+ const attention = buildProgressAttentionTasks(params.lines, finalStatus).map((task) => escapeSlackMrkdwn(task.title));
671
+ const blocks = [
672
+ `${icon} *${renderProgressCardText(params.title.trim() || "Working", "bold")}*`,
673
+ narration ? `_${renderProgressCardText(narration, "italic")}_` : "",
674
+ planLines.map((line) => renderProgressCardText(line)).join("\n"),
675
+ buildActivityText(params.lines.filter((line) => line.kind !== "approval" && lineTaskStatus(line) !== "error"), maxLineChars),
676
+ joinRecentProgressRows(attention)
677
+ ].filter(Boolean).map((text) => ({
678
+ type: "section",
679
+ text: field(text)
680
+ }));
681
+ if (footer) blocks.push({
682
+ type: "context",
683
+ elements: [field(footer)]
684
+ });
685
+ if (params.state !== "working" && params.sessionUrl) blocks.push({
686
+ type: "actions",
687
+ elements: [{
688
+ type: "button",
689
+ action_id: SLACK_SESSION_LINK_ACTION_ID,
690
+ text: {
691
+ type: "plain_text",
692
+ text: "Open in OpenClaw"
693
+ },
694
+ url: params.sessionUrl
695
+ }]
696
+ });
697
+ return blocks.slice(0, 50);
879
698
  }
880
699
  const EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT = { tasks: /* @__PURE__ */ new Map() };
881
700
  const SLACK_TASK_FIELD_SEPARATOR = " · ";
@@ -930,24 +749,23 @@ function reconcileSlackNativeTaskChunks(params) {
930
749
  continue;
931
750
  }
932
751
  const previousRow = params.previous.tasks.get(chunk.id);
933
- const status = chunk.status;
934
752
  const details = resolveTaskFieldDelta(previousRow?.details, chunk.details);
935
753
  const output = resolveTaskFieldDelta(previousRow?.output, chunk.output);
936
754
  const sourcesChanged = Boolean(chunk.sources) && !previousRow?.sourcesSent;
937
755
  const row = {
938
756
  title: chunk.title,
939
- status
757
+ status: chunk.status
940
758
  };
941
759
  if (details.field) row.details = details.field;
942
760
  if (output.field) row.output = output.field;
943
761
  if (sourcesChanged || previousRow?.sourcesSent) row.sourcesSent = true;
944
762
  nextTasks.set(chunk.id, row);
945
- if (!(!previousRow || previousRow.title !== chunk.title || previousRow.status !== status || Boolean(details.delta) || Boolean(output.delta) || sourcesChanged)) continue;
763
+ if (!(!previousRow || previousRow.title !== chunk.title || previousRow.status !== chunk.status || Boolean(details.delta) || Boolean(output.delta) || sourcesChanged)) continue;
946
764
  const update = {
947
765
  type: "task_update",
948
766
  id: chunk.id,
949
767
  title: chunk.title,
950
- status
768
+ status: chunk.status
951
769
  };
952
770
  if (details.delta) update.details = details.delta;
953
771
  if (output.delta) update.output = output.delta;
@@ -956,18 +774,21 @@ function reconcileSlackNativeTaskChunks(params) {
956
774
  }
957
775
  for (const [id, row] of params.previous.tasks) {
958
776
  if (nextTasks.has(id)) continue;
959
- if (row.status === "complete" || row.status === "error") {
777
+ const recovered = row.status === "error" && (id.startsWith(SLACK_ATTENTION_TASK_PREFIX) || params.finalStatus === "complete");
778
+ if (row.status === "complete" || row.status === "error" && !recovered) {
960
779
  nextTasks.set(id, row);
961
780
  continue;
962
781
  }
782
+ const title = recovered ? compactTitle(`Recovered: ${row.title}`) : row.title;
963
783
  nextTasks.set(id, {
964
784
  ...row,
785
+ title,
965
786
  status: "complete"
966
787
  });
967
788
  emitted.push({
968
789
  type: "task_update",
969
790
  id,
970
- title: row.title,
791
+ title,
971
792
  status: "complete"
972
793
  });
973
794
  }
@@ -979,12 +800,6 @@ function reconcileSlackNativeTaskChunks(params) {
979
800
  }
980
801
  };
981
802
  }
982
- function buildSlackProgressStreamCompletionChunks(params) {
983
- return buildSlackProgressStreamChunks({
984
- ...params,
985
- completeInProgress: true
986
- });
987
- }
988
803
  //#endregion
989
804
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress-render.ts
990
805
  function resolveStructuredProgressLines(lines) {
@@ -1001,9 +816,6 @@ function resolveStructuredProgressLines(lines) {
1001
816
  };
1002
817
  });
1003
818
  }
1004
- function resolveNativeProgressPlan(snapshot) {
1005
- return snapshot.plan?.length ? snapshot.plan : void 0;
1006
- }
1007
819
  function resolveNativeProgressLines(snapshot) {
1008
820
  const lines = resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id !== "reasoning" && line.id?.startsWith("commentary:") !== true);
1009
821
  if (snapshot.plan?.length || !snapshot.planExplanation) return lines;
@@ -1021,14 +833,6 @@ function resolveNativeProgressNarration(snapshot) {
1021
833
  function combineProgressHeadlineAndExplanation(headline, explanation) {
1022
834
  return headline && explanation && headline !== explanation ? `${headline} — ${explanation}` : headline ?? explanation;
1023
835
  }
1024
- function buildNativeProgressChunks(params) {
1025
- return buildSlackProgressStreamChunks({
1026
- title: params.title,
1027
- lines: resolveNativeProgressLines(params.snapshot),
1028
- plan: resolveNativeProgressPlan(params.snapshot),
1029
- maxLineChars: params.maxLineChars
1030
- });
1031
- }
1032
836
  //#endregion
1033
837
  //#region extensions/slack/src/monitor/message-handler/preview-finalize.ts
1034
838
  function buildExpectedSlackEditText(params) {
@@ -1066,7 +870,8 @@ function blocksMatch(expected, actual) {
1066
870
  const fallbackBlocks = fallbackPlan.fallbackMessages.flatMap((message) => message.blocks ?? []);
1067
871
  if (JSON.stringify(fallbackBlocks) === JSON.stringify(actual)) return true;
1068
872
  const fallbackText = fallbackPlan.fallbackMessages.map((message) => message.text).filter(Boolean).join("\n\n");
1069
- return normalizeSlackOutboundText(buildSlackBlocksFallbackText(actual)) === normalizeSlackOutboundText(fallbackText);
873
+ const actualText = buildSlackBlocksFallbackText(actual);
874
+ return normalizeSlackOutboundText(actualText) === normalizeSlackOutboundText(fallbackText);
1070
875
  } catch {
1071
876
  return false;
1072
877
  }
@@ -1166,18 +971,20 @@ function createSlackDraftProgressCardRuntime(params) {
1166
971
  });
1167
972
  const resolvePresentation = (snapshot, state) => {
1168
973
  const title = params.explicitTitle ?? snapshot.statusHeadline ?? "Working";
974
+ const narration = params.explicitTitle ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) : snapshot.planExplanation && snapshot.planExplanation !== title ? snapshot.planExplanation : void 0;
975
+ const workCounter = state === "working" ? params.progressWorkCounter : void 0;
976
+ const sessionUrl = state === "working" ? void 0 : resolveSessionUrl();
1169
977
  return buildSlackProgressCardBlocks({
1170
978
  state,
1171
979
  title,
1172
- narration: params.explicitTitle ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) : snapshot.planExplanation && snapshot.planExplanation !== title ? snapshot.planExplanation : void 0,
980
+ narration,
1173
981
  plan: snapshot.plan,
1174
982
  lines: resolveStructuredProgressLines(snapshot.lines),
1175
983
  maxLineChars: params.maxLineChars,
1176
984
  diffStat: snapshot.diffStat,
1177
- ...state === "working" ? {
1178
- toolCalls: params.progressWorkCounter.toolCalls,
1179
- elapsedSeconds: params.progressWorkCounter.elapsedSeconds
1180
- } : { sessionUrl: resolveSessionUrl() }
985
+ toolCalls: workCounter?.toolCalls,
986
+ elapsedSeconds: workCounter?.elapsedSeconds,
987
+ sessionUrl
1181
988
  });
1182
989
  };
1183
990
  const finalize = async (status, snapshot = params.getSnapshot(), fallbackText = resolveText(snapshot)) => {
@@ -1231,10 +1038,10 @@ function formatSlackProgressDraftLine(line) {
1231
1038
  if (/^(?:🧠|💬)\s/u.test(line)) return line;
1232
1039
  const italicCommentary = /^_(.*)_$/su.exec(line);
1233
1040
  if (!italicCommentary) return escapeSlackMrkdwn(line);
1234
- return `_${italicCommentary[1].split(/(`[^`\n]+`)/u).map((segment, index) => {
1235
- if (index % 2 === 0) return escapeSlackMrkdwn(segment);
1236
- return `\`${segment.slice(1, -1).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}\``;
1237
- }).join("")}_`;
1041
+ return `_${normalizeSlackOutboundText(italicCommentary[1], {
1042
+ mentions: "escape",
1043
+ enclosingStyle: "italic"
1044
+ })}_`;
1238
1045
  }
1239
1046
  //#endregion
1240
1047
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress-native.ts
@@ -1271,6 +1078,7 @@ function createSlackNativeProgressTransport(params) {
1271
1078
  const startPromise = (async () => {
1272
1079
  const session = await startSlackStream({
1273
1080
  client: slackClient,
1081
+ clientOptions: params.setup.slackClientOptions,
1274
1082
  channel: message.channel,
1275
1083
  threadTs: streamThreadTs,
1276
1084
  ...update.text ? { text: update.text } : {},
@@ -1341,7 +1149,7 @@ function createSlackProgressRuntime(runtimeParams) {
1341
1149
  const isProgressMode = slackStreaming.mode === "progress";
1342
1150
  const useNativeProgressStreaming = useStreaming && slackStreaming.mode === "progress";
1343
1151
  const progressDraftActive = Boolean(draftStream) || useNativeProgressStreaming;
1344
- const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, true, slackStreaming.mode);
1152
+ const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, slackStreaming.mode !== "progress", slackStreaming.mode);
1345
1153
  let shouldYieldDraftProgress = () => false;
1346
1154
  const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, {
1347
1155
  draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming,
@@ -1379,7 +1187,7 @@ function createSlackProgressRuntime(runtimeParams) {
1379
1187
  },
1380
1188
  draftStream,
1381
1189
  enabled: useDraftProgressCard,
1382
- progressWorkCounter,
1190
+ progressWorkCounter: previewToolProgressEnabled ? progressWorkCounter : void 0,
1383
1191
  progressSeed,
1384
1192
  explicitTitle: explicitProgressTitle,
1385
1193
  maxLineChars: progressDraftMaxLineChars,
@@ -1399,12 +1207,17 @@ function createSlackProgressRuntime(runtimeParams) {
1399
1207
  if (isError) nativeProgressTerminalStatus = "error";
1400
1208
  if (!session || nativeProgressCompletionSent) return;
1401
1209
  const chunks = buildNativeProgressCompletionChunks(isError ? "error" : "complete");
1402
- if (!chunks?.length) return;
1210
+ const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(progressDraft.getSnapshot()));
1211
+ if (!chunks?.length && !narrationUpdate.delta) return;
1403
1212
  try {
1404
1213
  await appendSlackStream({
1405
1214
  session,
1406
1215
  chunks
1407
1216
  });
1217
+ if (narrationUpdate.next.changed) {
1218
+ nativeNarrationRenderedText = narrationUpdate.next.rendered;
1219
+ nativeNarrationSourceText = narrationUpdate.next.source;
1220
+ }
1408
1221
  nativeProgressCompletionSent = true;
1409
1222
  delivery.observedReplyDelivery ||= session.delivered;
1410
1223
  } catch (err) {
@@ -1413,11 +1226,6 @@ function createSlackProgressRuntime(runtimeParams) {
1413
1226
  }
1414
1227
  };
1415
1228
  const resolveNativeProgressTitle = (snapshot) => combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation);
1416
- const buildNativeProgressChunks$1 = (snapshot) => buildNativeProgressChunks({
1417
- snapshot,
1418
- title: resolveNativeProgressTitle(snapshot),
1419
- maxLineChars: progressDraftMaxLineChars
1420
- });
1421
1229
  const normalizeProgressText = (text) => text?.replace(/\s+/gu, " ").trim() ?? "";
1422
1230
  const isRenderedAsProgressTitle = (text) => {
1423
1231
  const candidate = normalizeProgressText(text);
@@ -1436,17 +1244,20 @@ function createSlackProgressRuntime(runtimeParams) {
1436
1244
  delta: next.changed ? next.rendered.slice(nativeNarrationRenderedText.length) : ""
1437
1245
  };
1438
1246
  };
1439
- const updateNativeProgressStream = () => withNativeStreamOrder(updateNativeProgressStreamNow);
1440
1247
  const updateNativeProgressStreamNow = async () => {
1441
1248
  const snapshot = progressDraft.getSnapshot();
1442
- const progressLines = resolveNativeProgressLines(snapshot);
1443
1249
  const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(snapshot));
1444
- const hasRetirableNativeTasks = [...nativeStreamSnapshot.tasks.values()].some((task) => task.status !== "complete" && task.status !== "error");
1445
- if (!useNativeProgressStreaming || delivery.streamFailed || progressLines.length === 0 && !snapshot.plan?.length && !snapshot.statusHeadline && !explicitProgressTitle && !hasRetirableNativeTasks && !narrationUpdate.delta) return false;
1250
+ if (!useNativeProgressStreaming || delivery.streamFailed || nativeUpdatesStopped) return false;
1446
1251
  if (!await nativeTransport.waitForStart()) return false;
1447
1252
  const reconciled = reconcileSlackNativeTaskChunks({
1448
1253
  previous: nativeStreamSnapshot,
1449
- chunks: buildNativeProgressChunks$1(snapshot)
1254
+ chunks: buildSlackProgressStreamChunks({
1255
+ title: resolveNativeProgressTitle(snapshot),
1256
+ lines: resolveNativeProgressLines(snapshot),
1257
+ plan: snapshot.plan,
1258
+ maxLineChars: progressDraftMaxLineChars,
1259
+ summaryRow: !previewToolProgressEnabled
1260
+ })
1450
1261
  });
1451
1262
  const chunks = reconciled.chunks;
1452
1263
  if (!chunks?.length && !narrationUpdate.delta) return false;
@@ -1470,6 +1281,21 @@ function createSlackProgressRuntime(runtimeParams) {
1470
1281
  return false;
1471
1282
  }
1472
1283
  };
1284
+ let nativeUpdatesStopped = false;
1285
+ const nativeUpdates = createDraftStreamLoop({
1286
+ throttleMs: 1e3,
1287
+ coalesceInFlight: true,
1288
+ emptyValue: false,
1289
+ isEmpty: (pending) => !pending,
1290
+ isStopped: () => nativeUpdatesStopped,
1291
+ sendOrEditStreamMessage: () => withNativeStreamOrder(updateNativeProgressStreamNow),
1292
+ onBackgroundFlushError: (err) => runtime.error?.(danger(`slack-stream: progress update failed: ${formatSlackError(err)}`))
1293
+ });
1294
+ const cancelNativeUpdates = async () => {
1295
+ nativeUpdatesStopped = true;
1296
+ nativeUpdates.stop();
1297
+ await nativeUpdates.waitForInFlight();
1298
+ };
1473
1299
  const appendNativeNarration = (payload, kind) => withNativeStreamOrder(() => appendNativeNarrationNow(payload, kind));
1474
1300
  const appendNativeNarrationNow = async (payload, kind) => {
1475
1301
  if (isRenderedAsProgressTitle(payload.text)) return false;
@@ -1500,13 +1326,16 @@ function createSlackProgressRuntime(runtimeParams) {
1500
1326
  seed: progressSeed,
1501
1327
  formatLine: formatSlackProgressDraftLine,
1502
1328
  reasoningLinePrefix: "🧠 ",
1503
- commentaryLinePrefix: "",
1504
- reasoningGate: previewToolProgressEnabled,
1505
- commentaryItalics: true,
1506
- buildProgressEventLine: (input, options) => input.event === "tool" || input.event === "item" || input.event === "command-output" ? buildChannelProgressDraftLineForEntry(account.config, input, options) : buildChannelProgressDraftLine(input, options),
1507
1329
  updateOnLineChange: useNativeProgressStreaming || useDraftProgressCard,
1508
1330
  update: async (previewText, options) => {
1509
- if (useNativeProgressStreaming) return await updateNativeProgressStream();
1331
+ if (useNativeProgressStreaming) {
1332
+ const priorSnapshot = nativeStreamSnapshot;
1333
+ const priorNarration = nativeNarrationRenderedText;
1334
+ nativeUpdates.update(true);
1335
+ if (options?.flush) await nativeUpdates.flush();
1336
+ else await nativeUpdates.waitForInFlight();
1337
+ return priorSnapshot !== nativeStreamSnapshot || priorNarration !== nativeNarrationRenderedText;
1338
+ }
1510
1339
  if (!draftStream) return false;
1511
1340
  const snapshot = progressDraft.getSnapshot();
1512
1341
  progressCard.setFallbackText(previewText);
@@ -1519,7 +1348,11 @@ function createSlackProgressRuntime(runtimeParams) {
1519
1348
  }
1520
1349
  });
1521
1350
  const commentaryProgressEnabled = progressDraft.commentaryProgressEnabled;
1522
- const deliverNativeFinal = (payload, kind) => withNativeStreamOrder(() => deliverNativeFinalNow(payload, kind));
1351
+ const deliverNativeFinal = async (payload, kind) => {
1352
+ progressDraft.markFinalReplyStarted();
1353
+ await cancelNativeUpdates();
1354
+ await withNativeStreamOrder(() => deliverNativeFinalNow(payload, kind));
1355
+ };
1523
1356
  const deliverNativeFinalNow = async (payload, kind) => {
1524
1357
  progressDraft.markFinalReplyStarted();
1525
1358
  const streamReady = await nativeTransport.waitForStart();
@@ -1544,38 +1377,32 @@ function createSlackProgressRuntime(runtimeParams) {
1544
1377
  const snapshot = progressDraft.getSnapshot();
1545
1378
  const lines = resolveNativeProgressLines(snapshot);
1546
1379
  const sessionUrl = progressCard.resolveSessionUrl();
1380
+ const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(snapshot));
1547
1381
  const hasRetirableNativeTasks = [...nativeStreamSnapshot.tasks.values()].some((task) => task.status !== "complete" && task.status !== "error");
1548
- if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !snapshot.diffStat && !sessionUrl) return;
1549
- return reconcileSlackNativeTaskChunks({
1382
+ if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !snapshot.diffStat && !narrationUpdate.delta && !sessionUrl) return;
1383
+ const completion = reconcileSlackNativeTaskChunks({
1550
1384
  previous: nativeStreamSnapshot,
1551
- chunks: buildSlackProgressStreamCompletionChunks({
1385
+ finalStatus: finalInProgressStatus,
1386
+ chunks: buildSlackProgressStreamChunks({
1552
1387
  title: resolveNativeProgressTitle(snapshot) ?? (lines.length === 0 && !snapshot.plan?.length ? "Working" : void 0),
1553
1388
  lines,
1554
- plan: resolveNativeProgressPlan(snapshot),
1389
+ plan: snapshot.plan,
1555
1390
  maxLineChars: progressDraftMaxLineChars,
1391
+ summaryRow: !previewToolProgressEnabled,
1556
1392
  finalInProgressStatus,
1557
1393
  diffStat: snapshot.diffStat,
1558
1394
  sessionUrl
1559
1395
  })
1560
1396
  }).chunks;
1397
+ return narrationUpdate.delta ? [{
1398
+ type: "markdown_text",
1399
+ text: narrationUpdate.delta
1400
+ }, ...completion ?? []] : completion;
1561
1401
  };
1562
1402
  const finishNativeProgressTurn = async (completionChunks) => {
1563
1403
  if (delivery.nativeProgressStreamStartPromise) await delivery.nativeProgressStreamStartPromise.catch(() => null);
1564
- const session = delivery.streamSession;
1565
- if (session && !session.stopped) try {
1566
- if (completionChunks?.length) nativeProgressCompletionSent = true;
1567
- const stopResult = await stopSlackStream({
1568
- session,
1569
- ...completionChunks?.length ? { chunks: completionChunks } : {},
1570
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1571
- });
1572
- delivery.acknowledgeStoppedStreamedDeliveries(session, stopResult?.messageId);
1573
- } catch (err) {
1574
- const error = formatSlackError(err);
1575
- delivery.emitAcknowledgedStreamedDeliveries();
1576
- delivery.emitFailedPendingStreamedDeliveries(error);
1577
- logVerbose(`slack-stream: failed to rotate native progress stream (${error})`);
1578
- }
1404
+ if (completionChunks?.length) nativeProgressCompletionSent = true;
1405
+ await delivery.finishStream(completionChunks);
1579
1406
  delivery.streamSession = null;
1580
1407
  delivery.nativeProgressStreamStartPromise = null;
1581
1408
  delivery.nativeProgressStreamThreadTs = void 0;
@@ -1660,6 +1487,10 @@ function createSlackProgressRuntime(runtimeParams) {
1660
1487
  progressDraft.reset();
1661
1488
  };
1662
1489
  const beginNewProgressTurn = async (options) => {
1490
+ if (useNativeProgressStreaming) {
1491
+ if (!nativeUpdatesStopped && options?.force !== true) return false;
1492
+ await cancelNativeUpdates();
1493
+ }
1663
1494
  const priorSnapshot = progressDraft.getSnapshot();
1664
1495
  const priorFallbackText = progressCard.resolveText(priorSnapshot);
1665
1496
  const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent ? buildNativeProgressCompletionChunks(nativeProgressTerminalStatus) : void 0;
@@ -1674,6 +1505,8 @@ function createSlackProgressRuntime(runtimeParams) {
1674
1505
  nativeStreamSnapshot = EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT;
1675
1506
  nativeProgressCompletionSent = false;
1676
1507
  nativeProgressTerminalStatus = "complete";
1508
+ nativeUpdatesStopped = false;
1509
+ nativeUpdates.resetThrottleWindow();
1677
1510
  progressCard.reset();
1678
1511
  resetPreviewDeliveryState();
1679
1512
  delivery.resetDeliveryTracker();
@@ -1697,7 +1530,14 @@ function createSlackProgressRuntime(runtimeParams) {
1697
1530
  resetDraftDeliveryState();
1698
1531
  resetDraftProgressState();
1699
1532
  };
1700
- const onQueuedFollowupSettled = !useDraftProgressCard ? void 0 : async () => {
1533
+ const onQueuedFollowupSettled = !useDraftProgressCard && !useNativeProgressStreaming ? void 0 : async () => {
1534
+ if (useNativeProgressStreaming) {
1535
+ progressDraft.markFinalReplyStarted();
1536
+ await cancelNativeUpdates();
1537
+ await finishNativeProgressTurn(nativeProgressCompletionSent ? void 0 : buildNativeProgressCompletionChunks(nativeProgressTerminalStatus));
1538
+ progressDraft.markFinalReplyDelivered();
1539
+ return;
1540
+ }
1701
1541
  if (!progressCard.hasTerminalized) await draftStream?.clear();
1702
1542
  await dropDetachedProgressCards();
1703
1543
  };
@@ -1710,8 +1550,12 @@ function createSlackProgressRuntime(runtimeParams) {
1710
1550
  previewToolProgressEnabled,
1711
1551
  suppressDefaultToolProgressMessages,
1712
1552
  progressDraft,
1713
- commentaryProgressEnabled,
1714
1553
  progressWorkCounter,
1554
+ commentaryProgressEnabled,
1555
+ async cancel() {
1556
+ progressDraft.cancel();
1557
+ await cancelNativeUpdates();
1558
+ },
1715
1559
  get nativeProgressCompletionSent() {
1716
1560
  return nativeProgressCompletionSent;
1717
1561
  },
@@ -1743,6 +1587,10 @@ function createSlackProgressRuntime(runtimeParams) {
1743
1587
  async function createSlackDispatchSetup(prepared) {
1744
1588
  const { ctx, account, message, route } = prepared;
1745
1589
  const slackClient = prepared.eventScope?.client ?? ctx.app.client;
1590
+ const slackClientOptions = {
1591
+ ...ctx.app.webClientOptions,
1592
+ ...prepared.eventScope ? { teamId: prepared.eventScope.teamId } : {}
1593
+ };
1746
1594
  const slackStreamFallbackTeamId = prepared.eventScope?.teamId ?? ctx.teamId;
1747
1595
  const cfg = ctx.cfg;
1748
1596
  const runtime = ctx.runtime;
@@ -1816,24 +1664,17 @@ async function createSlackDispatchSetup(prepared) {
1816
1664
  await reactSlackMessage(message.channel, reactionMessageTs ?? "", emoji, {
1817
1665
  token: ctx.botToken,
1818
1666
  client: slackClient
1819
- }).catch((err) => {
1820
- if (formatErrorMessage(err).includes("already_reacted")) return;
1821
- throw err;
1822
1667
  });
1823
1668
  },
1824
1669
  removeReaction: async (emoji) => {
1825
1670
  await removeSlackReaction(message.channel, reactionMessageTs ?? "", emoji, {
1826
1671
  token: ctx.botToken,
1827
1672
  client: slackClient
1828
- }).catch((err) => {
1829
- if (formatErrorMessage(err).includes("no_reaction")) return;
1830
- throw err;
1831
1673
  });
1832
1674
  }
1833
1675
  },
1834
1676
  initialEmoji: prepared.ackReactionValue || "eyes",
1835
- emojis: void 0,
1836
- timing: DEFAULT_TIMING,
1677
+ presentation: "acknowledgement",
1837
1678
  onError: (err) => {
1838
1679
  logAckFailure({
1839
1680
  log: logVerbose,
@@ -1863,12 +1704,13 @@ async function createSlackDispatchSetup(prepared) {
1863
1704
  transformReplyPayload: sanitizeSlackMonitorReplyPayload,
1864
1705
  typing: {
1865
1706
  start: async () => {
1866
- if (!threadStatusGate.hasVisibleOutput()) {
1707
+ if (!didSetStatus && !threadStatusGate.hasVisibleOutput()) {
1867
1708
  didSetStatus = true;
1868
- await ctx.setSlackThreadStatus({
1709
+ await ctx.setSlackSessionStatus({
1869
1710
  channelId: message.channel,
1870
1711
  threadTs: statusThreadTs,
1871
- status: "is typing...",
1712
+ status: "processing",
1713
+ title: prepared.sessionDisplayName ?? prepared.ctxPayload.ThreadLabel,
1872
1714
  eventScope: prepared.eventScope
1873
1715
  });
1874
1716
  }
@@ -1885,10 +1727,10 @@ async function createSlackDispatchSetup(prepared) {
1885
1727
  stop: async () => {
1886
1728
  if (didSetStatus) {
1887
1729
  didSetStatus = false;
1888
- await ctx.setSlackThreadStatus({
1730
+ await ctx.setSlackSessionStatus({
1889
1731
  channelId: message.channel,
1890
1732
  threadTs: statusThreadTs,
1891
- status: "",
1733
+ status: "active",
1892
1734
  eventScope: prepared.eventScope
1893
1735
  });
1894
1736
  }
@@ -1943,15 +1785,6 @@ async function createSlackDispatchSetup(prepared) {
1943
1785
  });
1944
1786
  const shouldUseDraftStream = previewStreamingEnabled && !useStreaming;
1945
1787
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1946
- const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1947
- useStreaming,
1948
- shouldUseDraftStream,
1949
- blockStreamingEnabled
1950
- });
1951
- const onSlackDeliveryError = (err, info) => {
1952
- runtime.error?.(danger(`slack ${info.kind} reply failed: ${formatSlackError(err)}`));
1953
- replyPipeline.typingCallbacks?.onIdle?.();
1954
- };
1955
1788
  return {
1956
1789
  prepared,
1957
1790
  ctx,
@@ -1959,6 +1792,7 @@ async function createSlackDispatchSetup(prepared) {
1959
1792
  message,
1960
1793
  route,
1961
1794
  slackClient,
1795
+ slackClientOptions,
1962
1796
  slackStreamFallbackTeamId,
1963
1797
  cfg,
1964
1798
  runtime,
@@ -1988,15 +1822,18 @@ async function createSlackDispatchSetup(prepared) {
1988
1822
  previewStreamingEnabled,
1989
1823
  hasSlackCustomIdentity,
1990
1824
  shouldUseDraftStream,
1991
- disableBlockStreaming,
1992
- useStreaming,
1993
- onSlackDeliveryError
1825
+ disableBlockStreaming: sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1826
+ useStreaming,
1827
+ shouldUseDraftStream,
1828
+ blockStreamingEnabled
1829
+ }),
1830
+ useStreaming
1994
1831
  };
1995
1832
  }
1996
1833
  //#endregion
1997
1834
  //#region extensions/slack/src/monitor/message-handler/dispatch-streaming.ts
1998
1835
  function createSlackStreamingDeliveryRuntime(setup) {
1999
- const { account, ctx, forcedReplyThreadTs, isThreadReply, message, messageSentDeliveryHookContext, messageSentHookContext, messageSentHookTarget, prepared, replyDeliveryMode, replyPlan, runtime, slackClient, slackIdentity, slackMessageMetadata, slackStreamFallbackTeamId } = setup;
1836
+ const { account, ctx, forcedReplyThreadTs, isThreadReply, message, messageSentDeliveryHookContext, messageSentHookContext, messageSentHookTarget, prepared, replyDeliveryMode, replyPlan, runtime, slackClient, slackClientOptions, slackIdentity, slackMessageMetadata, slackStreamFallbackTeamId } = setup;
2000
1837
  const state = {
2001
1838
  streamSession: null,
2002
1839
  nativeProgressStreamStartPromise: null,
@@ -2007,69 +1844,14 @@ function createSlackStreamingDeliveryRuntime(setup) {
2007
1844
  observedReplyDelivery: false,
2008
1845
  observedFinalReplyDelivery: false
2009
1846
  };
2010
- const streamedDeliveries = [];
2011
- const refreshStreamedAcknowledgements = (session) => {
2012
- if (session.pendingText.length === 0) for (const delivery of streamedDeliveries) delivery.acknowledged = true;
2013
- };
2014
- const recordStreamedDelivery = (kind, content) => {
2015
- const delivery = {
2016
- kind,
1847
+ const emitStreamedDelivery = (content, result) => {
1848
+ emitSlackMessageSentHooks({
1849
+ ...messageSentHookContext,
1850
+ to: messageSentHookTarget,
1851
+ accountId: account.accountId,
2017
1852
  content,
2018
- acknowledged: false
2019
- };
2020
- streamedDeliveries.push(delivery);
2021
- return delivery;
2022
- };
2023
- const rememberStreamedDelivery = (kind, content, session) => {
2024
- recordStreamedDelivery(kind, content);
2025
- refreshStreamedAcknowledgements(session);
2026
- };
2027
- const emitAcknowledgedStreamedDeliveries = (messageId) => {
2028
- for (const delivery of streamedDeliveries) {
2029
- if (!delivery.acknowledged || delivery.outcome) continue;
2030
- emitSlackMessageSentHooks({
2031
- ...messageSentHookContext,
2032
- to: messageSentHookTarget,
2033
- accountId: account.accountId,
2034
- content: delivery.content,
2035
- success: true,
2036
- ...messageId ? { messageId } : {}
2037
- });
2038
- delivery.outcome = "success";
2039
- }
2040
- };
2041
- const acknowledgeStoppedStreamedDeliveries = (session, messageId) => {
2042
- refreshStreamedAcknowledgements(session);
2043
- for (const delivery of streamedDeliveries) delivery.acknowledged = true;
2044
- emitAcknowledgedStreamedDeliveries(messageId);
2045
- };
2046
- const emitFailedPendingStreamedDeliveries = (error) => {
2047
- for (const delivery of streamedDeliveries) {
2048
- if (delivery.acknowledged || delivery.outcome) continue;
2049
- emitSlackMessageSentHooks({
2050
- ...messageSentHookContext,
2051
- to: messageSentHookTarget,
2052
- accountId: account.accountId,
2053
- content: delivery.content,
2054
- success: false,
2055
- error
2056
- });
2057
- delivery.outcome = "failure";
2058
- }
2059
- };
2060
- const emitSuccessfulPendingStreamedDeliveries = (messageId) => {
2061
- for (const delivery of streamedDeliveries) {
2062
- if (delivery.acknowledged || delivery.outcome) continue;
2063
- emitSlackMessageSentHooks({
2064
- ...messageSentHookContext,
2065
- to: messageSentHookTarget,
2066
- accountId: account.accountId,
2067
- content: delivery.content,
2068
- success: true,
2069
- ...messageId ? { messageId } : {}
2070
- });
2071
- delivery.outcome = "success";
2072
- }
1853
+ ...result
1854
+ });
2073
1855
  };
2074
1856
  let deliveryTracker = createSlackEventDeliveryTracker();
2075
1857
  const markPreviewPayloadDelivered = (params) => {
@@ -2090,62 +1872,75 @@ function createSlackStreamingDeliveryRuntime(setup) {
2090
1872
  if (kind === "block") state.usedBlockReplyThreadTs = deliveredThreadTs;
2091
1873
  };
2092
1874
  const deliverPendingStreamFallback = async (session, err) => {
1875
+ if (session.stoppedBySlack) return;
2093
1876
  let fallbackError = err;
2094
1877
  if (!session.stopped) try {
2095
1878
  const stopResult = await stopSlackStream({
2096
1879
  session,
2097
1880
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2098
1881
  });
2099
- acknowledgeStoppedStreamedDeliveries(session, stopResult.messageId);
1882
+ if (session.stoppedBySlack) return;
2100
1883
  state.observedReplyDelivery = true;
2101
1884
  state.usedReplyThreadTs ??= session.threadTs;
2102
- return true;
1885
+ return stopResult;
2103
1886
  } catch (stopErr) {
2104
1887
  if (stopErr instanceof SlackStreamNotDeliveredError) fallbackError = stopErr;
2105
- else runtime.error?.(danger(`slack-stream: failed to finalize buffered text before fallback: ${formatSlackError(stopErr)}`));
1888
+ else throw stopErr;
2106
1889
  }
2107
- emitAcknowledgedStreamedDeliveries();
2108
1890
  const fallbackText = fallbackError.pendingText.trim();
2109
- if (!fallbackText) return false;
2110
- try {
2111
- await deliverReplies({
2112
- cfg: ctx.cfg,
2113
- replies: [{ text: fallbackText }],
2114
- target: prepared.replyTarget,
2115
- token: ctx.botToken,
2116
- accountId: account.accountId,
2117
- runtime,
2118
- textLimit: ctx.textLimit,
2119
- mediaMaxBytes: ctx.mediaMaxBytes,
2120
- replyThreadTs: session.threadTs,
2121
- replyToMode: replyDeliveryMode,
2122
- ...slackIdentity ? { identity: slackIdentity } : {},
2123
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
2124
- ...messageSentDeliveryHookContext,
2125
- deferMessageSentHooks: true,
2126
- eventScope: prepared.eventScope
1891
+ if (!fallbackText) return;
1892
+ await deliverReplies({
1893
+ cfg: ctx.cfg,
1894
+ replies: [{ text: fallbackText }],
1895
+ target: prepared.replyTarget,
1896
+ token: ctx.botToken,
1897
+ accountId: account.accountId,
1898
+ runtime,
1899
+ textLimit: ctx.textLimit,
1900
+ mediaMaxBytes: ctx.mediaMaxBytes,
1901
+ replyThreadTs: session.threadTs,
1902
+ replyToMode: replyDeliveryMode,
1903
+ ...slackIdentity ? { identity: slackIdentity } : {},
1904
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1905
+ ...messageSentDeliveryHookContext,
1906
+ deferMessageSentHooks: true,
1907
+ eventScope: prepared.eventScope
1908
+ });
1909
+ markSlackStreamFallbackDelivered(session);
1910
+ if (!session.stopped) try {
1911
+ await stopSlackStream({
1912
+ session,
1913
+ ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2127
1914
  });
2128
- markSlackStreamFallbackDelivered(session);
2129
- if (!session.stopped) try {
1915
+ } catch (finalizeErr) {
1916
+ runtime.error?.(danger(`slack-stream: failed to finalize native stream after fallback delivery: ${formatSlackError(finalizeErr)}`));
1917
+ }
1918
+ state.observedReplyDelivery = true;
1919
+ state.usedReplyThreadTs ??= session.threadTs;
1920
+ logVerbose(`slack-stream: streamed delivery failed (${fallbackError.slackCode}); delivered ${fallbackText.length} chars via deliverReplies fallback`);
1921
+ return {};
1922
+ };
1923
+ const finishStream = async (chunks) => {
1924
+ const session = state.streamSession;
1925
+ if (session && !session.stopped) try {
1926
+ try {
2130
1927
  await stopSlackStream({
2131
1928
  session,
1929
+ ...chunks?.length ? { chunks } : {},
2132
1930
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2133
1931
  });
2134
- } catch (finalizeErr) {
2135
- runtime.error?.(danger(`slack-stream: failed to finalize native stream after fallback delivery: ${formatSlackError(finalizeErr)}`));
1932
+ state.observedReplyDelivery ||= session.delivered;
1933
+ } catch (error) {
1934
+ if (!(error instanceof SlackStreamNotDeliveredError)) throw error;
1935
+ await deliverPendingStreamFallback(session, error);
2136
1936
  }
2137
- emitSuccessfulPendingStreamedDeliveries();
2138
- state.observedReplyDelivery = true;
2139
- state.usedReplyThreadTs ??= session.threadTs;
2140
- logVerbose(`slack-stream: streamed delivery failed (${fallbackError.slackCode}); delivered ${fallbackText.length} chars via deliverReplies fallback`);
2141
- return true;
2142
- } catch (postErr) {
2143
- emitFailedPendingStreamedDeliveries(formatErrorMessage(postErr));
2144
- runtime.error?.(danger(`slack-stream: fallback deliverReplies failed after ${fallbackError.slackCode}: ${formatErrorMessage(postErr)}`));
2145
- return false;
1937
+ } catch (error) {
1938
+ state.streamFailed = true;
1939
+ runtime.error?.(danger(`slack-stream: failed to stop stream: ${formatSlackError(error)}`));
2146
1940
  }
2147
1941
  };
2148
1942
  const deliverNormally = async (params) => {
1943
+ if (state.streamSession?.stoppedBySlack) return;
2149
1944
  const replyThreadTs = resolveDeliveryThreadTs(params);
2150
1945
  const deliveryReplyThreadTs = replyDeliveryMode === "off" && !forcedReplyThreadTs && !isThreadReply ? void 0 : replyThreadTs;
2151
1946
  if (deliveryTracker.hasDelivered({
@@ -2174,10 +1969,11 @@ function createSlackStreamingDeliveryRuntime(setup) {
2174
1969
  });
2175
1970
  state.observedReplyDelivery = true;
2176
1971
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
2177
- const deliveredThreadTs = resolveDeliveredSlackReplyThreadTs({
1972
+ const deliveredThreadTs = resolveSlackReplyThreadTs({
2178
1973
  replyToMode: replyDeliveryMode,
2179
- payloadReplyToId: params.payload.replyToId,
2180
- replyThreadTs: deliveryReplyThreadTs
1974
+ replyToId: params.payload.replyToId,
1975
+ threadId: deliveryReplyThreadTs,
1976
+ replyToCurrent: params.payload.replyToCurrent
2181
1977
  });
2182
1978
  rememberDeliveredThreadTs(params.kind, deliveredThreadTs);
2183
1979
  replyPlan.markSent();
@@ -2188,19 +1984,6 @@ function createSlackStreamingDeliveryRuntime(setup) {
2188
1984
  });
2189
1985
  return deliveryReplyThreadTs;
2190
1986
  };
2191
- const deliverBufferedStreamFallback = async (params) => {
2192
- if (!await deliverPendingStreamFallback(params.session, params.err)) return false;
2193
- replyPlan.markSent();
2194
- if (params.kind === "final") state.observedFinalReplyDelivery = true;
2195
- deliveryTracker.markDelivered({
2196
- kind: params.kind,
2197
- payload: params.payload,
2198
- threadTs: params.session.threadTs,
2199
- textOverride: params.textOverride
2200
- });
2201
- rememberDeliveredThreadTs(params.kind, params.session.threadTs);
2202
- return true;
2203
- };
2204
1987
  const isStreamingEligible = (payload, options) => {
2205
1988
  const reply = resolveSendableOutboundReplyParts(payload);
2206
1989
  const renderPlan = resolveSlackReplyRenderPlan(payload);
@@ -2208,7 +1991,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
2208
1991
  return !state.streamFailed && !reply.hasMedia && renderPlan.mode !== "split" && !plannedBlocks?.length && !readSlackReplyBlocks(payload)?.length && reply.hasText && (!options?.maxTextBytes || countSlackTextUtf8Bytes(reply.trimmedText) <= options.maxTextBytes);
2209
1992
  };
2210
1993
  const deliverWithStreaming = async (params) => {
2211
- const reply = resolveSendableOutboundReplyParts(params.payload);
1994
+ if (state.streamSession?.stoppedBySlack) return;
2212
1995
  if (!isStreamingEligible(params.payload)) {
2213
1996
  await deliverNormally({
2214
1997
  payload: params.payload,
@@ -2217,45 +2000,44 @@ function createSlackStreamingDeliveryRuntime(setup) {
2217
2000
  });
2218
2001
  return;
2219
2002
  }
2220
- const text = params.streamText ?? reply.trimmedText;
2221
- const hookContent = reply.trimmedText;
2222
- let plannedThreadTs;
2003
+ if (!state.streamSession && state.nativeProgressStreamStartPromise) await state.nativeProgressStreamStartPromise;
2004
+ if (state.streamSession?.stoppedBySlack) return;
2005
+ let session = state.streamSession;
2006
+ const threadTs = session?.threadTs ?? replyPlan.nextThreadTs();
2007
+ if (state.streamFailed || !threadTs) {
2008
+ state.streamFailed = true;
2009
+ await deliverNormally({
2010
+ payload: params.payload,
2011
+ kind: params.kind,
2012
+ forcedThreadTs: threadTs ?? state.nativeProgressStreamThreadTs
2013
+ });
2014
+ return;
2015
+ }
2016
+ const hookContent = resolveSendableOutboundReplyParts(params.payload).trimmedText;
2017
+ const text = params.streamText ?? hookContent;
2018
+ if (deliveryTracker.hasDelivered({
2019
+ ...params,
2020
+ threadTs,
2021
+ textOverride: text
2022
+ })) {
2023
+ logVerbose("slack-stream: suppressed duplicate reply payload");
2024
+ return;
2025
+ }
2026
+ let messageId;
2223
2027
  try {
2224
- if (!state.streamSession && state.nativeProgressStreamStartPromise) await state.nativeProgressStreamStartPromise;
2225
- if (state.streamFailed) {
2226
- await deliverNormally({
2227
- payload: params.payload,
2228
- kind: params.kind,
2229
- forcedThreadTs: state.streamSession?.threadTs ?? state.nativeProgressStreamThreadTs
2230
- });
2231
- return;
2232
- }
2233
- if (!state.streamSession) {
2234
- const streamThreadTs = replyPlan.nextThreadTs();
2235
- plannedThreadTs = streamThreadTs;
2236
- if (!streamThreadTs) {
2237
- logVerbose("slack-stream: no reply thread target for stream start, falling back to normal delivery");
2238
- state.streamFailed = true;
2239
- await deliverNormally({
2240
- payload: params.payload,
2241
- kind: params.kind
2242
- });
2243
- return;
2244
- }
2245
- if (deliveryTracker.hasDelivered({
2246
- kind: params.kind,
2247
- payload: params.payload,
2248
- threadTs: streamThreadTs,
2249
- textOverride: text
2250
- })) {
2251
- logVerbose("slack-stream: suppressed duplicate stream start payload");
2252
- return;
2253
- }
2254
- state.streamSession = await startSlackStream({
2028
+ if (session) await appendSlackStream({
2029
+ session,
2030
+ text: `${params.appendSeparator === false ? "" : "\n"}${text}`,
2031
+ chunks: []
2032
+ });
2033
+ else {
2034
+ session = await startSlackStream({
2255
2035
  client: slackClient,
2036
+ clientOptions: slackClientOptions,
2256
2037
  channel: message.channel,
2257
- threadTs: streamThreadTs,
2038
+ threadTs,
2258
2039
  text,
2040
+ chunks: [],
2259
2041
  ...params.taskDisplayMode ? { taskDisplayMode: params.taskDisplayMode } : {},
2260
2042
  ...slackIdentity ? { identity: slackIdentity } : {},
2261
2043
  teamId: await resolveSlackStreamRecipientTeamId({
@@ -2266,94 +2048,63 @@ function createSlackStreamingDeliveryRuntime(setup) {
2266
2048
  }),
2267
2049
  userId: message.user
2268
2050
  });
2269
- refreshStreamedAcknowledgements(state.streamSession);
2270
- if (state.streamSession.delivered) {
2271
- state.observedReplyDelivery = true;
2272
- if (params.kind === "final") state.observedFinalReplyDelivery = true;
2273
- }
2274
- if (hookContent) rememberStreamedDelivery(params.kind, hookContent, state.streamSession);
2275
- rememberDeliveredThreadTs(params.kind, streamThreadTs);
2276
- replyPlan.markSent();
2277
- deliveryTracker.markDelivered({
2278
- kind: params.kind,
2279
- payload: params.payload,
2280
- threadTs: streamThreadTs,
2281
- textOverride: text
2282
- });
2283
- return;
2051
+ state.streamSession = session;
2284
2052
  }
2285
- if (deliveryTracker.hasDelivered({
2286
- kind: params.kind,
2287
- payload: params.payload,
2288
- threadTs: state.streamSession.threadTs,
2289
- textOverride: text
2290
- })) {
2291
- logVerbose("slack-stream: suppressed duplicate append payload");
2292
- return;
2293
- }
2294
- if (hookContent) recordStreamedDelivery(params.kind, hookContent);
2295
- await appendSlackStream({
2296
- session: state.streamSession,
2297
- text: `${params.appendSeparator === false ? "" : "\n"}${text}`
2298
- });
2299
- refreshStreamedAcknowledgements(state.streamSession);
2300
- if (state.streamSession.delivered && state.streamSession.pendingText.length === 0) {
2301
- state.observedReplyDelivery = true;
2302
- if (params.kind === "final") state.observedFinalReplyDelivery = true;
2053
+ messageId = session.streamer.ts;
2054
+ } catch (error) {
2055
+ state.streamFailed = true;
2056
+ if (!(error instanceof SlackStreamNotDeliveredError)) {
2057
+ emitStreamedDelivery(hookContent, {
2058
+ success: false,
2059
+ error: formatErrorMessage(error)
2060
+ });
2061
+ throw error;
2303
2062
  }
2304
- deliveryTracker.markDelivered({
2305
- kind: params.kind,
2306
- payload: params.payload,
2307
- threadTs: state.streamSession.threadTs,
2308
- textOverride: text
2309
- });
2310
- } catch (err) {
2311
- if (err instanceof SlackStreamNotDeliveredError) {
2312
- state.streamFailed = true;
2313
- if (state.streamSession) {
2314
- if (await deliverBufferedStreamFallback({
2315
- session: state.streamSession,
2316
- err,
2317
- payload: params.payload,
2318
- kind: params.kind,
2319
- textOverride: text
2320
- })) return;
2321
- throw err;
2322
- }
2063
+ if (!session) {
2323
2064
  await deliverNormally({
2324
2065
  payload: params.payload,
2325
2066
  kind: params.kind,
2326
- forcedThreadTs: plannedThreadTs
2067
+ forcedThreadTs: threadTs
2327
2068
  });
2328
2069
  return;
2329
2070
  }
2330
- runtime.error?.(danger(`slack-stream: streaming API call failed: ${formatSlackError(err)}, falling back`));
2331
- state.streamFailed = true;
2332
- if (state.streamSession && state.streamSession.pendingText) {
2333
- const bufferedFallbackErr = new SlackStreamNotDeliveredError(state.streamSession.pendingText, "unknown");
2334
- if (await deliverBufferedStreamFallback({
2335
- session: state.streamSession,
2336
- err: bufferedFallbackErr,
2337
- payload: params.payload,
2338
- kind: params.kind,
2339
- textOverride: text
2340
- })) return;
2341
- throw err;
2071
+ try {
2072
+ const fallback = await deliverPendingStreamFallback(session, error);
2073
+ if (!fallback && !session.stoppedBySlack) throw error;
2074
+ messageId = fallback?.messageId;
2075
+ } catch (fallbackError) {
2076
+ emitStreamedDelivery(hookContent, {
2077
+ success: false,
2078
+ error: formatErrorMessage(fallbackError)
2079
+ });
2080
+ throw fallbackError;
2342
2081
  }
2343
- await deliverNormally({
2344
- payload: params.payload,
2345
- kind: params.kind,
2346
- forcedThreadTs: state.streamSession?.threadTs ?? state.nativeProgressStreamThreadTs ?? plannedThreadTs
2082
+ }
2083
+ if (session.stoppedBySlack && session.pendingText) {
2084
+ emitStreamedDelivery(hookContent, {
2085
+ success: false,
2086
+ error: "Stopped by Slack user"
2347
2087
  });
2088
+ return;
2348
2089
  }
2090
+ state.observedReplyDelivery = true;
2091
+ if (params.kind === "final") state.observedFinalReplyDelivery = true;
2092
+ rememberDeliveredThreadTs(params.kind, threadTs);
2093
+ replyPlan.markSent();
2094
+ deliveryTracker.markDelivered({
2095
+ ...params,
2096
+ threadTs,
2097
+ textOverride: text
2098
+ });
2099
+ emitStreamedDelivery(hookContent, {
2100
+ success: true,
2101
+ ...messageId ? { messageId } : {}
2102
+ });
2349
2103
  };
2350
2104
  return Object.assign(state, {
2351
- acknowledgeStoppedStreamedDeliveries,
2352
2105
  deliverNormally,
2353
- deliverPendingStreamFallback,
2354
2106
  deliverWithStreaming,
2355
- emitAcknowledgedStreamedDeliveries,
2356
- emitFailedPendingStreamedDeliveries,
2107
+ finishStream,
2357
2108
  hasDelivered: (params) => deliveryTracker.hasDelivered(params),
2358
2109
  isStreamingEligible,
2359
2110
  markPreviewPayloadDelivered,
@@ -2367,7 +2118,35 @@ function createSlackStreamingDeliveryRuntime(setup) {
2367
2118
  //#region extensions/slack/src/monitor/message-handler/dispatch.ts
2368
2119
  async function dispatchPreparedSlackMessage(prepared) {
2369
2120
  const setup = await createSlackDispatchSetup(prepared);
2370
- const { account, cfg, ctx, disableBlockStreaming, hasSlackCustomIdentity, hasRepliedRef, message, messageSentHookContext, messageSentHookTarget, onModelSelected, onSlackDeliveryError, previewStreamingEnabled, replyPipeline, replyPlan, route, runtime, slackClient, slackMessageMetadata, slackStreaming, sourceReplyDeliveryMode, statusReactions, statusReactionsEnabled, statusThreadTs, suppressRoomEventTyping, useStreaming } = setup;
2121
+ const beginSessionRun = () => registerSlackSessionRun(prepared.ctx, {
2122
+ channelId: prepared.message.channel,
2123
+ threadTs: setup.streamThreadHint,
2124
+ eventScope: prepared.eventScope
2125
+ }, {
2126
+ ...prepared.route,
2127
+ sessionKey: prepared.ctxPayload.SessionKey ?? prepared.route.sessionKey
2128
+ });
2129
+ const upstreamLifecycle = prepared.turnAdoptionLifecycle;
2130
+ let releaseDeferred;
2131
+ const turnAdoptionLifecycle = upstreamLifecycle && {
2132
+ ...upstreamLifecycle,
2133
+ onDeferred: () => {
2134
+ const accepted = upstreamLifecycle.onDeferred?.();
2135
+ if (accepted !== false) releaseDeferred ??= beginSessionRun();
2136
+ return accepted;
2137
+ },
2138
+ onSettled: () => {
2139
+ releaseDeferred?.();
2140
+ upstreamLifecycle.onSettled?.();
2141
+ }
2142
+ };
2143
+ const release = beginSessionRun();
2144
+ await dispatchSlackMessageWithSetup(setup, beginSessionRun, turnAdoptionLifecycle).finally(release);
2145
+ }
2146
+ async function dispatchSlackMessageWithSetup(setup, beginSessionRun, turnAdoptionLifecycle) {
2147
+ const { prepared } = setup;
2148
+ const { account, cfg, ctx, disableBlockStreaming, hasSlackCustomIdentity, hasRepliedRef, message, messageSentHookContext, messageSentHookTarget, onModelSelected, previewStreamingEnabled, replyPipeline, replyPlan, route, runtime, slackClient, slackStreaming, sourceReplyDeliveryMode, statusReactions, statusReactionsEnabled, statusThreadTs, suppressRoomEventTyping, useStreaming } = setup;
2149
+ let dispatchError;
2371
2150
  const delivery = createSlackStreamingDeliveryRuntime(setup);
2372
2151
  const draftPreviewCommitted = { value: false };
2373
2152
  const progress = createSlackProgressRuntime({
@@ -2408,6 +2187,14 @@ async function dispatchPreparedSlackMessage(prepared) {
2408
2187
  pendingFailureNotice = notice;
2409
2188
  return payload;
2410
2189
  };
2190
+ let compactFinalDeliveryStarted = false;
2191
+ const clearCompactProgress = async () => {
2192
+ try {
2193
+ await draftStream?.clear();
2194
+ } catch (err) {
2195
+ logVerbose(`slack: progress preview cleanup failed (${formatSlackError(err)})`);
2196
+ }
2197
+ };
2411
2198
  const deliverSlackPayload = async (payload, info) => {
2412
2199
  if (info.kind === "final" && slackStreaming.mode === "progress" && progress.isProgressMode) {
2413
2200
  if (progress.useNativeProgressStreaming) {
@@ -2415,6 +2202,24 @@ async function dispatchPreparedSlackMessage(prepared) {
2415
2202
  return;
2416
2203
  }
2417
2204
  progress.progressDraft.markFinalReplyStarted();
2205
+ if (!progress.useDraftProgressCard) {
2206
+ compactFinalDeliveryStarted = true;
2207
+ await draftStream?.discardPending();
2208
+ const supplement = getReplyPayloadTtsSupplement(payload);
2209
+ await delivery.deliverNormally({
2210
+ payload: supplement && !supplement.visibleTextAlreadyDelivered && !payload.text?.trim() ? {
2211
+ ...payload,
2212
+ text: supplement.spokenText
2213
+ } : payload,
2214
+ kind: info.kind,
2215
+ forcedThreadTs: delivery.usedReplyThreadTs
2216
+ });
2217
+ if (delivery.observedFinalReplyDelivery) {
2218
+ progress.progressDraft.markFinalReplyDelivered();
2219
+ await clearCompactProgress();
2220
+ }
2221
+ return;
2222
+ }
2418
2223
  if (progress.useDraftProgressCard) {
2419
2224
  await delivery.deliverNormally({
2420
2225
  payload,
@@ -2459,7 +2264,11 @@ async function dispatchPreparedSlackMessage(prepared) {
2459
2264
  const slackBlocks = replyRenderPlan.mode === "single" ? replyRenderPlan.blocks : replyRenderPlan.blockPart?.blocks;
2460
2265
  const requiresSeparateFallbackDelivery = replyRenderPlan.mode === "split";
2461
2266
  const trimmedFinalText = replyRenderPlan.mode === "single" ? replyRenderPlan.text.trim() : replyRenderPlan.fallbackText.trim();
2462
- const previewFinalText = replyRenderPlan.mode === "single" && replyRenderPlan.textIsSlackMrkdwn ? trimmedFinalText : normalizeSlackOutboundText((replySourceText ?? "").trim());
2267
+ const previewFinalText = replyRenderPlan.mode === "single" && replyRenderPlan.textIsSlackMrkdwn ? trimmedFinalText : normalizeSlackOutboundText((replySourceText ?? "").trim(), { tableMode: resolveMarkdownTableMode({
2268
+ cfg,
2269
+ channel: "slack",
2270
+ accountId: account.accountId
2271
+ }) });
2463
2272
  const previewFinalTextFitsEdit = countSlackTextUtf8Bytes(previewFinalText) <= SLACK_EDIT_TEXT_MAX_BYTES;
2464
2273
  const shouldRestoreTtsSupplementTextForPreviewFallback = Boolean(ttsSupplement) && ttsSupplement?.visibleTextAlreadyDelivered !== true && Boolean(draftStream) && !draftPreviewCommitted.value && !delivery.observedFinalReplyDelivery && previewStreamingEnabled && !payload.text?.trim();
2465
2274
  let ttsPreviewFinalization;
@@ -2562,7 +2371,6 @@ async function dispatchPreparedSlackMessage(prepared) {
2562
2371
  });
2563
2372
  if (info.kind === "final") progress.progressDraft.markFinalReplyDelivered();
2564
2373
  };
2565
- let dispatchError;
2566
2374
  let agentRunFailed = false;
2567
2375
  let settledDispatchResult;
2568
2376
  try {
@@ -2586,13 +2394,18 @@ async function dispatchPreparedSlackMessage(prepared) {
2586
2394
  },
2587
2395
  delivery: {
2588
2396
  deliver: deliverSlackPayload,
2589
- onError: onSlackDeliveryError
2397
+ onError: (err, info) => {
2398
+ dispatchError ??= err;
2399
+ runtime.error?.(danger(`slack ${info.kind} reply failed: ${formatSlackError(err)}`));
2400
+ replyPipeline.typingCallbacks?.onIdle?.();
2401
+ }
2590
2402
  },
2591
2403
  record: prepared.turn.record,
2592
2404
  history: prepared.turn.history,
2593
2405
  botLoopProtection: resolveSlackBotLoopProtection(prepared),
2594
2406
  replyOptions: {
2595
- ...prepared.turnAdoptionLifecycle ? { turnAdoptionLifecycle: prepared.turnAdoptionLifecycle } : {},
2407
+ queuedDeliveryCorrelations: [{ begin: beginSessionRun }],
2408
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
2596
2409
  skillFilter: prepared.channelConfig?.skills,
2597
2410
  sourceReplyDeliveryMode,
2598
2411
  suppressTyping: suppressRoomEventTyping ? true : void 0,
@@ -2622,18 +2435,18 @@ async function dispatchPreparedSlackMessage(prepared) {
2622
2435
  },
2623
2436
  onQueuedFollowupAdmitted: progress.onQueuedFollowupAdmitted,
2624
2437
  onQueuedFollowupSettled: progress.onQueuedFollowupSettled,
2625
- onReasoningStream: statusReactionsEnabled || progress.previewToolProgressEnabled ? async (payload) => {
2438
+ onReasoningStream: async (payload) => {
2626
2439
  const visible = await progress.pushReasoningProgress(payload);
2627
- if (!statusReactionsEnabled) return visible;
2628
- await statusReactions.setThinking();
2440
+ if (statusReactionsEnabled) await statusReactions.setThinking();
2629
2441
  return visible;
2630
- } : void 0,
2442
+ },
2631
2443
  onToolStart: async (payload) => {
2632
2444
  if (statusReactionsEnabled) await statusReactions.setTool(payload.name);
2633
2445
  if (payload.phase === "start") progress.progressWorkCounter.noteToolCall(payload.name);
2634
2446
  return await progress.progressDraft.pushToolEvent(payload);
2635
2447
  },
2636
2448
  onItemEvent: async (payload) => {
2449
+ if (payload.kind === "preamble" && (payload.phase === "start" || payload.phase === "update") && !draftStream?.messageId() && !delivery.streamSession?.delivered) return false;
2637
2450
  if (progress.isProgressMode && payload.kind === "preamble") {
2638
2451
  if (progress.shouldYieldDraftProgress()) return false;
2639
2452
  const headlineVisible = await progress.progressDraft.pushPreambleHeadline(payload.progressText, { itemId: payload.itemId });
@@ -2670,48 +2483,28 @@ async function dispatchPreparedSlackMessage(prepared) {
2670
2483
  });
2671
2484
  }
2672
2485
  } catch (err) {
2673
- dispatchError = err;
2486
+ dispatchError ??= err;
2674
2487
  } finally {
2675
- progress.progressDraft.cancel();
2488
+ await progress.cancel();
2676
2489
  if (!progress.useDraftProgressCard) await draftStream?.discardPending();
2677
2490
  }
2678
- let streamFallbackDelivered = false;
2679
- const finalStream = delivery.streamSession;
2680
- if (finalStream && !finalStream.stopped) try {
2681
- const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError || agentRunFailed ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2682
- if (completionChunks?.length) progress.nativeProgressCompletionSent = true;
2683
- const stopResult = await stopSlackStream({
2684
- session: finalStream,
2685
- ...completionChunks?.length ? { chunks: completionChunks } : {},
2686
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2687
- });
2688
- delivery.acknowledgeStoppedStreamedDeliveries(finalStream, stopResult?.messageId);
2689
- } catch (err) {
2690
- if (err instanceof SlackStreamNotDeliveredError) {
2691
- streamFallbackDelivered = await delivery.deliverPendingStreamFallback(finalStream, err);
2692
- if (!streamFallbackDelivered) dispatchError ??= err;
2693
- } else {
2694
- const error = formatSlackError(err);
2695
- delivery.emitAcknowledgedStreamedDeliveries();
2696
- delivery.emitFailedPendingStreamedDeliveries(error);
2697
- runtime.error?.(danger(`slack-stream: failed to stop stream: ${error}`));
2698
- if (!finalStream.delivered) dispatchError ??= err;
2699
- }
2700
- }
2701
- const anyReplyDelivered = hasVisibleInboundReplyDispatch(settledDispatchResult, {
2702
- observedReplyDelivery: delivery.observedReplyDelivery,
2703
- fallbackDelivered: streamFallbackDelivered
2704
- });
2491
+ const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError || agentRunFailed ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2492
+ if (completionChunks?.length) progress.nativeProgressCompletionSent = true;
2493
+ await delivery.finishStream(completionChunks);
2494
+ const anyReplyDelivered = hasVisibleInboundReplyDispatch(settledDispatchResult, { observedReplyDelivery: delivery.observedReplyDelivery });
2495
+ if (progress.isProgressMode && !progress.useNativeProgressStreaming && !progress.useDraftProgressCard && !compactFinalDeliveryStarted && !dispatchError && !agentRunFailed && anyReplyDelivered) await clearCompactProgress();
2705
2496
  if (pendingFailureNotice && anyReplyDelivered) recordSlackThreadFailureNotice(pendingFailureNotice);
2706
2497
  if (dispatchError || agentRunFailed) await progress.finalizeDraftProgressCard("error");
2707
2498
  await progress.dropDetachedProgressCards();
2708
- if (statusReactionsEnabled) if (dispatchError || agentRunFailed) {
2709
- await statusReactions.setError();
2710
- statusReactions.restoreInitial();
2711
- } else if (anyReplyDelivered) {
2712
- await statusReactions.setDone();
2713
- statusReactions.restoreInitial();
2714
- } else await statusReactions.restoreInitial();
2499
+ if (statusReactionsEnabled) {
2500
+ if (dispatchError || agentRunFailed) {
2501
+ await statusReactions.setError();
2502
+ statusReactions.restoreInitial();
2503
+ } else if (anyReplyDelivered) {
2504
+ await statusReactions.setDone();
2505
+ statusReactions.restoreInitial();
2506
+ } else await statusReactions.restoreInitial();
2507
+ }
2715
2508
  const participationThreadTs = delivery.usedReplyThreadTs ?? statusThreadTs;
2716
2509
  if (anyReplyDelivered && participationThreadTs) recordSlackThreadParticipation(account.accountId, message.channel, participationThreadTs, {
2717
2510
  agentId: route.agentId,
@@ -2770,7 +2563,7 @@ function normalizeSlackAppContextEntities(value) {
2770
2563
  //#endregion
2771
2564
  //#region extensions/slack/src/monitor/thread.ts
2772
2565
  const THREAD_STARTER_CACHE = /* @__PURE__ */ new Map();
2773
- const THREAD_STARTER_CACHE_TTL_MS = 360 * 6e4;
2566
+ const THREAD_STARTER_CACHE_TTL_MS = 216e5;
2774
2567
  const THREAD_STARTER_CACHE_MAX = 2e3;
2775
2568
  function evictThreadStarterCache() {
2776
2569
  const now = asDateTimestampMs(Date.now());
@@ -2965,7 +2758,24 @@ async function discardSlackPreflightMedia(media) {
2965
2758
  const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4;
2966
2759
  const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20;
2967
2760
  const SLACK_USER_MENTION_RE$1 = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
2968
- const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-XdQyjG_p.js").then((n) => n._));
2761
+ const MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS = 2e3;
2762
+ const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-CFkvEv1d.js").then((n) => n._));
2763
+ function formatSlackUnavailableMedia(params) {
2764
+ let fileReferences = params.files?.map((file) => `${formatSlackFileReference(file)} unavailable (${file.reason})`).join(", ");
2765
+ if (fileReferences && fileReferences.length > MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS) fileReferences = `${truncateUtf16Safe(fileReferences, MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS)}; … (file references truncated)`;
2766
+ const fileBlock = fileReferences ? `[Slack file: ${fileReferences}]` : void 0;
2767
+ const body = [params.body, fileBlock].filter(Boolean).join("\n");
2768
+ if (params.unavailableMediaCount === 0) return body;
2769
+ const notice = `[slack ${params.unavailableMediaCount > 1 ? `${params.unavailableMediaCount} attachments` : "attachment"} unavailable]`;
2770
+ return params.prependUnavailable ? [
2771
+ notice,
2772
+ fileBlock,
2773
+ params.body
2774
+ ].filter(Boolean).join("\n") : formatInboundMediaUnavailableText({
2775
+ body,
2776
+ notice
2777
+ });
2778
+ }
2969
2779
  function collectUniqueSlackMentionIds$1(texts) {
2970
2780
  const seen = /* @__PURE__ */ new Set();
2971
2781
  const mentionIds = [];
@@ -3016,7 +2826,6 @@ async function resolveSlackMessageContent(params) {
3016
2826
  })) : null;
3017
2827
  const effectiveDirectMedia = attachmentContent?.media.length ? attachmentContent.media : null;
3018
2828
  const mediaPlaceholder = effectiveDirectMedia?.map((item) => item.placeholder).join(" ");
3019
- const fileOnlyFallback = attachmentContent?.files?.map(formatSlackFileReference).join(", ");
3020
2829
  let botAttachmentText;
3021
2830
  if (params.isBotMessage && !attachmentContent?.text) {
3022
2831
  const botAttachmentTextParts = [];
@@ -3040,7 +2849,8 @@ async function resolveSlackMessageContent(params) {
3040
2849
  if (skippedLookups > 0) logVerbose(`slack: skipping ${skippedLookups} mention lookup(s) beyond per-message cap (${SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE})`);
3041
2850
  const { results } = await runTasksWithConcurrency({
3042
2851
  tasks: lookupIds.map((userId) => async () => {
3043
- const renderedName = normalizeOptionalString((await resolveUserName(userId))?.name);
2852
+ const user = await resolveUserName(userId);
2853
+ const renderedName = normalizeOptionalString(user?.name);
3044
2854
  return {
3045
2855
  userId,
3046
2856
  rendered: renderedName ? `<@${userId}> (${renderedName})` : null
@@ -3053,17 +2863,15 @@ async function resolveSlackMessageContent(params) {
3053
2863
  renderedMentions.set(result.userId, result.rendered);
3054
2864
  }
3055
2865
  }
3056
- let rawBody = [
3057
- renderSlackUserMentions(textParts[0], renderedMentions),
3058
- renderSlackUserMentions(textParts[1], renderedMentions),
3059
- renderSlackUserMentions(textParts[2], renderedMentions),
3060
- mediaPlaceholder,
3061
- fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : void 0
3062
- ].filter(Boolean).join("\n") || "";
3063
- const unavailableImageCount = attachmentContent?.unavailableImageCount ?? 0;
3064
- if (unavailableImageCount > 0) rawBody = formatInboundMediaUnavailableText({
3065
- body: rawBody,
3066
- notice: `[slack ${unavailableImageCount > 1 ? `${unavailableImageCount} forwarded images` : "forwarded image"} unavailable]`
2866
+ const rawBody = formatSlackUnavailableMedia({
2867
+ body: [
2868
+ renderSlackUserMentions(textParts[0], renderedMentions),
2869
+ renderSlackUserMentions(textParts[1], renderedMentions),
2870
+ renderSlackUserMentions(textParts[2], renderedMentions),
2871
+ mediaPlaceholder
2872
+ ].filter(Boolean).join("\n") || "",
2873
+ files: attachmentContent?.files,
2874
+ unavailableMediaCount: attachmentContent?.unavailableMediaCount ?? 0
3067
2875
  });
3068
2876
  return rawBody ? {
3069
2877
  rawBody,
@@ -3143,104 +2951,6 @@ async function resolveSlackDmHistoryContext(params) {
3143
2951
  }
3144
2952
  }
3145
2953
  //#endregion
3146
- //#region extensions/slack/src/monitor/message-handler/prepare-routing.ts
3147
- function resolveSlackBaseConversationId(params) {
3148
- return qualifySlackConversationId(params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel, params.eventScope);
3149
- }
3150
- function resolveSlackInitialAgentRoute(params) {
3151
- const route = resolveAgentRoute({
3152
- cfg: normalizeSlackRouteBindingConfig(params.ctx.cfg),
3153
- channel: "slack",
3154
- accountId: params.account.accountId,
3155
- teamId: params.eventScope?.teamId || params.ctx.teamId || void 0,
3156
- peer: {
3157
- kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
3158
- id: qualifySlackRoutePeerId({
3159
- id: params.isDirectMessage ? params.message.user ?? "unknown" : params.message.channel,
3160
- kind: params.isDirectMessage ? "user" : "channel",
3161
- eventScope: params.eventScope
3162
- })
3163
- }
3164
- });
3165
- if (!params.eventScope || !params.isDirectMessage || route.dmScope !== "main") return route;
3166
- const sessionKey = resolveSlackEnterpriseMainDmSessionKey({
3167
- baseSessionKey: route.sessionKey,
3168
- accountId: params.account.accountId,
3169
- eventScope: params.eventScope
3170
- });
3171
- return {
3172
- ...route,
3173
- sessionKey,
3174
- mainSessionKey: sessionKey
3175
- };
3176
- }
3177
- function resolveSlackRoutingContext(params) {
3178
- const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish, channelConfig, seedTopLevelRoomThread, assistantThreadTs, agentViewThreadTs, eventScope } = params;
3179
- let route = resolveSlackInitialAgentRoute({
3180
- ctx,
3181
- account,
3182
- message,
3183
- isDirectMessage,
3184
- isRoom,
3185
- eventScope
3186
- });
3187
- const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";
3188
- const replyToMode = channelConfig?.replyToMode ?? resolveSlackReplyToMode(account, chatType);
3189
- const threadContext = resolveSlackThreadContext({
3190
- message,
3191
- replyToMode,
3192
- isDirectMessage
3193
- });
3194
- const threadTs = threadContext.incomingThreadTs;
3195
- const isThreadReply = threadContext.isThreadReply;
3196
- const autoThreadId = !isThreadReply && replyToMode === "all" && threadContext.messageTs ? threadContext.messageTs : void 0;
3197
- const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
3198
- const routedThreadId = (isDirectMessage ? assistantThreadTs ?? agentViewThreadTs : isRoomish ? isThreadReply && threadTs ? threadTs : void 0 : isThreadReply ? threadTs : autoThreadId) ?? (isRoomish ? !isThreadReply && isRoom && seedTopLevelRoomThread && replyToMode !== "off" && seedCandidateThreadId ? seedCandidateThreadId : void 0 : void 0);
3199
- const baseConversationId = resolveSlackBaseConversationId({
3200
- message,
3201
- isDirectMessage,
3202
- eventScope
3203
- });
3204
- const runtimeBindingThreadId = routedThreadId ?? (isDirectMessage && isThreadReply ? threadTs : void 0);
3205
- const bindingRoute = resolveSlackConversationBindingRoute({
3206
- cfg: ctx.cfg,
3207
- route,
3208
- accountId: account.accountId,
3209
- baseConversationId,
3210
- runtimeBindingThreadId,
3211
- bindingsEnabled: !eventScope
3212
- });
3213
- const runtimeRoute = bindingRoute.runtimeRoute;
3214
- const configuredBinding = bindingRoute.configuredRoute?.bindingResolution ?? null;
3215
- const configuredBindingSessionKey = bindingRoute.configuredRoute?.boundSessionKey ?? "";
3216
- route = bindingRoute.route;
3217
- const threadKeys = runtimeRoute.boundSessionKey || configuredBindingSessionKey ? {
3218
- sessionKey: route.sessionKey,
3219
- parentSessionKey: void 0
3220
- } : resolveThreadSessionKeys({
3221
- baseSessionKey: route.sessionKey,
3222
- threadId: routedThreadId,
3223
- parentSessionKey: routedThreadId && ctx.threadInheritParent ? route.sessionKey : void 0
3224
- });
3225
- const sessionKey = threadKeys.sessionKey;
3226
- const historyKey = isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : eventScope ? `${account.accountId}:${eventScope.teamId}:${message.channel}` : message.channel;
3227
- return {
3228
- route,
3229
- runtimeBinding: runtimeRoute.bindingRecord,
3230
- runtimeBoundSessionKey: runtimeRoute.boundSessionKey,
3231
- configuredBinding,
3232
- configuredBindingSessionKey,
3233
- chatType,
3234
- replyToMode,
3235
- threadContext,
3236
- threadTs,
3237
- isThreadReply,
3238
- threadKeys,
3239
- sessionKey,
3240
- historyKey
3241
- };
3242
- }
3243
- //#endregion
3244
2954
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context-root.ts
3245
2955
  function isSlackThreadAuthorCurrentBot(params) {
3246
2956
  const { identity, author } = params;
@@ -3296,7 +3006,7 @@ function formatSlackThreadLabelSnippet(text) {
3296
3006
  }
3297
3007
  //#endregion
3298
3008
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
3299
- const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-XdQyjG_p.js").then((n) => n._));
3009
+ const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-CFkvEv1d.js").then((n) => n._));
3300
3010
  const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
3301
3011
  function resolveSlackThreadSessionFreshness(params) {
3302
3012
  return params.ctx.channelRuntime?.session?.resolveEntryResetFreshness?.({
@@ -3408,14 +3118,24 @@ async function resolveSlackThreadContextData(params) {
3408
3118
  const snippet = formatSlackThreadLabelSnippet(starter.text);
3409
3119
  threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`;
3410
3120
  if (shouldSeedInitialThreadContext && !params.effectiveDirectMedia && starter.files && starter.files.length > 0) {
3411
- const { resolveSlackMedia } = await loadSlackMediaModule();
3412
- threadStarterMedia = await resolveSlackMedia({
3121
+ const { resolveSlackAttachmentContent } = await loadSlackMediaModule();
3122
+ const attachmentContent = await resolveSlackAttachmentContent({
3413
3123
  files: starter.files,
3414
3124
  client: params.eventScope?.client ?? params.ctx.app.client,
3415
3125
  token: params.ctx.botToken,
3416
3126
  maxBytes: params.ctx.mediaMaxBytes
3417
3127
  });
3418
- if (threadStarterMedia) logVerbose(`slack: hydrated thread starter file ${threadStarterMedia.map((item) => item.placeholder).join(", ")} from root message`);
3128
+ threadStarterMedia = attachmentContent?.media.length ? attachmentContent.media : null;
3129
+ if (attachmentContent) threadStarterBody = formatSlackUnavailableMedia({
3130
+ body: threadStarterBody,
3131
+ files: attachmentContent.files,
3132
+ unavailableMediaCount: attachmentContent.unavailableMediaCount,
3133
+ prependUnavailable: true
3134
+ });
3135
+ if (threadStarterMedia) {
3136
+ const starterPlaceholders = threadStarterMedia.map((item) => item.placeholder).join(", ");
3137
+ logVerbose(`slack: hydrated thread starter file ${starterPlaceholders} from root message`);
3138
+ }
3419
3139
  }
3420
3140
  } else threadLabel = `Slack thread ${params.roomLabel}`;
3421
3141
  const includeBotStarterAsRootContext = shouldIncludeBotThreadStarterContext({
@@ -3435,14 +3155,20 @@ async function resolveSlackThreadContextData(params) {
3435
3155
  const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20;
3436
3156
  if (threadInitialHistoryLimit > 0 && shouldLoadInitialThreadHistory) {
3437
3157
  const currentBotRootTs = starter?.ts ?? params.threadTs;
3158
+ const threadHistory = await resolveSlackThreadHistory({
3159
+ channelId: params.message.channel,
3160
+ threadTs: params.threadTs,
3161
+ client: params.eventScope?.client ?? params.ctx.app.client,
3162
+ currentMessageTs: params.message.ts,
3163
+ limit: threadInitialHistoryLimit
3164
+ });
3165
+ const enrichedStarter = starter && threadStarterBody && threadStarterBody !== starter.text ? {
3166
+ ...starter,
3167
+ text: threadStarterBody,
3168
+ ts: currentBotRootTs
3169
+ } : null;
3438
3170
  const threadHistoryWithBotRoot = ensureSlackThreadHistoryHasBotRoot({
3439
- history: await resolveSlackThreadHistory({
3440
- channelId: params.message.channel,
3441
- threadTs: params.threadTs,
3442
- client: params.eventScope?.client ?? params.ctx.app.client,
3443
- currentMessageTs: params.message.ts,
3444
- limit: threadInitialHistoryLimit
3445
- }),
3171
+ history: enrichedStarter && !threadHistory.some((entry) => entry.ts === currentBotRootTs) ? [enrichedStarter, ...threadHistory.length >= threadInitialHistoryLimit ? threadHistory.slice(1) : threadHistory] : threadHistory,
3446
3172
  includeBotStarterAsRootContext,
3447
3173
  threadStarter: starter ? {
3448
3174
  ...starter,
@@ -3501,7 +3227,7 @@ async function resolveSlackThreadContextData(params) {
3501
3227
  });
3502
3228
  const role = isCurrentBot || isOtherBot || Boolean(historyMsg.botId) ? "assistant" : "user";
3503
3229
  const msgSenderName = isCurrentBot ? "Bot (this assistant)" : msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown");
3504
- const msgWithId = `${historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${params.message.channel}]`;
3230
+ const msgWithId = `${historyMsg.ts === currentBotRootTs && threadStarterBody ? threadStarterBody : historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${params.message.channel}]`;
3505
3231
  historyParts.push(formatInboundEnvelope({
3506
3232
  channel: "Slack",
3507
3233
  from: `${msgSenderName} (${role})`,
@@ -3528,7 +3254,7 @@ async function resolveSlackThreadContextData(params) {
3528
3254
  //#endregion
3529
3255
  //#region extensions/slack/src/monitor/message-handler/subteam-mentions.ts
3530
3256
  const SUBTEAM_MENTION_RE = /<!subteam\^([A-Z0-9]+)(?:\|[^>]*)?>/gi;
3531
- const SUBTEAM_MEMBER_CACHE_TTL_MS = 300 * 1e3;
3257
+ const SUBTEAM_MEMBER_CACHE_TTL_MS = 3e5;
3532
3258
  const subteamMemberCache = /* @__PURE__ */ new WeakMap();
3533
3259
  function normalizeSlackId(value) {
3534
3260
  return typeof value === "string" && value.trim() ? value.trim().toUpperCase() : void 0;
@@ -3599,7 +3325,7 @@ const SLACK_USER_MENTION_RE = /<@([^>|]+)(?:\|[^>]+)?>/g;
3599
3325
  const SLACK_SUBTEAM_MENTION_RE = /<!subteam\^([^>|]+)(?:\|[^>]+)?>/g;
3600
3326
  const SLACK_SUBTEAM_MENTION_MARKER = "<!subteam^";
3601
3327
  const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4;
3602
- const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
3328
+ const SLACK_HISTORY_MEDIA_MAX_BYTES = 10485760;
3603
3329
  const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1e3;
3604
3330
  const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3e3;
3605
3331
  const SLACK_CHANNEL_ACCESS_DOCS_URL = "https://docs.openclaw.ai/channels/slack#access-control-and-routing";
@@ -3653,25 +3379,12 @@ async function restoreSlackAssistantThreadContextFromMetadata(params) {
3653
3379
  const parentUserId = params.message.parent_user_id?.trim();
3654
3380
  if (!params.message.channel || !threadTs || !parentUserId || parentUserId !== params.ctx.botUserId && parentUserId !== params.ctx.botId) return;
3655
3381
  try {
3656
- const response = await (params.eventScope?.client ?? params.ctx.app.client).conversations.replies({
3657
- channel: params.message.channel,
3658
- ts: threadTs,
3659
- oldest: threadTs,
3660
- include_all_metadata: true,
3661
- limit: 4
3382
+ return await readSlackAssistantThreadContext({
3383
+ client: params.eventScope?.client ?? params.ctx.app.client,
3384
+ channelId: params.message.channel,
3385
+ threadTs,
3386
+ userId: params.message.user
3662
3387
  });
3663
- for (const message of response.messages ?? []) {
3664
- const context = parseSlackAssistantThreadMetadata(message.metadata);
3665
- if (!context) continue;
3666
- return {
3667
- assistantChannelId: params.message.channel,
3668
- threadTs,
3669
- userId: params.message.user,
3670
- channelId: context.channelId,
3671
- teamId: context.teamId,
3672
- enterpriseId: context.enterpriseId
3673
- };
3674
- }
3675
3388
  } catch (err) {
3676
3389
  logVerbose(`slack assistant context restore failed channel=${params.message.channel} ts=${threadTs}: ${formatErrorMessage(err)}`);
3677
3390
  }
@@ -3733,7 +3446,7 @@ function buildSlackHistoryMediaCandidateMessage(message) {
3733
3446
  async function resolveSlackHistoryMediaForPendingRecord(params) {
3734
3447
  const mediaMessage = buildSlackHistoryMediaCandidateMessage(params.message);
3735
3448
  if (!mediaMessage) return [];
3736
- return await toInboundMediaFactsWithMetadata((await resolveSlackMessageContent({
3449
+ const content = await resolveSlackMessageContent({
3737
3450
  message: mediaMessage,
3738
3451
  isThreadReply: params.isThreadReply,
3739
3452
  threadStarter: params.threadStarter,
@@ -3743,7 +3456,8 @@ async function resolveSlackHistoryMediaForPendingRecord(params) {
3743
3456
  mediaMaxBytes: Math.min(params.ctx.mediaMaxBytes, SLACK_HISTORY_MEDIA_MAX_BYTES),
3744
3457
  mediaReadIdleTimeoutMs: SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS,
3745
3458
  mediaTotalTimeoutMs: SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS
3746
- }))?.effectiveDirectMedia, {
3459
+ });
3460
+ return await toInboundMediaFactsWithMetadata(content?.effectiveDirectMedia, {
3747
3461
  kind: "image",
3748
3462
  messageId: params.message.ts
3749
3463
  });
@@ -3839,24 +3553,15 @@ async function resolveSlackConversationContext(params) {
3839
3553
  };
3840
3554
  }
3841
3555
  async function authorizeSlackInboundMessage(params) {
3842
- const { ctx, account, message, conversation } = params;
3556
+ const { ctx, account, message, conversation, drop } = params;
3843
3557
  const { isDirectMessage, channelName, resolvedChannelType, isBotMessage, allowBotsMode } = conversation;
3844
3558
  if (isBotMessage) {
3845
3559
  if (message.user && ctx.botUserId && message.user === ctx.botUserId) return null;
3846
- if (allowBotsMode === "off") {
3847
- logVerbose(`slack: drop bot message ${message.bot_id ?? "unknown"} (allowBots=false)`);
3848
- return null;
3849
- }
3850
- }
3851
- if (isDirectMessage && !message.user) {
3852
- logVerbose("slack: drop dm message (missing user id)");
3853
- return null;
3560
+ if (allowBotsMode === "off") return drop("bot-disabled");
3854
3561
  }
3562
+ if (isDirectMessage && !message.user) return drop("missing-user");
3855
3563
  const senderId = message.user ?? (isBotMessage ? message.bot_id : void 0);
3856
- if (!senderId) {
3857
- logVerbose("slack: drop message (missing sender id)");
3858
- return null;
3859
- }
3564
+ if (!senderId) return drop("missing-sender");
3860
3565
  if (!ctx.isChannelAllowed({
3861
3566
  teamId: params.eventScope?.teamId ?? ctx.teamId,
3862
3567
  channelId: message.channel,
@@ -3866,7 +3571,8 @@ async function authorizeSlackInboundMessage(params) {
3866
3571
  if (conversation.isRoom && ctx.groupPolicy === "allowlist" && params.explicitBotMention && !isBotMessage && message.user) {
3867
3572
  let subject = "This OpenClaw bot";
3868
3573
  if (ctx.botUserId) try {
3869
- const botName = normalizeOptionalString((await ctx.resolveUserName(ctx.botUserId, params.eventScope))?.name);
3574
+ const botIdentity = await ctx.resolveUserName(ctx.botUserId, params.eventScope);
3575
+ const botName = normalizeOptionalString(botIdentity?.name);
3870
3576
  if (botName) subject = escapeSlackMrkdwn(botName);
3871
3577
  } catch (error) {
3872
3578
  logVerbose(`slack allowlist denial bot-name lookup failed: ${formatSlackError(error)}`);
@@ -3883,8 +3589,7 @@ async function authorizeSlackInboundMessage(params) {
3883
3589
  ctx.runtime.error?.(`slack allowlist denial notice failed for channel ${message.channel}: ${formatSlackError(error)}`);
3884
3590
  }
3885
3591
  }
3886
- logVerbose("slack: drop message (channel not allowed)");
3887
- return null;
3592
+ return drop("channel-not-allowed");
3888
3593
  }
3889
3594
  const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, {
3890
3595
  includePairingStore: isDirectMessage,
@@ -3892,10 +3597,8 @@ async function authorizeSlackInboundMessage(params) {
3892
3597
  });
3893
3598
  if (isDirectMessage) {
3894
3599
  const directUserId = message.user;
3895
- if (!directUserId) {
3896
- logVerbose("slack: drop dm message (missing user id)");
3897
- return null;
3898
- }
3600
+ if (!directUserId) return drop("missing-user");
3601
+ let dropReason = "dm-unauthorized";
3899
3602
  if (!await authorizeSlackDirectMessage({
3900
3603
  ctx,
3901
3604
  accountId: account.accountId,
@@ -3913,13 +3616,13 @@ async function authorizeSlackInboundMessage(params) {
3913
3616
  });
3914
3617
  },
3915
3618
  onDisabled: () => {
3916
- logVerbose("slack: drop dm (dms disabled)");
3619
+ dropReason = "dm-disabled";
3917
3620
  },
3918
3621
  onUnauthorized: ({ allowMatchMeta }) => {
3919
3622
  logVerbose(`Blocked unauthorized slack sender ${message.user} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`);
3920
3623
  },
3921
3624
  log: logVerbose
3922
- })) return null;
3625
+ })) return drop(dropReason);
3923
3626
  }
3924
3627
  return {
3925
3628
  senderId,
@@ -3928,6 +3631,19 @@ async function authorizeSlackInboundMessage(params) {
3928
3631
  }
3929
3632
  async function prepareSlackMessage(params) {
3930
3633
  const { ctx, account, message, opts } = params;
3634
+ const drop = (reason, parentUserId) => {
3635
+ ctx.logger.info({
3636
+ provider: "slack",
3637
+ accountId: account.accountId,
3638
+ teamId: opts.eventScope?.teamId ?? ctx.teamId,
3639
+ channelId: message.channel,
3640
+ messageTs: message.ts,
3641
+ source: opts.source,
3642
+ reason,
3643
+ ...parentUserId ? { parentUserId } : {}
3644
+ }, "Slack inbound event rejected during preparation");
3645
+ return null;
3646
+ };
3931
3647
  const slackClient = opts.eventScope?.client ?? ctx.app.client;
3932
3648
  const threadStarterWorkspaceScope = {
3933
3649
  accountId: account.accountId,
@@ -3951,7 +3667,8 @@ async function prepareSlackMessage(params) {
3951
3667
  conversation,
3952
3668
  explicitBotMention: opts.source === "app_mention" || Boolean(normalizedBotUserId && mentionMetadata.mentionedUserIds.includes(normalizedBotUserId)),
3953
3669
  eventScope: opts.eventScope,
3954
- onVisibleDrop: opts.onVisibleDrop
3670
+ onVisibleDrop: opts.onVisibleDrop,
3671
+ drop
3955
3672
  });
3956
3673
  if (!authorization) return null;
3957
3674
  const { senderId, allowFromLower } = authorization;
@@ -3959,7 +3676,8 @@ async function prepareSlackMessage(params) {
3959
3676
  const resolveSenderName = async () => {
3960
3677
  if (resolvedSenderName) return resolvedSenderName;
3961
3678
  if (message.user) {
3962
- const normalized = normalizeOptionalString((await ctx.resolveUserName(message.user, opts.eventScope))?.name);
3679
+ const sender = await ctx.resolveUserName(message.user, opts.eventScope);
3680
+ const normalized = normalizeOptionalString(sender?.name);
3963
3681
  if (normalized) {
3964
3682
  resolvedSenderName = normalized;
3965
3683
  return resolvedSenderName;
@@ -4055,13 +3773,7 @@ async function prepareSlackMessage(params) {
4055
3773
  if (shouldLogVerbose()) logVerbose(`slack: using configured ACP binding for ${configuredBinding.record.conversation.conversationId} -> ${configuredBindingSessionKey}`);
4056
3774
  } else {
4057
3775
  if (shouldLogVerbose()) logVerbose(`slack: configured ACP binding unavailable for ${configuredBinding.record.conversation.conversationId}: ${ensured.error}`);
4058
- logInboundDrop({
4059
- log: logVerbose,
4060
- channel: "slack",
4061
- reason: "configured ACP binding unavailable",
4062
- target: configuredBinding.record.conversation.conversationId
4063
- });
4064
- return null;
3776
+ return drop("configured-binding-unavailable");
4065
3777
  }
4066
3778
  }
4067
3779
  const senderNameForAuthPromise = ctx.allowNameMatching ? resolveSenderName().then((name) => ({
@@ -4176,14 +3888,7 @@ async function prepareSlackMessage(params) {
4176
3888
  channel: "slack",
4177
3889
  accountId: account.accountId
4178
3890
  });
4179
- if (message["_ambiguousThreadReply"]) {
4180
- ctx.logger.info({
4181
- channel: message.channel,
4182
- ts: message.ts,
4183
- parentUserId: message.parent_user_id
4184
- }, "skipping ambiguous slack thread reply");
4185
- return null;
4186
- }
3891
+ if (message["_ambiguousThreadReply"]) return drop("ambiguous-thread", message.parent_user_id);
4187
3892
  let canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0;
4188
3893
  const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? "");
4189
3894
  const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
@@ -4216,10 +3921,7 @@ async function prepareSlackMessage(params) {
4216
3921
  });
4217
3922
  let messageIngress = await resolveMessageIngress();
4218
3923
  const senderGate = messageIngress.senderAccess.gate;
4219
- if (isRoomish && senderGate?.allowed === false) {
4220
- logVerbose(`Blocked unauthorized slack sender ${senderId} (not in sender allowlist)`);
4221
- return null;
4222
- }
3924
+ if (isRoomish && senderGate?.allowed === false) return drop("unauthorized-sender");
4223
3925
  if (isRoom && isBotMessage && allowBotsMode !== "off" && !await authorizeSlackBotRoomMessage({
4224
3926
  ctx,
4225
3927
  channelId: message.channel,
@@ -4228,7 +3930,7 @@ async function prepareSlackMessage(params) {
4228
3930
  channelUsers: channelConfig?.users,
4229
3931
  allowFromLower,
4230
3932
  eventScope: opts.eventScope
4231
- })) return null;
3933
+ })) return drop("unauthorized-bot");
4232
3934
  const threadContextAllowFromLower = isRoom ? channelUsersAllowlistConfigured ? normalizeAllowListLower(channelConfig?.users) : [] : isDirectMessage ? allowFromLower : [];
4233
3935
  const contextVisibilityMode = resolveChannelContextVisibilityMode({
4234
3936
  cfg: ctx.cfg,
@@ -4248,15 +3950,7 @@ async function prepareSlackMessage(params) {
4248
3950
  explicitKind: true
4249
3951
  });
4250
3952
  const commandAuthorized = messageIngress.commandAccess.authorized;
4251
- if (isRoomish && messageIngress.commandAccess.shouldBlockControlCommand) {
4252
- logInboundDrop({
4253
- log: logVerbose,
4254
- channel: "slack",
4255
- reason: "control command (unauthorized)",
4256
- target: senderId
4257
- });
4258
- return null;
4259
- }
3953
+ if (isRoomish && messageIngress.commandAccess.shouldBlockControlCommand) return drop("control-command-unauthorized");
4260
3954
  const canSeedMentionedRoomThread = !seedTopLevelRoomThreadBySource && isRoom && !routing.isThreadReply && !hasBoundSession;
4261
3955
  let seededMentionRouting;
4262
3956
  const getSeededMentionRouting = () => {
@@ -4351,37 +4045,20 @@ async function prepareSlackMessage(params) {
4351
4045
  wasMentioned
4352
4046
  });
4353
4047
  if (isBotMessage && allowBotsMode === "mentions") {
4354
- if (!(isDirectMessage || effectiveWasMentioned || shouldBypassMention)) {
4355
- logVerbose("slack: drop bot message (allowBots=mentions, missing mention)");
4356
- return null;
4357
- }
4048
+ if (!(isDirectMessage || effectiveWasMentioned || shouldBypassMention)) return drop("bot-missing-mention");
4358
4049
  }
4359
4050
  if (isRoom && shouldRequireMention && !canDetectMention && !effectiveWasMentioned) {
4360
- ctx.logger.info({
4361
- channel: message.channel,
4362
- reason: "mention-detection-unavailable"
4363
- }, "skipping channel message");
4364
4051
  await recordDroppedHistory("slack-mention-detection-unavailable");
4365
- return null;
4052
+ return drop("mention-detection-unavailable");
4366
4053
  }
4367
4054
  const ignoreOtherMentions = channelConfig?.ignoreOtherMentions ?? false;
4368
4055
  if (isRoom && ignoreOtherMentions && Boolean(ctx.botUserId) && hasAnyMention && !wasMentioned) {
4369
- logInboundDrop({
4370
- log: logVerbose,
4371
- channel: "slack",
4372
- reason: "other-mention",
4373
- target: senderId
4374
- });
4375
4056
  await recordDroppedHistory("slack-other-mention");
4376
- return null;
4057
+ return drop("other-mention");
4377
4058
  }
4378
4059
  if (isRoom && shouldRequireMention && mentionDecision.shouldSkip) {
4379
- ctx.logger.info({
4380
- channel: message.channel,
4381
- reason: "no-mention"
4382
- }, "skipping channel message");
4383
4060
  await recordDroppedHistory("slack-no-mention");
4384
- return null;
4061
+ return drop("missing-mention");
4385
4062
  }
4386
4063
  const chatType = resolveSlackChatType(conversation.resolvedChannelType);
4387
4064
  const inboundEventKind = classifyChannelInboundEvent({
@@ -4396,7 +4073,7 @@ async function prepareSlackMessage(params) {
4396
4073
  });
4397
4074
  const threadStarter = await getThreadStarter();
4398
4075
  const resolvedMessageContent = await getMessageContent();
4399
- if (!resolvedMessageContent) return null;
4076
+ if (!resolvedMessageContent) return drop("empty-content");
4400
4077
  const { rawBody, effectiveDirectMedia } = resolvedMessageContent;
4401
4078
  const bodyForAgent = preflightAudioTranscript ? formatSlackAudioTranscriptForAgent({
4402
4079
  transcript: preflightAudioTranscript,
@@ -4417,7 +4094,7 @@ async function prepareSlackMessage(params) {
4417
4094
  const statusReactionsExplicitlyEnabled = cfg.messages?.statusReactions?.enabled === true;
4418
4095
  const isRoomEvent = inboundEventKind === "room_event";
4419
4096
  const shouldAckReaction$1 = () => Boolean(ackReaction && shouldAckReaction({
4420
- scope: ctx.ackReactionScope,
4097
+ scope: cfg.messages?.ackReactionScope,
4421
4098
  inboundEventKind,
4422
4099
  isDirect: isDirectMessage,
4423
4100
  isGroup: isRoomish,
@@ -4465,10 +4142,11 @@ async function prepareSlackMessage(params) {
4465
4142
  const textWithId = `${bodyForAgent}\n[slack message id: ${message.ts} channel: ${message.channel}${threadInfo}]`;
4466
4143
  const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId });
4467
4144
  const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
4468
- const previousTimestamp = readSessionUpdatedAt({
4145
+ const sessionEntry = getSessionEntry({
4469
4146
  storePath,
4470
4147
  sessionKey
4471
4148
  });
4149
+ const previousTimestamp = sessionEntry?.updatedAt;
4472
4150
  if (opts.source === "app_mention" && !ctx.botUserId && message.ts) {
4473
4151
  const pendingHistory = ctx.channelHistories.get(historyKey);
4474
4152
  if (pendingHistory) ctx.channelHistories.set(historyKey, pendingHistory.filter((entry) => entry.messageId !== message.ts));
@@ -4559,10 +4237,7 @@ async function prepareSlackMessage(params) {
4559
4237
  messageId: threadContext.messageTs,
4560
4238
  inboundEventKind
4561
4239
  }, boundMessageThreadId);
4562
- if (messageIngress.ingress.admission !== "dispatch") {
4563
- logVerbose(`Blocked slack sender ${senderId} after final route binding`);
4564
- return null;
4565
- }
4240
+ if (messageIngress.ingress.admission !== "dispatch") return drop("final-route-denied");
4566
4241
  const agentContextEntities = isAgentViewMessage ? normalizeSlackAppContextEntities(message.app_context) : [];
4567
4242
  const ctxPayload = (ctx.buildContext ?? buildChannelInboundEventContext)({
4568
4243
  channelIngress: messageIngress,
@@ -4647,6 +4322,7 @@ async function prepareSlackMessage(params) {
4647
4322
  payload: { entities: agentContextEntities }
4648
4323
  }] : void 0,
4649
4324
  TransportThreadId: directThreadRoutedToDmSession ? threadContext.messageThreadId : void 0,
4325
+ ReplyThreading: isThreadReply ? { implicitCurrentMessage: "deny" } : void 0,
4650
4326
  SlackAssistantThread: assistantThreadContext ? true : void 0,
4651
4327
  SlackAgentThread: agentViewThreadTs ? true : void 0,
4652
4328
  SlackAssistantThreadContextChannelId: assistantThreadContext?.channelId,
@@ -4682,7 +4358,6 @@ async function prepareSlackMessage(params) {
4682
4358
  normalizeEntry: normalizeSlackAllowOwnerEntry
4683
4359
  }) : null;
4684
4360
  const replyTarget = `channel:${message.channel}`;
4685
- if (!replyTarget) return null;
4686
4361
  if (preflightAudioTranscript) await sendSlackPreflightAudioTranscriptEcho({
4687
4362
  transcript: preflightAudioTranscript,
4688
4363
  cfg,
@@ -4705,6 +4380,7 @@ async function prepareSlackMessage(params) {
4705
4380
  channelConfig,
4706
4381
  replyTarget,
4707
4382
  ctxPayload,
4383
+ sessionDisplayName: sessionEntry?.displayName,
4708
4384
  turn: {
4709
4385
  storePath,
4710
4386
  record: {