@openclaw/slack 2026.9.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 (70) hide show
  1. package/dist/{action-runtime-DND4Pyp2.js → action-runtime-DftQWpI4.js} +4 -4
  2. package/dist/action-runtime.runtime-rHI3UAh4.js +2 -0
  3. package/dist/{actions-fh66A6lc.js → actions-CFkvEv1d.js} +3 -3
  4. package/dist/api.js +9 -9
  5. package/dist/{approval-handler.runtime-D7d8sKM0.js → approval-handler.runtime-BeILKdw4.js} +31 -15
  6. package/dist/{channel-4m8IAdMD.js → channel-CmOuxbOD.js} +24 -23
  7. package/dist/channel-config-api.js +1 -1
  8. package/dist/channel-plugin-api.js +1 -1
  9. package/dist/{channel-type-3-SqEMVc.js → channel-type-BWt2CRaI.js} +2 -2
  10. package/dist/{channel.setup-t_ghLGb_.js → channel.setup-D5z1ReaT.js} +8 -3
  11. package/dist/{config-schema-bP3eV2ra.js → config-schema-JDx_f9Zb.js} +2 -2
  12. package/dist/{directory-live-q32AW7N1.js → directory-live-BuBPHmju.js} +1 -1
  13. package/dist/{monitor-Chwu_SiZ.js → monitor-DI07pfJl.js} +3 -3
  14. package/dist/{outbound-adapter-BB8wAjm5.js → outbound-adapter-B5QyTRpJ.js} +3 -2
  15. package/dist/{pipeline.runtime-BklS_NEj.js → pipeline.runtime-BdIwZgm4.js} +567 -733
  16. package/dist/{policy-D7PKkiet.js → policy-Bh5TOqII.js} +2 -2
  17. package/dist/{probe-gj5yQgN2.js → probe-Biy--yhL.js} +42 -10
  18. package/dist/{provider-Do50wMWn.js → provider-DoaUvAM4.js} +5583 -4921
  19. package/dist/{relay-source-DWuduiJ2.js → relay-source-D4uecp68.js} +1 -1
  20. package/dist/{replies-Dc6WVh_Q.js → replies-DENuTcPO.js} +4 -3
  21. package/dist/reply-blocks-PRkhyAN7.js +790 -0
  22. package/dist/{resolve-channels-CU7Bg5Ka.js → resolve-channels-DQGV2VrE.js} +1 -1
  23. package/dist/{resolve-users-DrhttvRQ.js → resolve-users-DRGt_Q4I.js} +1 -1
  24. package/dist/runtime-api.js +10 -10
  25. package/dist/{scopes-B9HRguDe.js → scopes-CZzeGdWV.js} +1 -1
  26. package/dist/{send-Djswy6lf.js → send-DElUcgIs.js} +10 -10
  27. package/dist/{send.runtime-__WVTWLQ.js → send.runtime-C8D6cKGm.js} +1 -1
  28. package/dist/{group-policy-BKjMqT-8.js → session-status-BynwcZSO.js} +169 -799
  29. package/dist/setup-plugin-api.js +1 -1
  30. package/dist/{setup-surface-DO0e-MMS.js → setup-surface-B1_gpGBl.js} +4 -4
  31. package/dist/{shared-DEQBEKBL.js → shared-CNQsWJT7.js} +2 -2
  32. package/dist/{slash-dispatch.runtime-C3B4h5ZM.js → slash-dispatch.runtime-DhsXavSY.js} +1 -1
  33. package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
  34. package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
  35. package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
  36. package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
  37. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  38. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  39. package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
  40. package/node_modules/typebox/build/system/memory/create.mjs +3 -4
  41. package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
  42. package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
  43. package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
  44. package/node_modules/typebox/build/system/memory/update.mjs +3 -2
  45. package/node_modules/typebox/package.json +1 -1
  46. package/node_modules/undici/lib/dispatcher/balanced-pool.js +4 -2
  47. package/node_modules/undici/lib/dispatcher/client-h1.js +12 -4
  48. package/node_modules/undici/lib/dispatcher/client-h2.js +70 -14
  49. package/node_modules/undici/lib/dispatcher/client.js +6 -2
  50. package/node_modules/undici/lib/handler/cache-handler.js +21 -5
  51. package/node_modules/undici/lib/handler/retry-handler.js +42 -7
  52. package/node_modules/undici/lib/interceptor/cache.js +20 -1
  53. package/node_modules/undici/lib/interceptor/decompress.js +146 -14
  54. package/node_modules/undici/lib/interceptor/dump.js +10 -23
  55. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +245 -150
  56. package/node_modules/undici/lib/web/websocket/connection.js +1 -1
  57. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
  58. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -10
  59. package/node_modules/undici/package.json +1 -1
  60. package/node_modules/undici/types/interceptors.d.ts +2 -0
  61. package/openclaw.plugin.json +3 -3
  62. package/package.json +6 -6
  63. package/skills/_vendor/slack-skills-plugin.LICENSE +21 -0
  64. package/skills/_vendor/slack-skills-plugin.json +31 -0
  65. package/skills/block-kit/SKILL.md +37 -0
  66. package/skills/block-kit/references/official-block-kit.md +257 -0
  67. package/skills/block-kit/references/official-common-patterns.md +385 -0
  68. package/skills/slack/SKILL.md +1 -1
  69. package/dist/action-runtime.runtime-CEhbmS-Z.js +0 -2
  70. package/dist/conversation-binding-route-Vee8Dgbf.js +0 -115
@@ -1,23 +1,24 @@
1
- import { n as resolveSlackConversationBindingRoute, r as resolveSlackReplyToMode, t as normalizeSlackRouteBindingConfig } from "./conversation-binding-route-Vee8Dgbf.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
2
  import { n as formatSlackTarget } from "./target-parsing-PbxGDbr9.js";
3
- import { I as countSlackTextUtf8Bytes, L as truncateSlackText, M as SLACK_TEXT_LIMIT, Q as normalizeSlackOutboundText, R as truncateSlackTextByUtf8Bytes, T as buildSlackBlocksFallbackText, U as SLACK_SESSION_LINK_ACTION_ID, a as qualifySlackConversationId, b as hasSlackNativeDataBlock, et as escapeSlackMrkdwn, k as SLACK_EDIT_TEXT_MAX_BYTES, m as resolveSlackReplyRenderPlan, o as qualifySlackRoutePeerId, s as resolveSlackEnterpriseMainDmSessionKey } from "./group-policy-BKjMqT-8.js";
4
- import { C as normalizeAllowListLower, E as resolveSlackAllowListMatch, o as formatSlackError, w as normalizeSlackAllowOwnerEntry } from "./probe-gj5yQgN2.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
5
  import { n as resolveSlackReplyThreadTs } from "./thread-ts-CukseRVL.js";
6
6
  import { n as resolveSlackStreamingMode, t as resolveSlackNativeStreaming } from "./streaming-compat-B-e0mqM0.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-fh66A6lc.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-Djswy6lf.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-Do50wMWn.js";
10
- import { n as resolveSlackChannelConfig } from "./policy-D7PKkiet.js";
11
- import { a as resolveSlackThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as sanitizeSlackMonitorReplyPayload, s as emitSlackMessageSentHooks, t as createSlackReplyDeliveryPlan } from "./replies-Dc6WVh_Q.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 { createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createDraftStreamLoop, createFinalizableDraftStreamControlsForState, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, 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
18
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
19
19
  import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, isReplyPayloadNonTerminalToolErrorWarning, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
20
20
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
21
+ import { createHash } from "node:crypto";
21
22
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
22
23
  import { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversation-runtime";
23
24
  import { resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
@@ -28,11 +29,11 @@ import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist,
28
29
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
29
30
  import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
30
31
  import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
31
- import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
32
32
  import { asDateTimestampMs, asFiniteNumberInRange, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
33
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
34
+ import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
33
35
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
34
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";
35
- import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
36
37
  import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
37
38
  import { buildControlUiSessionPath } from "openclaw/plugin-sdk/session-discussion";
38
39
  import { createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
@@ -40,186 +41,6 @@ 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 ? { 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 ? { 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;
@@ -574,11 +395,65 @@ function createSlackDraftStream(params) {
574
395
  };
575
396
  }
576
397
  //#endregion
398
+ //#region extensions/slack/src/stream-mode.ts
399
+ function resolveSlackStreamingConfig(params) {
400
+ return {
401
+ mode: resolveSlackStreamingMode(params),
402
+ nativeStreaming: resolveSlackNativeStreaming(params)
403
+ };
404
+ }
405
+ function applyAppendOnlyStreamUpdate(params) {
406
+ const incoming = params.incoming.trimEnd();
407
+ if (!incoming) return {
408
+ rendered: params.rendered,
409
+ source: params.source,
410
+ changed: false
411
+ };
412
+ if (!params.rendered) return {
413
+ rendered: incoming,
414
+ source: incoming,
415
+ changed: true
416
+ };
417
+ if (incoming === params.source) return {
418
+ rendered: params.rendered,
419
+ source: params.source,
420
+ changed: false
421
+ };
422
+ if (incoming.startsWith(params.rendered)) return {
423
+ rendered: incoming,
424
+ source: incoming,
425
+ changed: incoming !== params.rendered
426
+ };
427
+ if (incoming.startsWith(params.source)) {
428
+ const delta = incoming.slice(params.source.length);
429
+ return {
430
+ rendered: `${params.rendered}${delta}`,
431
+ source: incoming,
432
+ changed: delta.length > 0
433
+ };
434
+ }
435
+ if (params.source.startsWith(incoming)) return {
436
+ rendered: params.rendered,
437
+ source: params.source,
438
+ changed: false
439
+ };
440
+ const separator = params.separator ?? (params.rendered.endsWith("\n") ? "" : "\n");
441
+ return {
442
+ rendered: `${params.rendered}${separator}${incoming}`,
443
+ source: incoming,
444
+ changed: true
445
+ };
446
+ }
447
+ //#endregion
577
448
  //#region extensions/slack/src/progress-blocks.ts
578
449
  const SLACK_PROGRESS_FIELD_MAX = 1800;
579
450
  const DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS = 120;
451
+ const DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS = 48;
580
452
  const SLACK_PROGRESS_CHUNK_TEXT_MAX = 256;
581
453
  const SLACK_PROGRESS_TASK_TITLE_MAX = 120;
454
+ const SLACK_PROGRESS_PLAN_FALLBACK_TITLE = "Thinking";
455
+ const SLACK_PROGRESS_LINE_DELTA_RE = /(?:^|\s)\+(\d+)\s+[−-](\d+)(?=\s|$)/u;
456
+ const SLACK_ATTENTION_TASK_PREFIX = "openclaw-attention-";
582
457
  function buildSessionSources(url) {
583
458
  return [{
584
459
  type: "url_source",
@@ -586,6 +461,12 @@ function buildSessionSources(url) {
586
461
  text: "Open in OpenClaw"
587
462
  }];
588
463
  }
464
+ function field(text) {
465
+ return {
466
+ type: "mrkdwn",
467
+ text: truncateSlackText(text, SLACK_PROGRESS_FIELD_MAX)
468
+ };
469
+ }
589
470
  function resolveMaxLineChars(value, fallback) {
590
471
  return value && value > 0 ? Math.floor(value) : fallback;
591
472
  }
@@ -604,58 +485,148 @@ function compactTitle(value) {
604
485
  function compactChunkText(value) {
605
486
  return truncateSlackText(value.replace(/\s+/g, " ").trim(), SLACK_PROGRESS_CHUNK_TEXT_MAX);
606
487
  }
607
- function isAuthoredProgressLine(line) {
608
- return line.id === "reasoning" || line.id?.startsWith("commentary:") === true;
609
- }
610
- function resolveProgressAttention(lines) {
611
- const approval = lines.findLast((line) => line.kind === "approval" && line.status === "requested");
612
- if (approval) return {
613
- id: "openclaw_attention",
614
- title: compactTitle(`Approval required: ${approval.detail || approval.label}`),
615
- status: "pending"
616
- };
617
- const failure = lines.findLast((line) => {
618
- const status = line.status?.trim().toLowerCase();
619
- return status === "error" || status === "failed" || status === "failure" || status?.startsWith("exit ") && status !== "exit 0";
488
+ function renderProgressCardText(text, enclosingStyle) {
489
+ return normalizeSlackOutboundText(text, {
490
+ mentions: "escape",
491
+ enclosingStyle
620
492
  });
621
- if (!failure) return;
622
- return {
623
- id: "openclaw_attention",
624
- title: compactTitle([...new Set([
625
- failure.label,
626
- failure.detail,
627
- failure.status
628
- ].filter(Boolean))].join(" — ")),
629
- status: "error"
630
- };
631
493
  }
632
- function buildSlackProgressStreamChunks(params) {
633
- const title = compactChunkText(params.title?.trim() || params.label?.trim() || "Working");
634
- if (!params.title && !params.label && !params.lines.length && !params.plan?.length && !params.sessionUrl) return;
635
- const tasks = params.plan?.length ? params.plan.slice(-50).map((entry, index) => ({
494
+ function lineDetailParts(line) {
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));
496
+ }
497
+ function activityLineDetail(line, maxChars) {
498
+ const detail = lineDetailParts(line).join(" · ");
499
+ if (detail) return escapeSlackMrkdwn(compactDetail(detail, maxChars));
500
+ const text = line.text.replace(/^(?:🧠|💬)\s+/u, "").trim();
501
+ return line.kind === "item" && !line.toolName && text && text !== line.label ? renderProgressCardText(compactDetail(text, maxChars)) : "—";
502
+ }
503
+ function lineTaskTitle(line) {
504
+ const label = line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
505
+ const fallback = line.text.replace(/\s+/g, " ").trim();
506
+ if (fallback && fallback !== label) return compactTitle(lineDetailParts(line).length > 0 || line.status ? label : fallback);
507
+ return compactTitle(label);
508
+ }
509
+ function lineTaskDetails(line, maxLineChars) {
510
+ const detail = line.detail?.replace(SLACK_PROGRESS_LINE_DELTA_RE, "").replace(/\s+·\s*$/u, "").trim();
511
+ return detail && detail !== line.status?.trim() ? compactDetail(detail, maxLineChars) : void 0;
512
+ }
513
+ function lineTaskOutput(line) {
514
+ const match = line.detail ? SLACK_PROGRESS_LINE_DELTA_RE.exec(line.detail) : null;
515
+ if (match) return `+${match[1]} −${match[2]}`;
516
+ const status = line.status?.replace(/\s+/g, " ").trim();
517
+ return status && lineTaskStatus(line) === "error" ? status : void 0;
518
+ }
519
+ function lineTaskStatus(line) {
520
+ const normalized = line.status?.replace(/\s+/g, " ").trim().toLowerCase();
521
+ if (!normalized) return "in_progress";
522
+ if (normalized === "complete" || normalized === "completed" || normalized === "done" || normalized === "ok" || normalized === "success" || normalized === "succeeded" || normalized === "successful" || normalized === "exit 0") return "complete";
523
+ if (normalized === "error" || normalized === "failed" || normalized === "failure" || normalized.startsWith("exit ")) return "error";
524
+ return "in_progress";
525
+ }
526
+ function stableTaskIdPart(value, slugValue = value) {
527
+ const slug = slugValue.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
528
+ const suffix = createHash("sha256").update(value).digest("hex").slice(0, 8);
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) => ({
636
545
  id: `plan_step_${index + 1}`,
637
546
  title: compactTitle(entry.step),
638
547
  status: entry.status === "completed" ? "complete" : entry.status
639
- })) : [{
548
+ }));
549
+ const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS);
550
+ const contentIdOccurrences = /* @__PURE__ */ new Map();
551
+ for (const line of params.lines) {
552
+ const task = {
553
+ id: resolveLineTaskIdentity(line, contentIdOccurrences),
554
+ title: lineTaskTitle(line),
555
+ status: lineTaskStatus(line)
556
+ };
557
+ const details = lineTaskDetails(line, maxLineChars);
558
+ const output = lineTaskOutput(line);
559
+ if (details) task.details = details;
560
+ if (output) task.output = output;
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
+ }];
581
+ });
582
+ }
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;
593
+ }
594
+ function buildSlackProgressStreamChunks(params) {
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"),
598
+ plan: params.plan,
599
+ maxLineChars: params.maxLineChars
600
+ });
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({
640
607
  id: "openclaw_summary",
641
- title: compactTitle(title),
642
- status: "in_progress"
643
- }];
644
- let attention = resolveProgressAttention(params.lines);
645
- if (params.finalInProgressStatus === "error" && !tasks.some((task) => task.status === "in_progress")) attention = {
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({
646
613
  id: "openclaw_attention",
647
- title: attention?.status === "error" ? attention.title : "Failed",
614
+ title: "Failed",
648
615
  status: "error"
649
- };
650
- if (attention && !(params.finalInProgressStatus && attention.status === "pending")) tasks.push(attention);
616
+ });
617
+ if (!headline && tasks.length === 0) return;
651
618
  const finalTaskIndex = tasks.length - 1;
652
619
  const taskChunks = tasks.map((task, index) => {
620
+ const recovered = params.finalInProgressStatus === "complete" && task.status === "error";
653
621
  const chunk = {
654
622
  type: "task_update",
655
623
  id: task.id,
656
- title: task.title,
657
- status: task.status === "in_progress" ? params.finalInProgressStatus ?? 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
658
626
  };
627
+ if (task.details) chunk.details = task.details;
628
+ if (task.output) chunk.output = task.output;
629
+ if (index === finalTaskIndex && diffOutput) chunk.output = [task.output, diffOutput].filter(Boolean).join(" · ");
659
630
  if (index === finalTaskIndex && params.sessionUrl) chunk.sources = buildSessionSources(params.sessionUrl);
660
631
  return chunk;
661
632
  });
@@ -664,30 +635,53 @@ function buildSlackProgressStreamChunks(params) {
664
635
  title
665
636
  }, ...taskChunks];
666
637
  }
638
+ function joinRecentProgressRows(rows) {
639
+ const rendered = [];
640
+ let length = 0;
641
+ for (const row of rows.toReversed()) {
642
+ const nextLength = length + row.length + (rendered.length > 0 ? 1 : 0);
643
+ if (nextLength > SLACK_PROGRESS_FIELD_MAX) break;
644
+ rendered.push(row);
645
+ length = nextLength;
646
+ }
647
+ return rendered.toReversed().join("\n");
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
+ }
667
654
  function buildSlackProgressCardBlocks(params) {
668
655
  const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS);
669
- const statusLabels = {
670
- completed: "Completed",
671
- in_progress: "In progress",
672
- pending: "Pending"
673
- };
674
- const planLines = (params.plan ?? []).slice(-50).map((entry) => `${statusLabels[entry.status]}: ${compactDetail(entry.step, maxLineChars)}`);
656
+ const planLines = formatPlanChecklistLines(params.plan ?? [], {
657
+ maxLines: 50,
658
+ maxLineChars
659
+ });
675
660
  const narration = params.narration?.replace(/\s+/g, " ").trim();
676
- const authoredText = params.lines.filter(isAuthoredProgressLine).map((line) => line.text.trim()).filter((text, index, values) => text && text !== narration && values.indexOf(text) === index).map((text) => normalizeSlackOutboundText(compactDetail(text, maxLineChars))).join("\n");
677
- const attention = resolveProgressAttention(params.lines);
661
+ const diffStat = formatDiffStat(params.diffStat);
662
+ const workingFooter = [
663
+ ...params.toolCalls && params.toolCalls > 0 ? [`🛠️ ${params.toolCalls} tools`] : [],
664
+ ...diffStat ? [diffStat] : [],
665
+ ...params.elapsedSeconds && params.elapsedSeconds > 0 ? [`⏱ ${params.elapsedSeconds}s`] : []
666
+ ].join(" · ");
667
+ const footer = params.state === "working" ? workingFooter : diffStat;
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));
678
671
  const blocks = [
679
- `${params.state === "success" ? "Completed: " : params.state === "error" ? "Failed: " : ""}*${escapeSlackMrkdwn(params.title.trim() || "Working")}*`,
680
- narration ? `_${escapeSlackMrkdwn(narration)}_` : "",
681
- planLines.map((line) => escapeSlackMrkdwn(line)).join("\n"),
682
- authoredText,
683
- attention && !(params.state !== "working" && attention.status === "pending") ? escapeSlackMrkdwn(attention.title) : ""
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)
684
677
  ].filter(Boolean).map((text) => ({
685
678
  type: "section",
686
- text: {
687
- type: "mrkdwn",
688
- text: truncateSlackText(text, SLACK_PROGRESS_FIELD_MAX)
689
- }
679
+ text: field(text)
690
680
  }));
681
+ if (footer) blocks.push({
682
+ type: "context",
683
+ elements: [field(footer)]
684
+ });
691
685
  if (params.state !== "working" && params.sessionUrl) blocks.push({
692
686
  type: "actions",
693
687
  elements: [{
@@ -703,11 +697,39 @@ function buildSlackProgressCardBlocks(params) {
703
697
  return blocks.slice(0, 50);
704
698
  }
705
699
  const EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT = { tasks: /* @__PURE__ */ new Map() };
700
+ const SLACK_TASK_FIELD_SEPARATOR = " · ";
701
+ function resolveTaskFieldDelta(previous, incoming) {
702
+ if (!incoming) return {
703
+ field: previous,
704
+ delta: void 0
705
+ };
706
+ if (previous?.rendered.includes(incoming)) return {
707
+ field: {
708
+ rendered: previous.rendered,
709
+ source: incoming
710
+ },
711
+ delta: void 0
712
+ };
713
+ const next = applyAppendOnlyStreamUpdate({
714
+ incoming,
715
+ rendered: previous?.rendered ?? "",
716
+ source: previous?.source ?? "",
717
+ separator: SLACK_TASK_FIELD_SEPARATOR
718
+ });
719
+ const delta = next.changed ? next.rendered.slice(previous?.rendered.length ?? 0) : void 0;
720
+ return {
721
+ field: {
722
+ rendered: next.rendered,
723
+ source: next.source
724
+ },
725
+ delta
726
+ };
727
+ }
706
728
  /**
707
729
  * Turns a full task snapshot into the delta Slack must receive. Native streams
708
730
  * key rows by persistent id with no removal chunk: unchanged rows are omitted,
709
- * changed titles/statuses replace in place, and rows that dropped out
710
- * (plan shrinks, summary <-> plan source switches) get a final complete
731
+ * changed rows carry only their unsent field text, and rows that dropped out
732
+ * (plan shrinks, tool-line <-> plan source switches) get a final complete
711
733
  * update or they linger in_progress forever.
712
734
  */
713
735
  function reconcileSlackNativeTaskChunks(params) {
@@ -727,38 +749,46 @@ function reconcileSlackNativeTaskChunks(params) {
727
749
  continue;
728
750
  }
729
751
  const previousRow = params.previous.tasks.get(chunk.id);
730
- const status = chunk.status;
752
+ const details = resolveTaskFieldDelta(previousRow?.details, chunk.details);
753
+ const output = resolveTaskFieldDelta(previousRow?.output, chunk.output);
731
754
  const sourcesChanged = Boolean(chunk.sources) && !previousRow?.sourcesSent;
732
755
  const row = {
733
756
  title: chunk.title,
734
- status
757
+ status: chunk.status
735
758
  };
759
+ if (details.field) row.details = details.field;
760
+ if (output.field) row.output = output.field;
736
761
  if (sourcesChanged || previousRow?.sourcesSent) row.sourcesSent = true;
737
762
  nextTasks.set(chunk.id, row);
738
- if (!(!previousRow || previousRow.title !== chunk.title || previousRow.status !== status || sourcesChanged)) continue;
763
+ if (!(!previousRow || previousRow.title !== chunk.title || previousRow.status !== chunk.status || Boolean(details.delta) || Boolean(output.delta) || sourcesChanged)) continue;
739
764
  const update = {
740
765
  type: "task_update",
741
766
  id: chunk.id,
742
767
  title: chunk.title,
743
- status
768
+ status: chunk.status
744
769
  };
770
+ if (details.delta) update.details = details.delta;
771
+ if (output.delta) update.output = output.delta;
745
772
  if (sourcesChanged) update.sources = chunk.sources;
746
773
  emitted.push(update);
747
774
  }
748
775
  for (const [id, row] of params.previous.tasks) {
749
776
  if (nextTasks.has(id)) continue;
750
- 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) {
751
779
  nextTasks.set(id, row);
752
780
  continue;
753
781
  }
782
+ const title = recovered ? compactTitle(`Recovered: ${row.title}`) : row.title;
754
783
  nextTasks.set(id, {
755
784
  ...row,
785
+ title,
756
786
  status: "complete"
757
787
  });
758
788
  emitted.push({
759
789
  type: "task_update",
760
790
  id,
761
- title: row.title,
791
+ title,
762
792
  status: "complete"
763
793
  });
764
794
  }
@@ -771,62 +801,30 @@ function reconcileSlackNativeTaskChunks(params) {
771
801
  };
772
802
  }
773
803
  //#endregion
774
- //#region extensions/slack/src/stream-mode.ts
775
- function resolveSlackStreamingConfig(params) {
776
- return {
777
- mode: resolveSlackStreamingMode(params),
778
- nativeStreaming: resolveSlackNativeStreaming(params)
779
- };
780
- }
781
- function applyAppendOnlyStreamUpdate(params) {
782
- const incoming = params.incoming.trimEnd();
783
- if (!incoming) return {
784
- rendered: params.rendered,
785
- source: params.source,
786
- changed: false
787
- };
788
- if (!params.rendered) return {
789
- rendered: incoming,
790
- source: incoming,
791
- changed: true
792
- };
793
- if (incoming === params.source) return {
794
- rendered: params.rendered,
795
- source: params.source,
796
- changed: false
797
- };
798
- if (incoming.startsWith(params.rendered)) return {
799
- rendered: incoming,
800
- source: incoming,
801
- changed: incoming !== params.rendered
802
- };
803
- if (incoming.startsWith(params.source)) {
804
- const delta = incoming.slice(params.source.length);
805
- return {
806
- rendered: `${params.rendered}${delta}`,
807
- source: incoming,
808
- changed: delta.length > 0
809
- };
810
- }
811
- if (params.source.startsWith(incoming)) return {
812
- rendered: params.rendered,
813
- source: params.source,
814
- changed: false
815
- };
816
- const separator = params.separator ?? (params.rendered.endsWith("\n") ? "" : "\n");
817
- return {
818
- rendered: `${params.rendered}${separator}${incoming}`,
819
- source: incoming,
820
- changed: true
821
- };
822
- }
823
- //#endregion
824
804
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress-render.ts
825
805
  function resolveStructuredProgressLines(lines) {
826
- return lines.filter((line) => typeof line !== "string");
806
+ return lines.map((line) => {
807
+ if (typeof line !== "string") return line;
808
+ const reasoning = line.startsWith("🧠 ");
809
+ const text = line.replace(/^(?:🧠|💬)\s+/u, "").replace(/^_(.*)_$/su, "$1").trim();
810
+ return {
811
+ ...reasoning ? { id: "reasoning" } : {},
812
+ kind: "item",
813
+ text,
814
+ label: reasoning ? "Reasoning" : "Update",
815
+ prefix: false
816
+ };
817
+ });
827
818
  }
828
819
  function resolveNativeProgressLines(snapshot) {
829
- return resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id !== "reasoning" && line.id?.startsWith("commentary:") !== true);
820
+ const lines = resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id !== "reasoning" && line.id?.startsWith("commentary:") !== true);
821
+ if (snapshot.plan?.length || !snapshot.planExplanation) return lines;
822
+ const explanationLine = buildChannelProgressDraftLine({
823
+ event: "plan",
824
+ phase: "update",
825
+ explanation: snapshot.planExplanation
826
+ });
827
+ return explanationLine ? [...lines, explanationLine] : lines;
830
828
  }
831
829
  function resolveNativeProgressNarration(snapshot) {
832
830
  const paragraphs = resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id === "reasoning" || line.id?.startsWith("commentary:") === true).map((line) => line.text.trim()).filter((text, index, values) => Boolean(text) && values.indexOf(text) === index);
@@ -964,7 +962,6 @@ function createSlackDraftProgressCardRuntime(params) {
964
962
  return url.toString();
965
963
  };
966
964
  const resolveText = (snapshot) => latestFallbackText || formatChannelProgressDraftText({
967
- presentation: "summary",
968
965
  entry: account.config,
969
966
  lines: [...snapshot.lines],
970
967
  seed: params.progressSeed,
@@ -974,14 +971,20 @@ function createSlackDraftProgressCardRuntime(params) {
974
971
  });
975
972
  const resolvePresentation = (snapshot, state) => {
976
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();
977
977
  return buildSlackProgressCardBlocks({
978
978
  state,
979
979
  title,
980
- narration: params.explicitTitle ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) : snapshot.planExplanation && snapshot.planExplanation !== title ? snapshot.planExplanation : void 0,
980
+ narration,
981
981
  plan: snapshot.plan,
982
982
  lines: resolveStructuredProgressLines(snapshot.lines),
983
983
  maxLineChars: params.maxLineChars,
984
- ...state !== "working" ? { sessionUrl: resolveSessionUrl() } : {}
984
+ diffStat: snapshot.diffStat,
985
+ toolCalls: workCounter?.toolCalls,
986
+ elapsedSeconds: workCounter?.elapsedSeconds,
987
+ sessionUrl
985
988
  });
986
989
  };
987
990
  const finalize = async (status, snapshot = params.getSnapshot(), fallbackText = resolveText(snapshot)) => {
@@ -1035,10 +1038,10 @@ function formatSlackProgressDraftLine(line) {
1035
1038
  if (/^(?:🧠|💬)\s/u.test(line)) return line;
1036
1039
  const italicCommentary = /^_(.*)_$/su.exec(line);
1037
1040
  if (!italicCommentary) return escapeSlackMrkdwn(line);
1038
- return `_${italicCommentary[1].split(/(`[^`\n]+`)/u).map((segment, index) => {
1039
- if (index % 2 === 0) return escapeSlackMrkdwn(segment);
1040
- return `\`${segment.slice(1, -1).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}\``;
1041
- }).join("")}_`;
1041
+ return `_${normalizeSlackOutboundText(italicCommentary[1], {
1042
+ mentions: "escape",
1043
+ enclosingStyle: "italic"
1044
+ })}_`;
1042
1045
  }
1043
1046
  //#endregion
1044
1047
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress-native.ts
@@ -1075,6 +1078,7 @@ function createSlackNativeProgressTransport(params) {
1075
1078
  const startPromise = (async () => {
1076
1079
  const session = await startSlackStream({
1077
1080
  client: slackClient,
1081
+ clientOptions: params.setup.slackClientOptions,
1078
1082
  channel: message.channel,
1079
1083
  threadTs: streamThreadTs,
1080
1084
  ...update.text ? { text: update.text } : {},
@@ -1145,7 +1149,7 @@ function createSlackProgressRuntime(runtimeParams) {
1145
1149
  const isProgressMode = slackStreaming.mode === "progress";
1146
1150
  const useNativeProgressStreaming = useStreaming && slackStreaming.mode === "progress";
1147
1151
  const progressDraftActive = Boolean(draftStream) || useNativeProgressStreaming;
1148
- const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, true, slackStreaming.mode);
1152
+ const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, slackStreaming.mode !== "progress", slackStreaming.mode);
1149
1153
  let shouldYieldDraftProgress = () => false;
1150
1154
  const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, {
1151
1155
  draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming,
@@ -1167,6 +1171,7 @@ function createSlackProgressRuntime(runtimeParams) {
1167
1171
  nativeStreamOrder = run.catch(() => void 0);
1168
1172
  return run;
1169
1173
  };
1174
+ const progressWorkCounter = createChannelProgressWorkCounter();
1170
1175
  const progressSeed = `${account.accountId}:${message.channel}`;
1171
1176
  const slackProgressStyle = resolveSlackProgressStyle(account.config);
1172
1177
  const useDraftProgressCard = Boolean(draftStream) && isProgressMode && slackProgressStyle === "card";
@@ -1182,6 +1187,7 @@ function createSlackProgressRuntime(runtimeParams) {
1182
1187
  },
1183
1188
  draftStream,
1184
1189
  enabled: useDraftProgressCard,
1190
+ progressWorkCounter: previewToolProgressEnabled ? progressWorkCounter : void 0,
1185
1191
  progressSeed,
1186
1192
  explicitTitle: explicitProgressTitle,
1187
1193
  maxLineChars: progressDraftMaxLineChars,
@@ -1246,9 +1252,11 @@ function createSlackProgressRuntime(runtimeParams) {
1246
1252
  const reconciled = reconcileSlackNativeTaskChunks({
1247
1253
  previous: nativeStreamSnapshot,
1248
1254
  chunks: buildSlackProgressStreamChunks({
1249
- title: resolveNativeProgressTitle(snapshot) ?? "Working",
1255
+ title: resolveNativeProgressTitle(snapshot),
1250
1256
  lines: resolveNativeProgressLines(snapshot),
1251
- plan: snapshot.plan
1257
+ plan: snapshot.plan,
1258
+ maxLineChars: progressDraftMaxLineChars,
1259
+ summaryRow: !previewToolProgressEnabled
1252
1260
  })
1253
1261
  });
1254
1262
  const chunks = reconciled.chunks;
@@ -1307,18 +1315,17 @@ function createSlackProgressRuntime(runtimeParams) {
1307
1315
  return true;
1308
1316
  };
1309
1317
  const resetProgressTurnState = () => {
1318
+ progressWorkCounter.reset();
1310
1319
  nativeNarrationRenderedText = "";
1311
1320
  nativeNarrationSourceText = "";
1312
1321
  };
1313
1322
  const progressDraft = createChannelProgressDraftCompositor({
1314
- presentation: isProgressMode && slackProgressStyle === "card" ? "summary" : void 0,
1315
1323
  entry: account.config,
1316
1324
  mode: slackStreaming.mode,
1317
1325
  active: progressDraftActive,
1318
1326
  seed: progressSeed,
1319
1327
  formatLine: formatSlackProgressDraftLine,
1320
1328
  reasoningLinePrefix: "🧠 ",
1321
- reasoningGate: previewToolProgressEnabled,
1322
1329
  updateOnLineChange: useNativeProgressStreaming || useDraftProgressCard,
1323
1330
  update: async (previewText, options) => {
1324
1331
  if (useNativeProgressStreaming) {
@@ -1372,14 +1379,18 @@ function createSlackProgressRuntime(runtimeParams) {
1372
1379
  const sessionUrl = progressCard.resolveSessionUrl();
1373
1380
  const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(snapshot));
1374
1381
  const hasRetirableNativeTasks = [...nativeStreamSnapshot.tasks.values()].some((task) => task.status !== "complete" && task.status !== "error");
1375
- if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !narrationUpdate.delta && !sessionUrl) return;
1382
+ if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !snapshot.diffStat && !narrationUpdate.delta && !sessionUrl) return;
1376
1383
  const completion = reconcileSlackNativeTaskChunks({
1377
1384
  previous: nativeStreamSnapshot,
1385
+ finalStatus: finalInProgressStatus,
1378
1386
  chunks: buildSlackProgressStreamChunks({
1379
1387
  title: resolveNativeProgressTitle(snapshot) ?? (lines.length === 0 && !snapshot.plan?.length ? "Working" : void 0),
1380
1388
  lines,
1381
1389
  plan: snapshot.plan,
1390
+ maxLineChars: progressDraftMaxLineChars,
1391
+ summaryRow: !previewToolProgressEnabled,
1382
1392
  finalInProgressStatus,
1393
+ diffStat: snapshot.diffStat,
1383
1394
  sessionUrl
1384
1395
  })
1385
1396
  }).chunks;
@@ -1390,28 +1401,14 @@ function createSlackProgressRuntime(runtimeParams) {
1390
1401
  };
1391
1402
  const finishNativeProgressTurn = async (completionChunks) => {
1392
1403
  if (delivery.nativeProgressStreamStartPromise) await delivery.nativeProgressStreamStartPromise.catch(() => null);
1393
- const session = delivery.streamSession;
1394
- if (session && !session.stopped) try {
1395
- if (completionChunks?.length) nativeProgressCompletionSent = true;
1396
- const stopResult = await stopSlackStream({
1397
- session,
1398
- ...completionChunks?.length ? { chunks: completionChunks } : {},
1399
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1400
- });
1401
- delivery.acknowledgeStoppedStreamedDeliveries(session, stopResult?.messageId);
1402
- } catch (err) {
1403
- const error = formatSlackError(err);
1404
- delivery.emitAcknowledgedStreamedDeliveries();
1405
- delivery.emitFailedPendingStreamedDeliveries(error);
1406
- logVerbose(`slack-stream: failed to rotate native progress stream (${error})`);
1407
- }
1404
+ if (completionChunks?.length) nativeProgressCompletionSent = true;
1405
+ await delivery.finishStream(completionChunks);
1408
1406
  delivery.streamSession = null;
1409
1407
  delivery.nativeProgressStreamStartPromise = null;
1410
1408
  delivery.nativeProgressStreamThreadTs = void 0;
1411
1409
  delivery.streamFailed = false;
1412
1410
  };
1413
1411
  const pushPlanProgress = async (steps, explanation) => {
1414
- if (!previewToolProgressEnabled) return false;
1415
1412
  if (isProgressMode) {
1416
1413
  if (slackProgressStyle === "compact") return false;
1417
1414
  return await progressDraft.pushPlanProgress(steps, { explanation });
@@ -1553,6 +1550,7 @@ function createSlackProgressRuntime(runtimeParams) {
1553
1550
  previewToolProgressEnabled,
1554
1551
  suppressDefaultToolProgressMessages,
1555
1552
  progressDraft,
1553
+ progressWorkCounter,
1556
1554
  commentaryProgressEnabled,
1557
1555
  async cancel() {
1558
1556
  progressDraft.cancel();
@@ -1589,6 +1587,10 @@ function createSlackProgressRuntime(runtimeParams) {
1589
1587
  async function createSlackDispatchSetup(prepared) {
1590
1588
  const { ctx, account, message, route } = prepared;
1591
1589
  const slackClient = prepared.eventScope?.client ?? ctx.app.client;
1590
+ const slackClientOptions = {
1591
+ ...ctx.app.webClientOptions,
1592
+ ...prepared.eventScope ? { teamId: prepared.eventScope.teamId } : {}
1593
+ };
1592
1594
  const slackStreamFallbackTeamId = prepared.eventScope?.teamId ?? ctx.teamId;
1593
1595
  const cfg = ctx.cfg;
1594
1596
  const runtime = ctx.runtime;
@@ -1702,12 +1704,13 @@ async function createSlackDispatchSetup(prepared) {
1702
1704
  transformReplyPayload: sanitizeSlackMonitorReplyPayload,
1703
1705
  typing: {
1704
1706
  start: async () => {
1705
- if (!threadStatusGate.hasVisibleOutput()) {
1707
+ if (!didSetStatus && !threadStatusGate.hasVisibleOutput()) {
1706
1708
  didSetStatus = true;
1707
- await ctx.setSlackThreadStatus({
1709
+ await ctx.setSlackSessionStatus({
1708
1710
  channelId: message.channel,
1709
1711
  threadTs: statusThreadTs,
1710
- status: "is typing...",
1712
+ status: "processing",
1713
+ title: prepared.sessionDisplayName ?? prepared.ctxPayload.ThreadLabel,
1711
1714
  eventScope: prepared.eventScope
1712
1715
  });
1713
1716
  }
@@ -1724,10 +1727,10 @@ async function createSlackDispatchSetup(prepared) {
1724
1727
  stop: async () => {
1725
1728
  if (didSetStatus) {
1726
1729
  didSetStatus = false;
1727
- await ctx.setSlackThreadStatus({
1730
+ await ctx.setSlackSessionStatus({
1728
1731
  channelId: message.channel,
1729
1732
  threadTs: statusThreadTs,
1730
- status: "",
1733
+ status: "active",
1731
1734
  eventScope: prepared.eventScope
1732
1735
  });
1733
1736
  }
@@ -1782,15 +1785,6 @@ async function createSlackDispatchSetup(prepared) {
1782
1785
  });
1783
1786
  const shouldUseDraftStream = previewStreamingEnabled && !useStreaming;
1784
1787
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1785
- const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1786
- useStreaming,
1787
- shouldUseDraftStream,
1788
- blockStreamingEnabled
1789
- });
1790
- const onSlackDeliveryError = (err, info) => {
1791
- runtime.error?.(danger(`slack ${info.kind} reply failed: ${formatSlackError(err)}`));
1792
- replyPipeline.typingCallbacks?.onIdle?.();
1793
- };
1794
1788
  return {
1795
1789
  prepared,
1796
1790
  ctx,
@@ -1798,6 +1792,7 @@ async function createSlackDispatchSetup(prepared) {
1798
1792
  message,
1799
1793
  route,
1800
1794
  slackClient,
1795
+ slackClientOptions,
1801
1796
  slackStreamFallbackTeamId,
1802
1797
  cfg,
1803
1798
  runtime,
@@ -1827,15 +1822,18 @@ async function createSlackDispatchSetup(prepared) {
1827
1822
  previewStreamingEnabled,
1828
1823
  hasSlackCustomIdentity,
1829
1824
  shouldUseDraftStream,
1830
- disableBlockStreaming,
1831
- useStreaming,
1832
- onSlackDeliveryError
1825
+ disableBlockStreaming: sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1826
+ useStreaming,
1827
+ shouldUseDraftStream,
1828
+ blockStreamingEnabled
1829
+ }),
1830
+ useStreaming
1833
1831
  };
1834
1832
  }
1835
1833
  //#endregion
1836
1834
  //#region extensions/slack/src/monitor/message-handler/dispatch-streaming.ts
1837
1835
  function createSlackStreamingDeliveryRuntime(setup) {
1838
- 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;
1839
1837
  const state = {
1840
1838
  streamSession: null,
1841
1839
  nativeProgressStreamStartPromise: null,
@@ -1846,69 +1844,14 @@ function createSlackStreamingDeliveryRuntime(setup) {
1846
1844
  observedReplyDelivery: false,
1847
1845
  observedFinalReplyDelivery: false
1848
1846
  };
1849
- const streamedDeliveries = [];
1850
- const refreshStreamedAcknowledgements = (session) => {
1851
- if (session.pendingText.length === 0) for (const delivery of streamedDeliveries) delivery.acknowledged = true;
1852
- };
1853
- const recordStreamedDelivery = (kind, content) => {
1854
- const delivery = {
1855
- kind,
1847
+ const emitStreamedDelivery = (content, result) => {
1848
+ emitSlackMessageSentHooks({
1849
+ ...messageSentHookContext,
1850
+ to: messageSentHookTarget,
1851
+ accountId: account.accountId,
1856
1852
  content,
1857
- acknowledged: false
1858
- };
1859
- streamedDeliveries.push(delivery);
1860
- return delivery;
1861
- };
1862
- const rememberStreamedDelivery = (kind, content, session) => {
1863
- recordStreamedDelivery(kind, content);
1864
- refreshStreamedAcknowledgements(session);
1865
- };
1866
- const emitAcknowledgedStreamedDeliveries = (messageId) => {
1867
- for (const delivery of streamedDeliveries) {
1868
- if (!delivery.acknowledged || delivery.outcome) continue;
1869
- emitSlackMessageSentHooks({
1870
- ...messageSentHookContext,
1871
- to: messageSentHookTarget,
1872
- accountId: account.accountId,
1873
- content: delivery.content,
1874
- success: true,
1875
- ...messageId ? { messageId } : {}
1876
- });
1877
- delivery.outcome = "success";
1878
- }
1879
- };
1880
- const acknowledgeStoppedStreamedDeliveries = (session, messageId) => {
1881
- refreshStreamedAcknowledgements(session);
1882
- for (const delivery of streamedDeliveries) delivery.acknowledged = true;
1883
- emitAcknowledgedStreamedDeliveries(messageId);
1884
- };
1885
- const emitFailedPendingStreamedDeliveries = (error) => {
1886
- for (const delivery of streamedDeliveries) {
1887
- if (delivery.acknowledged || delivery.outcome) continue;
1888
- emitSlackMessageSentHooks({
1889
- ...messageSentHookContext,
1890
- to: messageSentHookTarget,
1891
- accountId: account.accountId,
1892
- content: delivery.content,
1893
- success: false,
1894
- error
1895
- });
1896
- delivery.outcome = "failure";
1897
- }
1898
- };
1899
- const emitSuccessfulPendingStreamedDeliveries = (messageId) => {
1900
- for (const delivery of streamedDeliveries) {
1901
- if (delivery.acknowledged || delivery.outcome) continue;
1902
- emitSlackMessageSentHooks({
1903
- ...messageSentHookContext,
1904
- to: messageSentHookTarget,
1905
- accountId: account.accountId,
1906
- content: delivery.content,
1907
- success: true,
1908
- ...messageId ? { messageId } : {}
1909
- });
1910
- delivery.outcome = "success";
1911
- }
1853
+ ...result
1854
+ });
1912
1855
  };
1913
1856
  let deliveryTracker = createSlackEventDeliveryTracker();
1914
1857
  const markPreviewPayloadDelivered = (params) => {
@@ -1929,62 +1872,75 @@ function createSlackStreamingDeliveryRuntime(setup) {
1929
1872
  if (kind === "block") state.usedBlockReplyThreadTs = deliveredThreadTs;
1930
1873
  };
1931
1874
  const deliverPendingStreamFallback = async (session, err) => {
1875
+ if (session.stoppedBySlack) return;
1932
1876
  let fallbackError = err;
1933
1877
  if (!session.stopped) try {
1934
1878
  const stopResult = await stopSlackStream({
1935
1879
  session,
1936
1880
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1937
1881
  });
1938
- acknowledgeStoppedStreamedDeliveries(session, stopResult.messageId);
1882
+ if (session.stoppedBySlack) return;
1939
1883
  state.observedReplyDelivery = true;
1940
1884
  state.usedReplyThreadTs ??= session.threadTs;
1941
- return true;
1885
+ return stopResult;
1942
1886
  } catch (stopErr) {
1943
1887
  if (stopErr instanceof SlackStreamNotDeliveredError) fallbackError = stopErr;
1944
- else runtime.error?.(danger(`slack-stream: failed to finalize buffered text before fallback: ${formatSlackError(stopErr)}`));
1888
+ else throw stopErr;
1945
1889
  }
1946
- emitAcknowledgedStreamedDeliveries();
1947
1890
  const fallbackText = fallbackError.pendingText.trim();
1948
- if (!fallbackText) return false;
1949
- try {
1950
- await deliverReplies({
1951
- cfg: ctx.cfg,
1952
- replies: [{ text: fallbackText }],
1953
- target: prepared.replyTarget,
1954
- token: ctx.botToken,
1955
- accountId: account.accountId,
1956
- runtime,
1957
- textLimit: ctx.textLimit,
1958
- mediaMaxBytes: ctx.mediaMaxBytes,
1959
- replyThreadTs: session.threadTs,
1960
- replyToMode: replyDeliveryMode,
1961
- ...slackIdentity ? { identity: slackIdentity } : {},
1962
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1963
- ...messageSentDeliveryHookContext,
1964
- deferMessageSentHooks: true,
1965
- 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 } : {}
1966
1914
  });
1967
- markSlackStreamFallbackDelivered(session);
1968
- 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 {
1969
1927
  await stopSlackStream({
1970
1928
  session,
1929
+ ...chunks?.length ? { chunks } : {},
1971
1930
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1972
1931
  });
1973
- } catch (finalizeErr) {
1974
- 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);
1975
1936
  }
1976
- emitSuccessfulPendingStreamedDeliveries();
1977
- state.observedReplyDelivery = true;
1978
- state.usedReplyThreadTs ??= session.threadTs;
1979
- logVerbose(`slack-stream: streamed delivery failed (${fallbackError.slackCode}); delivered ${fallbackText.length} chars via deliverReplies fallback`);
1980
- return true;
1981
- } catch (postErr) {
1982
- emitFailedPendingStreamedDeliveries(formatErrorMessage(postErr));
1983
- runtime.error?.(danger(`slack-stream: fallback deliverReplies failed after ${fallbackError.slackCode}: ${formatErrorMessage(postErr)}`));
1984
- return false;
1937
+ } catch (error) {
1938
+ state.streamFailed = true;
1939
+ runtime.error?.(danger(`slack-stream: failed to stop stream: ${formatSlackError(error)}`));
1985
1940
  }
1986
1941
  };
1987
1942
  const deliverNormally = async (params) => {
1943
+ if (state.streamSession?.stoppedBySlack) return;
1988
1944
  const replyThreadTs = resolveDeliveryThreadTs(params);
1989
1945
  const deliveryReplyThreadTs = replyDeliveryMode === "off" && !forcedReplyThreadTs && !isThreadReply ? void 0 : replyThreadTs;
1990
1946
  if (deliveryTracker.hasDelivered({
@@ -2028,19 +1984,6 @@ function createSlackStreamingDeliveryRuntime(setup) {
2028
1984
  });
2029
1985
  return deliveryReplyThreadTs;
2030
1986
  };
2031
- const deliverBufferedStreamFallback = async (params) => {
2032
- if (!await deliverPendingStreamFallback(params.session, params.err)) return false;
2033
- replyPlan.markSent();
2034
- if (params.kind === "final") state.observedFinalReplyDelivery = true;
2035
- deliveryTracker.markDelivered({
2036
- kind: params.kind,
2037
- payload: params.payload,
2038
- threadTs: params.session.threadTs,
2039
- textOverride: params.textOverride
2040
- });
2041
- rememberDeliveredThreadTs(params.kind, params.session.threadTs);
2042
- return true;
2043
- };
2044
1987
  const isStreamingEligible = (payload, options) => {
2045
1988
  const reply = resolveSendableOutboundReplyParts(payload);
2046
1989
  const renderPlan = resolveSlackReplyRenderPlan(payload);
@@ -2048,7 +1991,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
2048
1991
  return !state.streamFailed && !reply.hasMedia && renderPlan.mode !== "split" && !plannedBlocks?.length && !readSlackReplyBlocks(payload)?.length && reply.hasText && (!options?.maxTextBytes || countSlackTextUtf8Bytes(reply.trimmedText) <= options.maxTextBytes);
2049
1992
  };
2050
1993
  const deliverWithStreaming = async (params) => {
2051
- const reply = resolveSendableOutboundReplyParts(params.payload);
1994
+ if (state.streamSession?.stoppedBySlack) return;
2052
1995
  if (!isStreamingEligible(params.payload)) {
2053
1996
  await deliverNormally({
2054
1997
  payload: params.payload,
@@ -2057,47 +2000,44 @@ function createSlackStreamingDeliveryRuntime(setup) {
2057
2000
  });
2058
2001
  return;
2059
2002
  }
2060
- const text = params.streamText ?? reply.trimmedText;
2061
- const hookContent = reply.trimmedText;
2062
- const chunks = params.kind === "final" ? [] : void 0;
2063
- 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;
2064
2027
  try {
2065
- if (!state.streamSession && state.nativeProgressStreamStartPromise) await state.nativeProgressStreamStartPromise;
2066
- if (state.streamFailed) {
2067
- await deliverNormally({
2068
- payload: params.payload,
2069
- kind: params.kind,
2070
- forcedThreadTs: state.streamSession?.threadTs ?? state.nativeProgressStreamThreadTs
2071
- });
2072
- return;
2073
- }
2074
- if (!state.streamSession) {
2075
- const streamThreadTs = replyPlan.nextThreadTs();
2076
- plannedThreadTs = streamThreadTs;
2077
- if (!streamThreadTs) {
2078
- logVerbose("slack-stream: no reply thread target for stream start, falling back to normal delivery");
2079
- state.streamFailed = true;
2080
- await deliverNormally({
2081
- payload: params.payload,
2082
- kind: params.kind
2083
- });
2084
- return;
2085
- }
2086
- if (deliveryTracker.hasDelivered({
2087
- kind: params.kind,
2088
- payload: params.payload,
2089
- threadTs: streamThreadTs,
2090
- textOverride: text
2091
- })) {
2092
- logVerbose("slack-stream: suppressed duplicate stream start payload");
2093
- return;
2094
- }
2095
- 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({
2096
2035
  client: slackClient,
2036
+ clientOptions: slackClientOptions,
2097
2037
  channel: message.channel,
2098
- threadTs: streamThreadTs,
2038
+ threadTs,
2099
2039
  text,
2100
- chunks,
2040
+ chunks: [],
2101
2041
  ...params.taskDisplayMode ? { taskDisplayMode: params.taskDisplayMode } : {},
2102
2042
  ...slackIdentity ? { identity: slackIdentity } : {},
2103
2043
  teamId: await resolveSlackStreamRecipientTeamId({
@@ -2108,95 +2048,63 @@ function createSlackStreamingDeliveryRuntime(setup) {
2108
2048
  }),
2109
2049
  userId: message.user
2110
2050
  });
2111
- refreshStreamedAcknowledgements(state.streamSession);
2112
- if (state.streamSession.delivered) {
2113
- state.observedReplyDelivery = true;
2114
- if (params.kind === "final") state.observedFinalReplyDelivery = true;
2115
- }
2116
- if (hookContent) rememberStreamedDelivery(params.kind, hookContent, state.streamSession);
2117
- rememberDeliveredThreadTs(params.kind, streamThreadTs);
2118
- replyPlan.markSent();
2119
- deliveryTracker.markDelivered({
2120
- kind: params.kind,
2121
- payload: params.payload,
2122
- threadTs: streamThreadTs,
2123
- textOverride: text
2124
- });
2125
- return;
2051
+ state.streamSession = session;
2126
2052
  }
2127
- if (deliveryTracker.hasDelivered({
2128
- kind: params.kind,
2129
- payload: params.payload,
2130
- threadTs: state.streamSession.threadTs,
2131
- textOverride: text
2132
- })) {
2133
- logVerbose("slack-stream: suppressed duplicate append payload");
2134
- return;
2135
- }
2136
- if (hookContent) recordStreamedDelivery(params.kind, hookContent);
2137
- await appendSlackStream({
2138
- session: state.streamSession,
2139
- text: `${params.appendSeparator === false ? "" : "\n"}${text}`,
2140
- chunks
2141
- });
2142
- refreshStreamedAcknowledgements(state.streamSession);
2143
- if (state.streamSession.delivered && state.streamSession.pendingText.length === 0) {
2144
- state.observedReplyDelivery = true;
2145
- 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;
2146
2062
  }
2147
- deliveryTracker.markDelivered({
2148
- kind: params.kind,
2149
- payload: params.payload,
2150
- threadTs: state.streamSession.threadTs,
2151
- textOverride: text
2152
- });
2153
- } catch (err) {
2154
- if (err instanceof SlackStreamNotDeliveredError) {
2155
- state.streamFailed = true;
2156
- if (state.streamSession) {
2157
- if (await deliverBufferedStreamFallback({
2158
- session: state.streamSession,
2159
- err,
2160
- payload: params.payload,
2161
- kind: params.kind,
2162
- textOverride: text
2163
- })) return;
2164
- throw err;
2165
- }
2063
+ if (!session) {
2166
2064
  await deliverNormally({
2167
2065
  payload: params.payload,
2168
2066
  kind: params.kind,
2169
- forcedThreadTs: plannedThreadTs
2067
+ forcedThreadTs: threadTs
2170
2068
  });
2171
2069
  return;
2172
2070
  }
2173
- runtime.error?.(danger(`slack-stream: streaming API call failed: ${formatSlackError(err)}, falling back`));
2174
- state.streamFailed = true;
2175
- if (state.streamSession && state.streamSession.pendingText) {
2176
- const bufferedFallbackErr = new SlackStreamNotDeliveredError(state.streamSession.pendingText, "unknown");
2177
- if (await deliverBufferedStreamFallback({
2178
- session: state.streamSession,
2179
- err: bufferedFallbackErr,
2180
- payload: params.payload,
2181
- kind: params.kind,
2182
- textOverride: text
2183
- })) return;
2184
- 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;
2185
2081
  }
2186
- await deliverNormally({
2187
- payload: params.payload,
2188
- kind: params.kind,
2189
- 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"
2190
2087
  });
2088
+ return;
2191
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
+ });
2192
2103
  };
2193
2104
  return Object.assign(state, {
2194
- acknowledgeStoppedStreamedDeliveries,
2195
2105
  deliverNormally,
2196
- deliverPendingStreamFallback,
2197
2106
  deliverWithStreaming,
2198
- emitAcknowledgedStreamedDeliveries,
2199
- emitFailedPendingStreamedDeliveries,
2107
+ finishStream,
2200
2108
  hasDelivered: (params) => deliveryTracker.hasDelivered(params),
2201
2109
  isStreamingEligible,
2202
2110
  markPreviewPayloadDelivered,
@@ -2210,7 +2118,35 @@ function createSlackStreamingDeliveryRuntime(setup) {
2210
2118
  //#region extensions/slack/src/monitor/message-handler/dispatch.ts
2211
2119
  async function dispatchPreparedSlackMessage(prepared) {
2212
2120
  const setup = await createSlackDispatchSetup(prepared);
2213
- 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;
2214
2150
  const delivery = createSlackStreamingDeliveryRuntime(setup);
2215
2151
  const draftPreviewCommitted = { value: false };
2216
2152
  const progress = createSlackProgressRuntime({
@@ -2251,6 +2187,14 @@ async function dispatchPreparedSlackMessage(prepared) {
2251
2187
  pendingFailureNotice = notice;
2252
2188
  return payload;
2253
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
+ };
2254
2198
  const deliverSlackPayload = async (payload, info) => {
2255
2199
  if (info.kind === "final" && slackStreaming.mode === "progress" && progress.isProgressMode) {
2256
2200
  if (progress.useNativeProgressStreaming) {
@@ -2258,6 +2202,24 @@ async function dispatchPreparedSlackMessage(prepared) {
2258
2202
  return;
2259
2203
  }
2260
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
+ }
2261
2223
  if (progress.useDraftProgressCard) {
2262
2224
  await delivery.deliverNormally({
2263
2225
  payload,
@@ -2409,7 +2371,6 @@ async function dispatchPreparedSlackMessage(prepared) {
2409
2371
  });
2410
2372
  if (info.kind === "final") progress.progressDraft.markFinalReplyDelivered();
2411
2373
  };
2412
- let dispatchError;
2413
2374
  let agentRunFailed = false;
2414
2375
  let settledDispatchResult;
2415
2376
  try {
@@ -2433,13 +2394,18 @@ async function dispatchPreparedSlackMessage(prepared) {
2433
2394
  },
2434
2395
  delivery: {
2435
2396
  deliver: deliverSlackPayload,
2436
- 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
+ }
2437
2402
  },
2438
2403
  record: prepared.turn.record,
2439
2404
  history: prepared.turn.history,
2440
2405
  botLoopProtection: resolveSlackBotLoopProtection(prepared),
2441
2406
  replyOptions: {
2442
- ...prepared.turnAdoptionLifecycle ? { turnAdoptionLifecycle: prepared.turnAdoptionLifecycle } : {},
2407
+ queuedDeliveryCorrelations: [{ begin: beginSessionRun }],
2408
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {},
2443
2409
  skillFilter: prepared.channelConfig?.skills,
2444
2410
  sourceReplyDeliveryMode,
2445
2411
  suppressTyping: suppressRoomEventTyping ? true : void 0,
@@ -2469,14 +2435,14 @@ async function dispatchPreparedSlackMessage(prepared) {
2469
2435
  },
2470
2436
  onQueuedFollowupAdmitted: progress.onQueuedFollowupAdmitted,
2471
2437
  onQueuedFollowupSettled: progress.onQueuedFollowupSettled,
2472
- onReasoningStream: statusReactionsEnabled || progress.previewToolProgressEnabled ? async (payload) => {
2438
+ onReasoningStream: async (payload) => {
2473
2439
  const visible = await progress.pushReasoningProgress(payload);
2474
- if (!statusReactionsEnabled) return visible;
2475
- await statusReactions.setThinking();
2440
+ if (statusReactionsEnabled) await statusReactions.setThinking();
2476
2441
  return visible;
2477
- } : void 0,
2442
+ },
2478
2443
  onToolStart: async (payload) => {
2479
2444
  if (statusReactionsEnabled) await statusReactions.setTool(payload.name);
2445
+ if (payload.phase === "start") progress.progressWorkCounter.noteToolCall(payload.name);
2480
2446
  return await progress.progressDraft.pushToolEvent(payload);
2481
2447
  },
2482
2448
  onItemEvent: async (payload) => {
@@ -2517,38 +2483,16 @@ async function dispatchPreparedSlackMessage(prepared) {
2517
2483
  });
2518
2484
  }
2519
2485
  } catch (err) {
2520
- dispatchError = err;
2486
+ dispatchError ??= err;
2521
2487
  } finally {
2522
2488
  await progress.cancel();
2523
2489
  if (!progress.useDraftProgressCard) await draftStream?.discardPending();
2524
2490
  }
2525
- let streamFallbackDelivered = false;
2526
- const finalStream = delivery.streamSession;
2527
- if (finalStream && !finalStream.stopped) try {
2528
- const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError || agentRunFailed ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2529
- if (completionChunks?.length) progress.nativeProgressCompletionSent = true;
2530
- const stopResult = await stopSlackStream({
2531
- session: finalStream,
2532
- ...completionChunks?.length ? { chunks: completionChunks } : {},
2533
- ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2534
- });
2535
- delivery.acknowledgeStoppedStreamedDeliveries(finalStream, stopResult?.messageId);
2536
- } catch (err) {
2537
- if (err instanceof SlackStreamNotDeliveredError) {
2538
- streamFallbackDelivered = await delivery.deliverPendingStreamFallback(finalStream, err);
2539
- if (!streamFallbackDelivered) dispatchError ??= err;
2540
- } else {
2541
- const error = formatSlackError(err);
2542
- delivery.emitAcknowledgedStreamedDeliveries();
2543
- delivery.emitFailedPendingStreamedDeliveries(error);
2544
- runtime.error?.(danger(`slack-stream: failed to stop stream: ${error}`));
2545
- if (!finalStream.delivered) dispatchError ??= err;
2546
- }
2547
- }
2548
- const anyReplyDelivered = hasVisibleInboundReplyDispatch(settledDispatchResult, {
2549
- observedReplyDelivery: delivery.observedReplyDelivery,
2550
- fallbackDelivered: streamFallbackDelivered
2551
- });
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();
2552
2496
  if (pendingFailureNotice && anyReplyDelivered) recordSlackThreadFailureNotice(pendingFailureNotice);
2553
2497
  if (dispatchError || agentRunFailed) await progress.finalizeDraftProgressCard("error");
2554
2498
  await progress.dropDetachedProgressCards();
@@ -2815,7 +2759,7 @@ const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4;
2815
2759
  const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20;
2816
2760
  const SLACK_USER_MENTION_RE$1 = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
2817
2761
  const MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS = 2e3;
2818
- const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-fh66A6lc.js").then((n) => n._));
2762
+ const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-CFkvEv1d.js").then((n) => n._));
2819
2763
  function formatSlackUnavailableMedia(params) {
2820
2764
  let fileReferences = params.files?.map((file) => `${formatSlackFileReference(file)} unavailable (${file.reason})`).join(", ");
2821
2765
  if (fileReferences && fileReferences.length > MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS) fileReferences = `${truncateUtf16Safe(fileReferences, MAX_SLACK_UNAVAILABLE_FILE_TEXT_CHARS)}; … (file references truncated)`;
@@ -3007,105 +2951,6 @@ async function resolveSlackDmHistoryContext(params) {
3007
2951
  }
3008
2952
  }
3009
2953
  //#endregion
3010
- //#region extensions/slack/src/monitor/message-handler/prepare-routing.ts
3011
- function resolveSlackBaseConversationId(params) {
3012
- const raw = params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel;
3013
- return qualifySlackConversationId(raw, params.eventScope);
3014
- }
3015
- function resolveSlackInitialAgentRoute(params) {
3016
- const route = resolveAgentRoute({
3017
- cfg: normalizeSlackRouteBindingConfig(params.ctx.cfg),
3018
- channel: "slack",
3019
- accountId: params.account.accountId,
3020
- teamId: params.eventScope?.teamId || params.ctx.teamId || void 0,
3021
- peer: {
3022
- kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
3023
- id: qualifySlackRoutePeerId({
3024
- id: params.isDirectMessage ? params.message.user ?? "unknown" : params.message.channel,
3025
- kind: params.isDirectMessage ? "user" : "channel",
3026
- eventScope: params.eventScope
3027
- })
3028
- }
3029
- });
3030
- if (!params.eventScope || !params.isDirectMessage || route.dmScope !== "main") return route;
3031
- const sessionKey = resolveSlackEnterpriseMainDmSessionKey({
3032
- baseSessionKey: route.sessionKey,
3033
- accountId: params.account.accountId,
3034
- eventScope: params.eventScope
3035
- });
3036
- return {
3037
- ...route,
3038
- sessionKey,
3039
- mainSessionKey: sessionKey
3040
- };
3041
- }
3042
- function resolveSlackRoutingContext(params) {
3043
- const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish, channelConfig, seedTopLevelRoomThread, assistantThreadTs, agentViewThreadTs, eventScope } = params;
3044
- let route = resolveSlackInitialAgentRoute({
3045
- ctx,
3046
- account,
3047
- message,
3048
- isDirectMessage,
3049
- isRoom,
3050
- eventScope
3051
- });
3052
- const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";
3053
- const replyToMode = channelConfig?.replyToMode ?? resolveSlackReplyToMode(account, chatType);
3054
- const threadContext = resolveSlackThreadContext({
3055
- message,
3056
- replyToMode,
3057
- isDirectMessage
3058
- });
3059
- const threadTs = threadContext.incomingThreadTs;
3060
- const isThreadReply = threadContext.isThreadReply;
3061
- const autoThreadId = !isThreadReply && replyToMode === "all" && threadContext.messageTs ? threadContext.messageTs : void 0;
3062
- const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
3063
- 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);
3064
- const baseConversationId = resolveSlackBaseConversationId({
3065
- message,
3066
- isDirectMessage,
3067
- eventScope
3068
- });
3069
- const runtimeBindingThreadId = routedThreadId ?? (isDirectMessage && isThreadReply ? threadTs : void 0);
3070
- const bindingRoute = resolveSlackConversationBindingRoute({
3071
- cfg: ctx.cfg,
3072
- route,
3073
- accountId: account.accountId,
3074
- baseConversationId,
3075
- runtimeBindingThreadId,
3076
- bindingsEnabled: !eventScope
3077
- });
3078
- const runtimeRoute = bindingRoute.runtimeRoute;
3079
- const configuredBinding = bindingRoute.configuredRoute?.bindingResolution ?? null;
3080
- const configuredBindingSessionKey = bindingRoute.configuredRoute?.boundSessionKey ?? "";
3081
- route = bindingRoute.route;
3082
- const threadKeys = runtimeRoute.boundSessionKey || configuredBindingSessionKey ? {
3083
- sessionKey: route.sessionKey,
3084
- parentSessionKey: void 0
3085
- } : resolveThreadSessionKeys({
3086
- baseSessionKey: route.sessionKey,
3087
- threadId: routedThreadId,
3088
- parentSessionKey: routedThreadId && ctx.threadInheritParent ? route.sessionKey : void 0
3089
- });
3090
- const sessionKey = threadKeys.sessionKey;
3091
- const historyKey = isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : eventScope ? `${account.accountId}:${eventScope.teamId}:${message.channel}` : message.channel;
3092
- return {
3093
- route,
3094
- runtimeBinding: runtimeRoute.bindingRecord,
3095
- runtimeBoundSessionKey: runtimeRoute.boundSessionKey,
3096
- configuredBinding,
3097
- configuredBindingSessionKey,
3098
- chatType,
3099
- replyToMode,
3100
- threadContext,
3101
- threadTs,
3102
- isThreadReply,
3103
- threadKeys,
3104
- sessionKey,
3105
- historyKey
3106
- };
3107
- }
3108
- //#endregion
3109
2954
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context-root.ts
3110
2955
  function isSlackThreadAuthorCurrentBot(params) {
3111
2956
  const { identity, author } = params;
@@ -3161,7 +3006,7 @@ function formatSlackThreadLabelSnippet(text) {
3161
3006
  }
3162
3007
  //#endregion
3163
3008
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
3164
- const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-fh66A6lc.js").then((n) => n._));
3009
+ const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-CFkvEv1d.js").then((n) => n._));
3165
3010
  const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
3166
3011
  function resolveSlackThreadSessionFreshness(params) {
3167
3012
  return params.ctx.channelRuntime?.session?.resolveEntryResetFreshness?.({
@@ -3534,25 +3379,12 @@ async function restoreSlackAssistantThreadContextFromMetadata(params) {
3534
3379
  const parentUserId = params.message.parent_user_id?.trim();
3535
3380
  if (!params.message.channel || !threadTs || !parentUserId || parentUserId !== params.ctx.botUserId && parentUserId !== params.ctx.botId) return;
3536
3381
  try {
3537
- const response = await (params.eventScope?.client ?? params.ctx.app.client).conversations.replies({
3538
- channel: params.message.channel,
3539
- ts: threadTs,
3540
- oldest: threadTs,
3541
- include_all_metadata: true,
3542
- 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
3543
3387
  });
3544
- for (const message of response.messages ?? []) {
3545
- const context = parseSlackAssistantThreadMetadata(message.metadata);
3546
- if (!context) continue;
3547
- return {
3548
- assistantChannelId: params.message.channel,
3549
- threadTs,
3550
- userId: params.message.user,
3551
- channelId: context.channelId,
3552
- teamId: context.teamId,
3553
- enterpriseId: context.enterpriseId
3554
- };
3555
- }
3556
3388
  } catch (err) {
3557
3389
  logVerbose(`slack assistant context restore failed channel=${params.message.channel} ts=${threadTs}: ${formatErrorMessage(err)}`);
3558
3390
  }
@@ -4262,7 +4094,7 @@ async function prepareSlackMessage(params) {
4262
4094
  const statusReactionsExplicitlyEnabled = cfg.messages?.statusReactions?.enabled === true;
4263
4095
  const isRoomEvent = inboundEventKind === "room_event";
4264
4096
  const shouldAckReaction$1 = () => Boolean(ackReaction && shouldAckReaction({
4265
- scope: ctx.ackReactionScope,
4097
+ scope: cfg.messages?.ackReactionScope,
4266
4098
  inboundEventKind,
4267
4099
  isDirect: isDirectMessage,
4268
4100
  isGroup: isRoomish,
@@ -4310,10 +4142,11 @@ async function prepareSlackMessage(params) {
4310
4142
  const textWithId = `${bodyForAgent}\n[slack message id: ${message.ts} channel: ${message.channel}${threadInfo}]`;
4311
4143
  const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId });
4312
4144
  const envelopeOptions = resolveEnvelopeFormatOptions(ctx.cfg);
4313
- const previousTimestamp = readSessionUpdatedAt({
4145
+ const sessionEntry = getSessionEntry({
4314
4146
  storePath,
4315
4147
  sessionKey
4316
4148
  });
4149
+ const previousTimestamp = sessionEntry?.updatedAt;
4317
4150
  if (opts.source === "app_mention" && !ctx.botUserId && message.ts) {
4318
4151
  const pendingHistory = ctx.channelHistories.get(historyKey);
4319
4152
  if (pendingHistory) ctx.channelHistories.set(historyKey, pendingHistory.filter((entry) => entry.messageId !== message.ts));
@@ -4547,6 +4380,7 @@ async function prepareSlackMessage(params) {
4547
4380
  channelConfig,
4548
4381
  replyTarget,
4549
4382
  ctxPayload,
4383
+ sessionDisplayName: sessionEntry?.displayName,
4550
4384
  turn: {
4551
4385
  storePath,
4552
4386
  record: {