@openclaw/slack 2026.7.2-beta.7 → 2026.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/{account-inspect-DEKpg9Ip.js → account-inspect-Drw2gAiN.js} +1 -1
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/{accounts-CHAeNZOX.js → accounts-CK4AVI2_.js} +2 -2
  4. package/dist/{accounts.runtime-BHD4Ma2R.js → accounts.runtime-Cb7QwWBz.js} +1 -1
  5. package/dist/{action-runtime-DfuYoMjG.js → action-runtime-BkGe7ny_.js} +148 -76
  6. package/dist/action-runtime.runtime-D9I6uc0Q.js +2 -0
  7. package/dist/{actions-D3NuJvva.js → actions-BTDXwwE3.js} +85 -44
  8. package/dist/actions.runtime-DLFyUkvg.js +2 -0
  9. package/dist/api.js +15 -15
  10. package/dist/{approval-handler.runtime-BA05P0QT.js → approval-handler.runtime-CJaNzAq2.js} +51 -14
  11. package/dist/{block-text-DkHaoGem.js → block-text-YtiNkqmx.js} +1 -1
  12. package/dist/{channel-B9fVdW3s.js → channel-D_tovcUL.js} +151 -91
  13. package/dist/channel-config-api.js +1 -1
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/{channel-type-DXyos1d2.js → channel-type-B9MSdoGN.js} +9 -8
  16. package/dist/{config-adapter-CxBQH8fq.js → channel.setup-Bch9dYmA.js} +83 -25
  17. package/dist/{config-schema-SgZT6nXV.js → config-schema-xto5jFIx.js} +5 -11
  18. package/dist/{directory-config-CnCIAktf.js → directory-config-CNBz8lJa.js} +3 -3
  19. package/dist/directory-contract-api.js +1 -1
  20. package/dist/{directory-live-DKunWh3A.js → directory-live-CFKUtW9E.js} +13 -22
  21. package/dist/{doctor-contract-CsGH1Dfc.js → doctor-contract-BAy7-EUQ.js} +30 -3
  22. package/dist/doctor-contract-api.js +1 -1
  23. package/dist/{group-policy-NQZti9t-.js → group-policy-c4RP9bjc.js} +248 -163
  24. package/dist/index.js +4 -1
  25. package/dist/{message-tool-api-DkhFpMoy.js → message-tool-api-BykQB4AU.js} +14 -3
  26. package/dist/message-tool-api.js +1 -1
  27. package/dist/{monitor-CmKz0wtI.js → monitor-1nF1NtqT.js} +3 -3
  28. package/dist/{outbound-adapter-BkuMp2Vs.js → outbound-adapter-S81tsswN.js} +19 -17
  29. package/dist/{pipeline.runtime-DgtmuYCP.js → pipeline.runtime-BvYsM5JO.js} +1087 -682
  30. package/dist/{policy-PRysPn23.js → policy-TapwbeaT.js} +18 -14
  31. package/dist/{probe-CgDsSops.js → probe-HoEg_WxB.js} +38 -18
  32. package/dist/{provider-DmRizMZt.js → provider-CDNvBXUi.js} +3422 -2590
  33. package/dist/{relay-source-Blxj8xb4.js → relay-source-C9leF11u.js} +11 -21
  34. package/dist/{replies-4s_Vq_z9.js → replies-CeOfv82I.js} +52 -6
  35. package/dist/{resolve-channels-yEqmM-3I.js → resolve-channels-CNPrxaXY.js} +39 -12
  36. package/dist/{resolve-users-BOrt5K-5.js → resolve-users-Dzomuwnr.js} +28 -5
  37. package/dist/{runtime-api-QMb-Q5Ko.js → runtime-api-Cj9nxBNv.js} +3 -3
  38. package/dist/runtime-api.js +11 -11
  39. package/dist/{scopes-BJSrkAp6.js → scopes-cYXKkrRD.js} +1 -1
  40. package/dist/{send-DsLeZpST.js → send-Nor0iBwY.js} +211 -105
  41. package/dist/{send.runtime-gUdK6V3h.js → send.runtime-BmgNe6S9.js} +1 -1
  42. package/dist/setup-entry.js +1 -4
  43. package/dist/setup-plugin-api.js +1 -1
  44. package/dist/{setup-surface-BeNzrMrJ.js → setup-surface-CbX1-Knh.js} +6 -6
  45. package/dist/{shared-DgIN3Dcr.js → shared-zdT5AMp-.js} +36 -42
  46. package/dist/{slash-dispatch.runtime-zPkyl7dB.js → slash-dispatch.runtime-Bnr9GR1S.js} +1 -1
  47. package/dist/{streaming-compat-3_X-21wW.js → streaming-compat-B-e0mqM0.js} +3 -8
  48. package/dist/{target-parsing-N6lHogYZ.js → target-parsing-BnMD2ZqZ.js} +40 -2
  49. package/dist/{targets-KmjP2E8s.js → targets-Cx5W_n3W.js} +1 -1
  50. package/node_modules/@slack/bolt/node_modules/@types/node/README.md +1 -1
  51. package/node_modules/@slack/bolt/node_modules/@types/node/crypto.d.ts +1 -1
  52. package/node_modules/@slack/bolt/node_modules/@types/node/package.json +2 -2
  53. package/node_modules/@slack/socket-mode/node_modules/@types/node/README.md +1 -1
  54. package/node_modules/@slack/socket-mode/node_modules/@types/node/crypto.d.ts +1 -1
  55. package/node_modules/@slack/socket-mode/node_modules/@types/node/package.json +2 -2
  56. package/node_modules/@slack/web-api/node_modules/@types/node/README.md +1 -1
  57. package/node_modules/@slack/web-api/node_modules/@types/node/crypto.d.ts +1 -1
  58. package/node_modules/@slack/web-api/node_modules/@types/node/package.json +2 -2
  59. package/node_modules/undici/lib/cache/memory-cache-store.js +54 -9
  60. package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
  61. package/node_modules/undici/lib/core/request.js +12 -1
  62. package/node_modules/undici/lib/dispatcher/client-h1.js +11 -2
  63. package/node_modules/undici/lib/handler/cache-handler.js +246 -64
  64. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +3 -3
  65. package/node_modules/undici/lib/handler/retry-handler.js +24 -0
  66. package/node_modules/undici/lib/interceptor/cache.js +159 -52
  67. package/node_modules/undici/lib/util/cache.js +324 -62
  68. package/node_modules/undici/lib/util/date.js +23 -6
  69. package/node_modules/undici/lib/web/cookies/util.js +79 -9
  70. package/node_modules/undici/package.json +1 -1
  71. package/openclaw.plugin.json +239 -34
  72. package/package.json +9 -9
  73. package/skills/slack/SKILL.md +10 -68
  74. package/dist/action-runtime.runtime-DQErZYpz.js +0 -2
  75. package/dist/actions.runtime-BIRU_G4M.js +0 -2
  76. package/dist/channel.setup-CLP5oTvh.js +0 -56
  77. package/dist/preflight-audio.runtime-D5Bgags0.js +0 -3
  78. package/dist/slash-plugin-commands.runtime-Dosfcx3X.js +0 -2
@@ -1,18 +1,18 @@
1
1
  import { t as resolveSlackReplyToMode } from "./account-reply-mode-DrUXogvO.js";
2
- import { i as parseSlackTarget } from "./target-parsing-N6lHogYZ.js";
3
- import "./targets-KmjP2E8s.js";
4
- import { $ as escapeSlackMrkdwn, A as normalizeSlackOutboundText, C as hasSlackNativeDataBlock, F as SLACK_EDIT_TEXT_MAX_BYTES, H as truncateSlackText, L as SLACK_TEXT_LIMIT, U as truncateSlackTextByUtf8Bytes, _ as resolveSlackReplyRenderPlan, c as normalizeSlackAllowOwnerEntry, j as buildSlackBlocksFallbackText, s as normalizeAllowListLower, u as resolveSlackAllowListMatch } from "./group-policy-NQZti9t-.js";
5
- import { r as formatSlackError } from "./probe-CgDsSops.js";
6
- import { n as resolveSlackNativeStreaming, r as resolveSlackStreamingMode, t as mapStreamingModeToSlackLegacyDraftStreamMode } from "./streaming-compat-3_X-21wW.js";
7
- import { _ as resolveSlackThreadHistory, b as buildSlackEditTextPayload, f as removeSlackReaction, l as reactSlackMessage, r as editSlackMessage, t as deleteSlackMessage, v as resolveSlackThreadStarter, y as formatSlackFileReference } from "./actions-D3NuJvva.js";
8
- import { a as resolveSlackMessageText } from "./block-text-DkHaoGem.js";
9
- import { c as hasSlackThreadParticipationWithPersistence, l as recordSlackThreadParticipation, r as sendMessageSlack, u as buildSlackNativeDataDeliveryPlan } from "./send-DsLeZpST.js";
10
- import { C as stripSlackMentionsForCommandDetection, _ as readSessionUpdatedAt, b as resolveStorePath, d as resolveSlackCommandIngress, f as resolveSlackEffectiveAllowFrom, g as resolveSlackChatType, h as normalizeSlackChannelType, l as resolveConversationLabel$1, m as parseSlackAssistantThreadMetadata, o as authorizeSlackDirectMessage, p as buildSlackAssistantThreadMetadata, r as resolveSlackRoomContextHints, u as authorizeSlackBotRoomMessage, v as resolveChannelContextVisibilityMode, x as updateLastRoute, y as resolveChannelResetConfig } from "./provider-DmRizMZt.js";
11
- import { n as resolveSlackChannelConfig } from "./policy-PRysPn23.js";
12
- import { a as resolveDeliveredSlackReplyThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as resolveSlackThreadTs, s as emitSlackMessageSentHooks, t as createSlackReplyDeliveryPlan } from "./replies-4s_Vq_z9.js";
2
+ import { a as parseSlackTarget, n as formatSlackTarget } from "./target-parsing-BnMD2ZqZ.js";
3
+ import "./targets-Cx5W_n3W.js";
4
+ import { B as SLACK_TEXT_LIMIT, G as truncateSlackText, K as truncateSlackTextByUtf8Bytes, L as SLACK_EDIT_TEXT_MAX_BYTES, M as normalizeSlackOutboundText, N as buildSlackBlocksFallbackText, T as hasSlackNativeDataBlock, W as countSlackTextUtf8Bytes, Z as SLACK_SESSION_LINK_ACTION_ID, at as escapeSlackMrkdwn, c as normalizeSlackAllowOwnerEntry, s as normalizeAllowListLower, u as resolveSlackAllowListMatch, y as resolveSlackReplyRenderPlan } from "./group-policy-c4RP9bjc.js";
5
+ import { r as formatSlackError } from "./probe-HoEg_WxB.js";
6
+ import { n as resolveSlackStreamingMode, t as resolveSlackNativeStreaming } from "./streaming-compat-B-e0mqM0.js";
7
+ import { b as formatSlackFileReference, i as editSlackRenderedMessage, p as removeSlackReaction, r as editSlackMessage, t as deleteSlackMessage, u as reactSlackMessage, v as resolveSlackThreadHistory, x as buildSlackEditTextPayload, y as resolveSlackThreadStarter } from "./actions-BTDXwwE3.js";
8
+ import { a as resolveSlackMessageText } from "./block-text-YtiNkqmx.js";
9
+ import { c as hasSlackThreadFailureNotice, d as recordSlackThreadFailureNotice, f as recordSlackThreadParticipation, l as hasSlackThreadParticipation, o as clearSlackThreadFailureNotice, p as buildSlackNativeDataDeliveryPlan, r as sendMessageSlack, u as hasSlackThreadParticipationWithPersistence } from "./send-Nor0iBwY.js";
10
+ import { C as resolveChannelResetConfig, D as stripSlackMentionsForCommandDetection, S as resolveChannelContextVisibilityMode, T as updateLastRoute, _ as normalizeSlackChannelType, b as parseSlackAssistantThreadMetadata, c as resolveConversationLabel$1, d as resolveSlackEffectiveAllowFrom, g as resolveSlackEnterpriseMainDmSessionKey, h as qualifySlackRoutePeerId, l as authorizeSlackBotRoomMessage, m as qualifySlackConversationId, o as authorizeSlackDirectMessage, r as resolveSlackRoomContextHints, s as trackSlackDraftMessage, u as resolveSlackCommandIngress, v as resolveSlackChatType, w as resolveStorePath, x as readSessionUpdatedAt, y as buildSlackAssistantThreadMetadata } from "./provider-CDNvBXUi.js";
11
+ import { n as resolveSlackChannelConfig } from "./policy-TapwbeaT.js";
12
+ import { a as resolveDeliveredSlackReplyThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as resolveSlackThreadTs, s as emitSlackMessageSentHooks, t as createSlackReplyDeliveryPlan } from "./replies-CeOfv82I.js";
13
13
  import { asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
14
14
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
15
- import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createChannelProgressReceiptTracker, createDraftStreamLoop, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, formatPlanChecklistLines, isChannelProgressDraftWorkToolName, mergeChannelProgressDraftLine, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelProgressDraftMaxLines, resolveChannelProgressDraftRender, resolveChannelStreamingBlockEnabled, resolveChannelStreamingNativeTransport, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
15
+ import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createChannelProgressReceiptTracker, createFinalizableDraftStreamControlsForState, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, formatPlanChecklistLines, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
16
16
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
17
17
  import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
18
18
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -20,21 +20,24 @@ import { createHash } from "node:crypto";
20
20
  import fs from "node:fs/promises";
21
21
  import { formatErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
22
22
  import { asDateTimestampMs, asFiniteNumberInRange, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
23
- import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
23
+ import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, isReplyPayloadNonTerminalToolErrorWarning, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
24
24
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist, shouldIncludeSupplementalContext } from "openclaw/plugin-sdk/security-runtime";
25
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
25
26
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
26
27
  import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
27
- import { resolveDefaultAgentId, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
28
28
  import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime";
29
- import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
30
- import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
29
+ import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
31
30
  import { resolveChannelImplicitMentions } from "openclaw/plugin-sdk/channel-ingress-runtime";
32
- import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundTurn, formatInboundEnvelope, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, logInboundDrop, matchesMentionWithExplicit, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundSupplementalSenderAllowed, resolveUnmentionedGroupInboundPolicy, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
31
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
32
+ import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundTurn, formatInboundEnvelope, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, logInboundDrop, matchesMentionWithExplicit, readAgentRunTerminalOutcome, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundSupplementalSenderAllowed, resolveUnmentionedGroupInboundPolicy, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
33
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
34
+ import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
35
+ import { buildControlUiSessionPath } from "openclaw/plugin-sdk/session-discussion";
33
36
  import { DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
34
37
  import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
35
38
  import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
36
39
  import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-surface";
37
- import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
40
+ import { createChannelPreflightAudio } from "openclaw/plugin-sdk/media-understanding-runtime";
38
41
  import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
39
42
  //#region extensions/slack/src/streaming.ts
40
43
  /**
@@ -133,7 +136,7 @@ async function appendSlackStream(params) {
133
136
  * Stop (finalize) a Slack stream.
134
137
  *
135
138
  * After calling this the stream message becomes a normal Slack message.
136
- * Optionally include final text to append before stopping.
139
+ * Optionally include final chunks to append before stopping.
137
140
  *
138
141
  * If Slack's `chat.stopStream` responds with a definitive recipient/channel
139
142
  * rejection while text is still buffered locally, this function throws a
@@ -152,17 +155,15 @@ async function appendSlackStream(params) {
152
155
  * caller can emit the `message_sent` hook with a populated `messageId`.
153
156
  */
154
157
  async function stopSlackStream(params) {
155
- const { session, text, chunks, metadata } = params;
158
+ const { session, chunks, metadata } = params;
156
159
  if (session.stopped) {
157
160
  logVerbose("slack-stream: stream already stopped, ignoring duplicate stop");
158
161
  return {};
159
162
  }
160
163
  session.stopped = true;
161
- if (text) session.pendingText += text;
162
- logVerbose(`slack-stream: stopping stream in ${session.channel} thread=${session.threadTs}${text ? ` (final text: ${text.length} chars)` : ""}`);
164
+ logVerbose(`slack-stream: stopping stream in ${session.channel} thread=${session.threadTs}`);
163
165
  try {
164
- const stopResponse = await session.streamer.stop(text || chunks?.length || metadata ? {
165
- ...text ? { markdown_text: text } : {},
166
+ const stopResponse = await session.streamer.stop(chunks?.length || metadata ? {
166
167
  ...chunks?.length ? { chunks } : {},
167
168
  ...metadata ? { metadata } : {}
168
169
  } : void 0);
@@ -212,7 +213,7 @@ function extractSlackErrorCode(err) {
212
213
  return (typeof record.message === "string" ? record.message : "").match(/An API error occurred:\s*([a-z_][a-z0-9_]*)/i)?.[1];
213
214
  }
214
215
  function markSlackStreamFallbackDelivered(session) {
215
- const nativeStreamWasStarted = session.delivered;
216
+ const nativeStreamWasStarted = session.delivered || Boolean(session.streamer.ts);
216
217
  session.pendingText = "";
217
218
  session.streamer.buffer = "";
218
219
  session.stopped = !nativeStreamWasStarted;
@@ -256,12 +257,6 @@ function isSlackStreamingEnabled(params) {
256
257
  if (params.mode === "progress") return params.nativeStreaming && params.nativeProgressTaskCards === true;
257
258
  return false;
258
259
  }
259
- function shouldEnableSlackPreviewStreaming(params) {
260
- return params.mode !== "off";
261
- }
262
- function shouldInitializeSlackDraftStream(params) {
263
- return params.previewStreamingEnabled && !params.useStreaming;
264
- }
265
260
  function resolveSlackDisableBlockStreaming(params) {
266
261
  if (params.useStreaming || params.shouldUseDraftStream) return true;
267
262
  return typeof params.blockStreamingEnabled === "boolean" ? !params.blockStreamingEnabled : void 0;
@@ -274,9 +269,10 @@ function resolveExplicitSlackProgressTitle(entry) {
274
269
  }
275
270
  function resolveSlackNativeProgressTaskCards(entry) {
276
271
  const streaming = entry?.streaming;
277
- if (!streaming || typeof streaming !== "object" || Array.isArray(streaming)) return false;
272
+ if (!streaming || typeof streaming !== "object" || Array.isArray(streaming)) return true;
278
273
  const progressConfig = streaming.progress;
279
- return Boolean(progressConfig) && typeof progressConfig === "object" && !Array.isArray(progressConfig) && progressConfig.nativeTaskCards === true;
274
+ if (!progressConfig || typeof progressConfig !== "object" || Array.isArray(progressConfig)) return true;
275
+ return progressConfig.nativeTaskCards !== false;
280
276
  }
281
277
  function resolveSlackStreamingThreadHint(params) {
282
278
  return resolveSlackThreadTs({
@@ -386,21 +382,27 @@ function createSlackDraftStream(params) {
386
382
  const remove = params.remove ?? deleteSlackMessage;
387
383
  let streamMessageId;
388
384
  let streamChannelId;
385
+ let untrackConversationBoundary;
386
+ let lastVisibleUpdate;
389
387
  let lastSentKey = "";
390
- let pendingUpdate;
391
- let stopped = false;
388
+ const detachedMessages = [];
389
+ const finalizedMessageIds = /* @__PURE__ */ new Set();
390
+ const streamState = {
391
+ stopped: false,
392
+ final: false
393
+ };
392
394
  const normalizeUpdate = (update) => typeof update === "string" ? { text: update } : update;
393
- const sendOrEditStreamMessage = async (text) => {
394
- if (stopped) return;
395
- const trimmed = text.trimEnd();
395
+ const sendOrEditStreamMessage = async (pending) => {
396
+ if (streamState.stopped) return;
397
+ const update = normalizeUpdate(pending);
398
+ const trimmed = update.text.trimEnd();
396
399
  if (!trimmed) return;
397
400
  if (trimmed.length > maxChars) {
398
- stopped = true;
401
+ streamState.stopped = true;
399
402
  params.warn?.(`slack stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
400
403
  return;
401
404
  }
402
- const update = normalizeUpdate(pendingUpdate ?? text);
403
- const blocks = update.text === text ? update.blocks : void 0;
405
+ const blocks = update.blocks;
404
406
  const sentKey = `${trimmed}\n${blocks ? JSON.stringify(blocks) : ""}`;
405
407
  if (sentKey === lastSentKey) return;
406
408
  lastSentKey = sentKey;
@@ -413,56 +415,73 @@ function createSlackDraftStream(params) {
413
415
  ...params.eventScope ? { client: params.eventScope.client } : {},
414
416
  ...blocks ? { blocks } : {}
415
417
  });
418
+ lastVisibleUpdate = {
419
+ text: trimmed,
420
+ ...blocks ? { blocks } : {}
421
+ };
416
422
  return;
417
423
  }
424
+ const threadTs = params.resolveThreadTs?.();
425
+ const pendingBoundary = params.conversationChannelId ? trackSlackDraftMessage({
426
+ accountId: params.accountId,
427
+ teamId: params.eventScope?.teamId,
428
+ channelId: params.conversationChannelId,
429
+ threadTs,
430
+ onInterveningMessage: forceNewMessage
431
+ }) : void 0;
432
+ untrackConversationBoundary = pendingBoundary?.stop;
418
433
  const sent = await send(params.target, trimmed, {
419
434
  cfg: params.cfg,
420
435
  token: params.token,
421
436
  accountId: params.accountId,
422
- threadTs: params.resolveThreadTs?.(),
437
+ threadTs,
423
438
  identity: params.identity,
424
- ...params.eventScope ? {
425
- client: params.eventScope.client,
426
- enterpriseEventScope: params.eventScope
427
- } : {},
439
+ eventScope: params.eventScope,
428
440
  ...params.metadata ? { metadata: params.metadata } : {},
429
441
  ...blocks ? { blocks } : {}
430
442
  });
431
443
  streamChannelId = sent.channelId || streamChannelId;
432
444
  streamMessageId = sent.messageId || streamMessageId;
433
445
  if (!streamChannelId || !streamMessageId) {
434
- stopped = true;
446
+ stopTrackingConversationBoundary();
447
+ streamState.stopped = true;
435
448
  params.warn?.("slack stream preview stopped (missing identifiers from sendMessage)");
436
449
  return;
437
450
  }
438
- params.onMessageSent?.();
451
+ lastVisibleUpdate = {
452
+ text: trimmed,
453
+ ...blocks ? { blocks } : {}
454
+ };
455
+ if (pendingBoundary && params.conversationChannelId === streamChannelId) pendingBoundary.setMessageTs(streamMessageId);
456
+ else {
457
+ stopTrackingConversationBoundary();
458
+ untrackConversationBoundary = trackSlackDraftMessage({
459
+ accountId: params.accountId,
460
+ teamId: params.eventScope?.teamId,
461
+ channelId: streamChannelId,
462
+ threadTs,
463
+ messageTs: streamMessageId,
464
+ onInterveningMessage: forceNewMessage
465
+ }).stop;
466
+ }
439
467
  } catch (err) {
440
- stopped = true;
468
+ stopTrackingConversationBoundary();
469
+ streamState.stopped = true;
441
470
  params.warn?.(`slack stream preview failed: ${formatSlackError(err)}`);
442
471
  }
443
472
  };
444
- const loop = createDraftStreamLoop({
473
+ const { loop, update, discardPending, seal } = createFinalizableDraftStreamControlsForState({
445
474
  throttleMs,
446
- isStopped: () => stopped,
447
- sendOrEditStreamMessage
475
+ state: streamState,
476
+ sendOrEditStreamMessage,
477
+ emptyValue: "",
478
+ isEmpty: (value) => !normalizeUpdate(value).text.trim()
448
479
  });
449
- const stop = () => {
450
- stopped = true;
451
- loop.stop();
480
+ const stopTrackingConversationBoundary = () => {
481
+ untrackConversationBoundary?.();
482
+ untrackConversationBoundary = void 0;
452
483
  };
453
- const discardPending = async () => {
454
- stop();
455
- await loop.waitForInFlight();
456
- };
457
- const clear = async () => {
458
- await discardPending();
459
- const channelId = streamChannelId;
460
- const messageId = streamMessageId;
461
- streamChannelId = void 0;
462
- streamMessageId = void 0;
463
- lastSentKey = "";
464
- pendingUpdate = void 0;
465
- if (!channelId || !messageId) return;
484
+ const removeMessage = async (channelId, messageId) => {
466
485
  try {
467
486
  await remove(channelId, messageId, {
468
487
  token: params.token,
@@ -473,26 +492,76 @@ function createSlackDraftStream(params) {
473
492
  params.warn?.(`slack stream preview cleanup failed: ${formatSlackError(err)}`);
474
493
  }
475
494
  };
495
+ const clear = async () => {
496
+ stopTrackingConversationBoundary();
497
+ await discardPending();
498
+ const channelId = streamChannelId;
499
+ const messageId = streamMessageId;
500
+ streamChannelId = void 0;
501
+ streamMessageId = void 0;
502
+ lastVisibleUpdate = void 0;
503
+ lastSentKey = "";
504
+ if (!channelId || !messageId) return;
505
+ await removeMessage(channelId, messageId);
506
+ };
476
507
  const forceNewMessage = () => {
508
+ stopTrackingConversationBoundary();
509
+ streamState.stopped = false;
510
+ streamState.final = false;
511
+ if (streamChannelId && streamMessageId && !finalizedMessageIds.has(streamMessageId)) detachedMessages.push({
512
+ channelId: streamChannelId,
513
+ messageId: streamMessageId
514
+ });
477
515
  streamMessageId = void 0;
478
516
  streamChannelId = void 0;
517
+ lastVisibleUpdate = void 0;
479
518
  lastSentKey = "";
480
- pendingUpdate = void 0;
481
519
  loop.resetPending();
482
520
  };
521
+ const dropDetachedMessages = async () => {
522
+ let message;
523
+ while ((message = detachedMessages.shift()) !== void 0) {
524
+ const { channelId, messageId } = message;
525
+ await removeMessage(channelId, messageId);
526
+ }
527
+ };
528
+ const discardPendingAndStopTracking = async () => {
529
+ stopTrackingConversationBoundary();
530
+ await discardPending();
531
+ };
532
+ const finalizeMessage = async (messageId, editFinal) => {
533
+ const channelId = streamChannelId;
534
+ const previousUpdate = lastVisibleUpdate;
535
+ if (!channelId || streamMessageId !== messageId || !previousUpdate) return false;
536
+ await editFinal();
537
+ if (streamChannelId === channelId && streamMessageId === messageId) {
538
+ finalizedMessageIds.add(messageId);
539
+ stopTrackingConversationBoundary();
540
+ return true;
541
+ }
542
+ try {
543
+ await edit(channelId, messageId, previousUpdate.text, {
544
+ cfg: params.cfg,
545
+ token: params.token,
546
+ accountId: params.accountId,
547
+ ...params.eventScope ? { client: params.eventScope.client } : {},
548
+ ...previousUpdate.blocks ? { blocks: previousUpdate.blocks } : {}
549
+ });
550
+ } catch (err) {
551
+ params.warn?.(`slack stream preview restore failed: ${formatSlackError(err)}`);
552
+ }
553
+ return false;
554
+ };
483
555
  params.log?.(`slack stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
484
556
  return {
485
- update: (update) => {
486
- const normalized = normalizeUpdate(update);
487
- pendingUpdate = update;
488
- loop.update(normalized.text);
489
- },
557
+ update,
490
558
  flush: loop.flush,
491
559
  clear,
492
- discardPending,
493
- seal: discardPending,
494
- stop,
560
+ discardPending: discardPendingAndStopTracking,
561
+ seal,
495
562
  forceNewMessage,
563
+ dropDetachedMessages,
564
+ finalizeMessage,
496
565
  messageId: () => streamMessageId,
497
566
  channelId: () => streamChannelId
498
567
  };
@@ -505,6 +574,14 @@ const DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS = 48;
505
574
  const SLACK_PROGRESS_CHUNK_TEXT_MAX = 256;
506
575
  const SLACK_PROGRESS_TASK_TITLE_MAX = 120;
507
576
  const SLACK_PROGRESS_PLAN_FALLBACK_TITLE = "Thinking";
577
+ const SLACK_PROGRESS_LINE_DELTA_RE = /(?:^|\s)\+(\d+)\s+[−-](\d+)(?=\s|$)/u;
578
+ function buildSessionSources(url) {
579
+ return [{
580
+ type: "url_source",
581
+ url,
582
+ text: "Open in OpenClaw"
583
+ }];
584
+ }
508
585
  function field(text) {
509
586
  return {
510
587
  type: "mrkdwn",
@@ -535,18 +612,29 @@ function lineDetailParts(line) {
535
612
  function legacyLineTitle(line) {
536
613
  return `${line.icon ?? "•"} *${escapeSlackMrkdwn(line.label)}*`;
537
614
  }
615
+ function isAuthoredProgressLine(line) {
616
+ return line.id === "reasoning" || line.id?.startsWith("commentary:") === true;
617
+ }
538
618
  function legacyLineDetail(line, maxChars) {
539
619
  const detail = lineDetailParts(line).join(" · ");
540
- return detail ? escapeSlackMrkdwn(compactDetail(detail, maxChars)) : "—";
620
+ if (detail) return escapeSlackMrkdwn(compactDetail(detail, maxChars));
621
+ if (isAuthoredProgressLine(line)) return normalizeSlackOutboundText(compactDetail(line.text.replace(/^(?:🧠|💬)\s+/u, ""), maxChars));
622
+ return "—";
541
623
  }
542
- function lineTaskTitle(line, maxLineChars) {
543
- const label = line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
544
- const detail = lineDetailParts(line).join(" · ") || line.status?.trim();
624
+ function lineTaskTitle(line) {
625
+ const label = (line.kind === "command-output" ? line.toolName : void 0) || line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
545
626
  const fallback = line.text.replace(/\s+/g, " ").trim();
546
- if (detail) return compactTitle(`${label} ${compactDetail(detail, maxLineChars)}`);
547
- if (fallback && fallback !== label) return compactTitle(fallback);
627
+ if (fallback && fallback !== label) return compactTitle(lineDetailParts(line).length > 0 || line.status ? label : fallback);
548
628
  return compactTitle(label);
549
629
  }
630
+ function lineTaskDetails(line, maxLineChars) {
631
+ const detail = (lineDetailParts(line).join(" · ") || line.status?.trim())?.replace(SLACK_PROGRESS_LINE_DELTA_RE, "").replace(/\s+·\s*$/u, "").trim();
632
+ return detail ? compactDetail(detail, maxLineChars) : void 0;
633
+ }
634
+ function lineTaskOutput(line) {
635
+ const match = SLACK_PROGRESS_LINE_DELTA_RE.exec(lineDetailParts(line).join(" · "));
636
+ return match ? `+${match[1]} −${match[2]}` : void 0;
637
+ }
550
638
  function lineTaskStatus(line) {
551
639
  const normalized = line.status?.replace(/\s+/g, " ").trim().toLowerCase();
552
640
  if (!normalized) return "in_progress";
@@ -594,15 +682,20 @@ function buildPlanTasks(params) {
594
682
  contentIdOccurrences.set(id, occurrence);
595
683
  id = `${id}_${occurrence}`;
596
684
  }
597
- return {
685
+ const details = lineTaskDetails(line, maxLineChars);
686
+ const output = lineTaskOutput(line);
687
+ const task = {
598
688
  id,
599
- title: lineTaskTitle(line, maxLineChars),
689
+ title: lineTaskTitle(line),
600
690
  status: lineTaskStatus(line)
601
691
  };
692
+ if (details) task.details = details;
693
+ if (output) task.output = output;
694
+ return task;
602
695
  });
603
696
  }
604
697
  function resolvePlanTitle(params) {
605
- return compactChunkText(params.title?.trim() || params.label?.trim() || params.tasks.at(-1)?.title || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
698
+ return compactChunkText(params.title?.trim() || params.label?.trim() || (params.tasks.at(-1)?.details ? `${params.tasks.at(-1)?.title} — ${params.tasks.at(-1)?.details}` : params.tasks.at(-1)?.title) || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
606
699
  }
607
700
  function buildSlackProgressStreamChunks(params) {
608
701
  const tasks = buildPlanTasks({
@@ -612,38 +705,92 @@ function buildSlackProgressStreamChunks(params) {
612
705
  });
613
706
  if (tasks.length === 0) {
614
707
  const title = params.title?.trim() || params.label?.trim();
615
- return title ? [{
708
+ if (!title) return;
709
+ if (!params.sessionUrl && !params.diffStat) return [{
710
+ type: "plan_update",
711
+ title: compactChunkText(title)
712
+ }];
713
+ return [{
616
714
  type: "plan_update",
617
715
  title: compactChunkText(title)
618
- }] : void 0;
716
+ }, {
717
+ type: "task_update",
718
+ id: "openclaw_summary",
719
+ title: "Completed",
720
+ status: "complete",
721
+ ...formatTaskDiffOutput(params.diffStat) ? { output: formatTaskDiffOutput(params.diffStat) } : {},
722
+ ...params.sessionUrl ? { sources: buildSessionSources(params.sessionUrl) } : {}
723
+ }];
619
724
  }
725
+ const title = resolvePlanTitle({
726
+ label: params.label,
727
+ title: params.title,
728
+ tasks
729
+ });
730
+ const finalTaskIndex = tasks.length - 1;
731
+ const diffOutput = formatTaskDiffOutput(params.diffStat);
732
+ const taskChunks = tasks.map((task, index) => {
733
+ const chunk = {
734
+ type: "task_update",
735
+ id: task.id,
736
+ title: task.title,
737
+ status: task.status === "in_progress" ? params.finalInProgressStatus ?? (params.completeInProgress ? "complete" : task.status) : task.status
738
+ };
739
+ if (task.details) chunk.details = task.details;
740
+ if (task.output) chunk.output = task.output;
741
+ if (index === finalTaskIndex && diffOutput) chunk.output = diffOutput;
742
+ if (index === finalTaskIndex && params.sessionUrl) chunk.sources = buildSessionSources(params.sessionUrl);
743
+ return chunk;
744
+ });
620
745
  return [{
621
746
  type: "plan_update",
622
- title: resolvePlanTitle({
623
- label: params.label,
624
- title: params.title,
625
- tasks
626
- })
627
- }, ...tasks.map((task) => ({
628
- type: "task_update",
629
- id: task.id,
630
- title: task.title,
631
- status: task.status === "in_progress" ? params.finalInProgressStatus ?? (params.completeInProgress ? "complete" : task.status) : task.status
632
- }))];
633
- }
634
- function buildSlackProgressDraftBlocks(params) {
635
- const label = params.label?.trim() || params.title?.trim();
747
+ title
748
+ }, ...taskChunks];
749
+ }
750
+ function formatDiffStat(diffStat) {
751
+ if (!diffStat || diffStat.files === 0 && diffStat.added === 0 && diffStat.removed === 0) return;
752
+ return [
753
+ `📝 ${diffStat.files} files`,
754
+ ...diffStat.added > 0 ? [`+${diffStat.added}`] : [],
755
+ ...diffStat.removed > 0 ? [`−${diffStat.removed}`] : []
756
+ ].join(" ");
757
+ }
758
+ function formatTaskDiffOutput(diffStat) {
759
+ return diffStat && (diffStat.added > 0 || diffStat.removed > 0) ? `+${diffStat.added} −${diffStat.removed}` : void 0;
760
+ }
761
+ function buildActivityText(lines, maxLineChars) {
762
+ const rendered = [];
763
+ let length = 0;
764
+ for (const line of lines.slice(-50).toReversed()) {
765
+ const row = `${legacyLineTitle(line)} — ${legacyLineDetail(line, maxLineChars)}`;
766
+ const nextLength = length + row.length + (rendered.length > 0 ? 1 : 0);
767
+ if (nextLength > SLACK_PROGRESS_FIELD_MAX) break;
768
+ rendered.push(row);
769
+ length = nextLength;
770
+ }
771
+ return rendered.toReversed().join("\n");
772
+ }
773
+ function buildSlackProgressCardBlocks(params) {
636
774
  const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS);
637
775
  const planLines = formatPlanChecklistLines(params.plan ?? [], {
638
776
  maxLines: 50,
639
777
  maxLineChars
640
778
  });
641
779
  const narration = params.narration?.replace(/\s+/g, " ").trim();
642
- const headBlocks = [
643
- ...label ? [{
780
+ const activityText = buildActivityText(params.lines, maxLineChars);
781
+ const diffStat = formatDiffStat(params.diffStat);
782
+ const workingFooter = [
783
+ ...params.toolCalls && params.toolCalls > 0 ? [`🛠️ ${params.toolCalls} tools`] : [],
784
+ ...diffStat ? [diffStat] : [],
785
+ ...params.elapsedSeconds && params.elapsedSeconds > 0 ? [`⏱ ${params.elapsedSeconds}s`] : []
786
+ ].join(" · ");
787
+ const terminalFooter = [params.receiptSummary?.trim(), diffStat].filter(Boolean).join(" · ");
788
+ const footer = params.state === "working" ? workingFooter : terminalFooter;
789
+ return [
790
+ {
644
791
  type: "section",
645
- text: field(`*${escapeSlackMrkdwn(label)}*`)
646
- }] : [],
792
+ text: field(`${params.state === "working" ? "🔄" : params.state === "success" ? "✅" : "❌"} *${escapeSlackMrkdwn(params.title.trim() || "Working")}*`)
793
+ },
647
794
  ...narration ? [{
648
795
  type: "section",
649
796
  text: field(`_${escapeSlackMrkdwn(narration)}_`)
@@ -651,14 +798,28 @@ function buildSlackProgressDraftBlocks(params) {
651
798
  ...planLines.length > 0 ? [{
652
799
  type: "section",
653
800
  text: field(planLines.map((line) => escapeSlackMrkdwn(line)).join("\n"))
801
+ }] : [],
802
+ ...activityText ? [{
803
+ type: "section",
804
+ text: field(activityText)
805
+ }] : [],
806
+ ...footer ? [{
807
+ type: "context",
808
+ elements: [field(footer)]
809
+ }] : [],
810
+ ...params.state !== "working" && params.sessionUrl ? [{
811
+ type: "actions",
812
+ elements: [{
813
+ type: "button",
814
+ action_id: SLACK_SESSION_LINK_ACTION_ID,
815
+ text: {
816
+ type: "plain_text",
817
+ text: "Open in OpenClaw"
818
+ },
819
+ url: params.sessionUrl
820
+ }]
654
821
  }] : []
655
822
  ].slice(0, 50);
656
- const lineBudget = Math.max(0, 50 - headBlocks.length);
657
- const renderedBlocks = [...headBlocks, ...params.lines.slice(-lineBudget).map((line) => ({
658
- type: "section",
659
- fields: [field(legacyLineTitle(line)), field(legacyLineDetail(line, maxLineChars))]
660
- }))];
661
- return renderedBlocks.length ? renderedBlocks : void 0;
662
823
  }
663
824
  /**
664
825
  * Slack native streams key task rows by persistent id with no removal chunk.
@@ -691,12 +852,6 @@ function reconcileSlackNativeTaskChunks(params) {
691
852
  tasks: nextTasks
692
853
  };
693
854
  }
694
- function buildSlackProgressStreamStartChunks(params) {
695
- return buildSlackProgressStreamChunks(params);
696
- }
697
- function buildSlackProgressStreamUpdateChunks(params) {
698
- return buildSlackProgressStreamChunks(params);
699
- }
700
855
  function buildSlackProgressStreamCompletionChunks(params) {
701
856
  return buildSlackProgressStreamChunks({
702
857
  ...params,
@@ -706,11 +861,9 @@ function buildSlackProgressStreamCompletionChunks(params) {
706
861
  //#endregion
707
862
  //#region extensions/slack/src/stream-mode.ts
708
863
  function resolveSlackStreamingConfig(params) {
709
- const mode = resolveSlackStreamingMode(params);
710
864
  return {
711
- mode,
712
- nativeStreaming: resolveSlackNativeStreaming(params),
713
- draftMode: mapStreamingModeToSlackLegacyDraftStreamMode(mode)
865
+ mode: resolveSlackStreamingMode(params),
866
+ nativeStreaming: resolveSlackNativeStreaming(params)
714
867
  };
715
868
  }
716
869
  function applyAppendOnlyStreamUpdate(params) {
@@ -730,11 +883,19 @@ function applyAppendOnlyStreamUpdate(params) {
730
883
  source: params.source,
731
884
  changed: false
732
885
  };
733
- if (incoming.startsWith(params.source) || incoming.startsWith(params.rendered)) return {
886
+ if (incoming.startsWith(params.rendered)) return {
734
887
  rendered: incoming,
735
888
  source: incoming,
736
889
  changed: incoming !== params.rendered
737
890
  };
891
+ if (incoming.startsWith(params.source)) {
892
+ const delta = incoming.slice(params.source.length);
893
+ return {
894
+ rendered: `${params.rendered}${delta}`,
895
+ source: incoming,
896
+ changed: delta.length > 0
897
+ };
898
+ }
738
899
  if (params.source.startsWith(incoming)) return {
739
900
  rendered: params.rendered,
740
901
  source: params.source,
@@ -748,6 +909,50 @@ function applyAppendOnlyStreamUpdate(params) {
748
909
  };
749
910
  }
750
911
  //#endregion
912
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-render.ts
913
+ function resolveStructuredProgressLines(lines) {
914
+ return lines.map((line) => {
915
+ if (typeof line !== "string") return line;
916
+ const reasoning = line.startsWith("🧠 ");
917
+ const text = line.replace(/^(?:🧠|💬)\s+/u, "").replace(/^_(.*)_$/su, "$1").trim();
918
+ return {
919
+ ...reasoning ? { id: "reasoning" } : {},
920
+ kind: "item",
921
+ text,
922
+ label: reasoning ? "Reasoning" : "Update",
923
+ prefix: false
924
+ };
925
+ });
926
+ }
927
+ function resolveNativeProgressPlan(snapshot) {
928
+ return snapshot.plan?.length ? snapshot.plan : void 0;
929
+ }
930
+ function resolveNativeProgressLines(snapshot) {
931
+ const lines = resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id !== "reasoning" && line.id?.startsWith("commentary:") !== true);
932
+ if (snapshot.plan?.length || !snapshot.planExplanation) return lines;
933
+ const explanationLine = buildChannelProgressDraftLine({
934
+ event: "plan",
935
+ phase: "update",
936
+ explanation: snapshot.planExplanation
937
+ });
938
+ return explanationLine ? [...lines, explanationLine] : lines;
939
+ }
940
+ function resolveNativeProgressNarration(snapshot) {
941
+ 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);
942
+ return paragraphs.length > 0 ? paragraphs.join("\n\n") : void 0;
943
+ }
944
+ function combineProgressHeadlineAndExplanation(headline, explanation) {
945
+ return headline && explanation && headline !== explanation ? `${headline} — ${explanation}` : headline ?? explanation;
946
+ }
947
+ function buildNativeProgressChunks(params) {
948
+ return buildSlackProgressStreamChunks({
949
+ title: params.title,
950
+ lines: resolveNativeProgressLines(params.snapshot),
951
+ plan: resolveNativeProgressPlan(params.snapshot),
952
+ maxLineChars: params.maxLineChars
953
+ });
954
+ }
955
+ //#endregion
751
956
  //#region extensions/slack/src/monitor/message-handler/preview-finalize.ts
752
957
  function buildExpectedSlackEditText(params) {
753
958
  const trimmedText = params.text.trim();
@@ -795,8 +1000,9 @@ async function readSlackMessageAfterEditError(params) {
795
1000
  channel: params.channelId,
796
1001
  ts: params.threadTs,
797
1002
  latest: params.messageId,
1003
+ oldest: params.messageId,
798
1004
  inclusive: true,
799
- limit: 100
1005
+ limit: 1
800
1006
  })).messages ?? []).find((message) => message?.ts === params.messageId) ?? null;
801
1007
  const message = (await params.client.conversations.history({
802
1008
  token: params.token,
@@ -826,7 +1032,7 @@ async function didSlackPreviewEditApplyAfterError(params) {
826
1032
  }
827
1033
  async function finalizeSlackPreviewEdit(params) {
828
1034
  try {
829
- await editSlackMessage(params.channelId, params.messageId, params.text, {
1035
+ await editSlackRenderedMessage(params.channelId, params.messageId, params.text, {
830
1036
  token: params.token,
831
1037
  accountId: params.accountId,
832
1038
  client: params.client,
@@ -853,74 +1059,199 @@ async function finalizeSlackPreviewEdit(params) {
853
1059
  }
854
1060
  }
855
1061
  //#endregion
856
- //#region extensions/slack/src/monitor/message-handler/dispatch-progress-io.ts
857
- async function collapseSlackProgressReceipt(params) {
858
- const { account, ctx, slackClient } = params.setup;
859
- try {
860
- await finalizeSlackPreviewEdit({
861
- client: slackClient,
862
- token: ctx.botToken,
863
- accountId: account.accountId,
864
- ...params.edit
1062
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-card.ts
1063
+ function createSlackDraftProgressCardRuntime(params) {
1064
+ const { account, cfg, ctx, prepared, slackClient } = params.setup;
1065
+ let latestFallbackText = "";
1066
+ let finalStatus;
1067
+ const resolveSessionUrl = () => {
1068
+ if (cfg.gateway?.controlUi?.enabled === false) return;
1069
+ const publicOrigin = resolveGatewayPublicOrigin(cfg);
1070
+ if (!publicOrigin) return;
1071
+ const url = new URL(publicOrigin);
1072
+ const path = buildControlUiSessionPath({
1073
+ namespace: "chat",
1074
+ sessionKey: prepared.route.sessionKey,
1075
+ fallbackAgentId: prepared.route.agentId,
1076
+ basePath: cfg.gateway?.controlUi?.basePath
865
1077
  });
866
- return true;
867
- } catch (err) {
868
- logVerbose(`slack: progress receipt edit failed (${formatSlackError(err)})`);
869
- return false;
870
- }
871
- }
872
- //#endregion
873
- //#region extensions/slack/src/monitor/message-handler/dispatch-progress-render.ts
874
- function resolveStructuredProgressLines(lines) {
875
- return lines.map((line) => {
876
- if (typeof line !== "string") return line;
877
- const reasoning = line.startsWith("🧠 ");
878
- const text = line.replace(/^(?:🧠|💬)\s+/u, "").replace(/^_(.*)_$/su, "$1").trim();
879
- return {
880
- ...reasoning ? { id: "reasoning" } : {},
881
- kind: "item",
882
- text,
883
- label: reasoning ? "Reasoning" : "Update",
884
- prefix: false
885
- };
886
- });
887
- }
888
- function resolveNativeProgressPlan(snapshot) {
889
- return snapshot.plan?.length ? snapshot.plan : void 0;
890
- }
891
- function resolveNativeProgressLines(snapshot) {
892
- const lines = resolveStructuredProgressLines(snapshot.lines);
893
- if (snapshot.plan?.length || !snapshot.planExplanation) return lines;
894
- const explanationLine = buildChannelProgressDraftLine({
895
- event: "plan",
896
- phase: "update",
897
- explanation: snapshot.planExplanation
1078
+ if (!path) return;
1079
+ url.pathname = path;
1080
+ return url.toString();
1081
+ };
1082
+ const resolveText = (snapshot) => latestFallbackText || formatChannelProgressDraftText({
1083
+ entry: account.config,
1084
+ lines: [...snapshot.lines],
1085
+ seed: params.progressSeed,
1086
+ formatLine: formatSlackProgressDraftLine,
1087
+ narration: snapshot.statusHeadline,
1088
+ plan: snapshot.plan
898
1089
  });
899
- return explanationLine ? [...lines, explanationLine] : lines;
1090
+ const resolvePresentation = (snapshot, state) => {
1091
+ const title = params.explicitTitle ?? snapshot.statusHeadline ?? "Working";
1092
+ return buildSlackProgressCardBlocks({
1093
+ state,
1094
+ title,
1095
+ narration: params.explicitTitle ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) : snapshot.planExplanation && snapshot.planExplanation !== title ? snapshot.planExplanation : void 0,
1096
+ plan: snapshot.plan,
1097
+ lines: resolveStructuredProgressLines(snapshot.lines),
1098
+ maxLineChars: params.maxLineChars,
1099
+ diffStat: snapshot.diffStat,
1100
+ ...state === "working" ? {
1101
+ toolCalls: params.progressReceipt.toolCalls,
1102
+ elapsedSeconds: params.progressReceipt.elapsedSeconds
1103
+ } : {
1104
+ receiptSummary: params.progressReceipt.buildSummaryLine(),
1105
+ sessionUrl: resolveSessionUrl()
1106
+ }
1107
+ });
1108
+ };
1109
+ const finalize = async (status, snapshot = params.getSnapshot(), fallbackText = resolveText(snapshot)) => {
1110
+ if (!params.draftStream || !params.enabled) return false;
1111
+ await params.draftStream.dropDetachedMessages();
1112
+ const terminalStatus = finalStatus === "error" || status === "error" ? "error" : "success";
1113
+ if (finalStatus === terminalStatus) return true;
1114
+ await params.draftStream.flush();
1115
+ const channelId = params.draftStream.channelId();
1116
+ const messageId = params.draftStream.messageId();
1117
+ if (!channelId || !messageId) return false;
1118
+ await params.draftStream.seal();
1119
+ try {
1120
+ const finalized = await params.draftStream.finalizeMessage(messageId, async () => {
1121
+ await finalizeSlackPreviewEdit({
1122
+ client: slackClient,
1123
+ token: ctx.botToken,
1124
+ accountId: account.accountId,
1125
+ channelId,
1126
+ messageId,
1127
+ text: fallbackText,
1128
+ blocks: resolvePresentation(snapshot, terminalStatus),
1129
+ threadTs: params.getThreadTs()
1130
+ });
1131
+ });
1132
+ if (finalized) finalStatus = terminalStatus;
1133
+ return finalized;
1134
+ } catch (err) {
1135
+ logVerbose(`slack: progress card final edit failed (${formatSlackError(err)})`);
1136
+ return false;
1137
+ }
1138
+ };
1139
+ return {
1140
+ resolveSessionUrl,
1141
+ resolveText,
1142
+ resolvePresentation,
1143
+ finalize,
1144
+ get hasTerminalized() {
1145
+ return finalStatus !== void 0;
1146
+ },
1147
+ setFallbackText(text) {
1148
+ latestFallbackText = text;
1149
+ },
1150
+ reset() {
1151
+ latestFallbackText = "";
1152
+ finalStatus = void 0;
1153
+ }
1154
+ };
900
1155
  }
901
- function combineProgressHeadlineAndExplanation(headline, explanation) {
902
- return headline && explanation && headline !== explanation ? `${headline} — ${explanation}` : headline ?? explanation;
1156
+ function formatSlackProgressDraftLine(line) {
1157
+ if (/^(?:🧠|💬)\s/u.test(line)) return line;
1158
+ const italicCommentary = /^_(.*)_$/su.exec(line);
1159
+ if (!italicCommentary) return escapeSlackMrkdwn(line);
1160
+ return `_${italicCommentary[1].split(/(`[^`\n]+`)/u).map((segment, index) => {
1161
+ if (index % 2 === 0) return escapeSlackMrkdwn(segment);
1162
+ return `\`${segment.slice(1, -1).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}\``;
1163
+ }).join("")}_`;
903
1164
  }
904
- function buildNativeProgressChunks(params) {
905
- const input = {
906
- title: params.title,
907
- lines: resolveNativeProgressLines(params.snapshot),
908
- plan: resolveNativeProgressPlan(params.snapshot),
909
- maxLineChars: params.maxLineChars
1165
+ //#endregion
1166
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-native.ts
1167
+ function createSlackNativeProgressTransport(params) {
1168
+ const { ctx, message, replyPlan, slackClient, slackIdentity, slackStreamFallbackTeamId } = params.setup;
1169
+ const { delivery } = params;
1170
+ const markDelivered = (threadTs) => {
1171
+ if (!delivery.streamSession?.delivered) return false;
1172
+ delivery.observedReplyDelivery = true;
1173
+ if (threadTs) {
1174
+ delivery.usedReplyThreadTs ??= threadTs;
1175
+ delivery.rememberDeliveredThreadTs("block", threadTs);
1176
+ }
1177
+ return true;
1178
+ };
1179
+ const waitForStart = async () => {
1180
+ if (delivery.streamSession || !delivery.nativeProgressStreamStartPromise) return true;
1181
+ try {
1182
+ await delivery.nativeProgressStreamStartPromise;
1183
+ } catch {
1184
+ delivery.streamFailed = true;
1185
+ return false;
1186
+ }
1187
+ return !delivery.streamFailed;
1188
+ };
1189
+ const start = async (update) => {
1190
+ const streamThreadTs = replyPlan.nextThreadTs();
1191
+ if (!streamThreadTs) {
1192
+ logVerbose("slack-stream: no reply thread target for native progress stream start, falling back");
1193
+ delivery.streamFailed = true;
1194
+ return false;
1195
+ }
1196
+ delivery.nativeProgressStreamThreadTs = streamThreadTs;
1197
+ const startPromise = (async () => {
1198
+ const session = await startSlackStream({
1199
+ client: slackClient,
1200
+ channel: message.channel,
1201
+ threadTs: streamThreadTs,
1202
+ ...update.text ? { text: update.text } : {},
1203
+ ...update.chunks?.length ? { chunks: update.chunks } : {},
1204
+ taskDisplayMode: "plan",
1205
+ ...slackIdentity ? { identity: slackIdentity } : {},
1206
+ teamId: await resolveSlackStreamRecipientTeamId({
1207
+ client: slackClient,
1208
+ token: ctx.botToken,
1209
+ userId: message.user,
1210
+ fallbackTeamId: slackStreamFallbackTeamId
1211
+ }),
1212
+ userId: message.user
1213
+ });
1214
+ delivery.streamSession = session;
1215
+ return session;
1216
+ })();
1217
+ delivery.nativeProgressStreamStartPromise = startPromise;
1218
+ try {
1219
+ if (!await startPromise) return false;
1220
+ const delivered = markDelivered(streamThreadTs);
1221
+ return update.chunks?.length ? delivered : true;
1222
+ } finally {
1223
+ if (delivery.nativeProgressStreamStartPromise === startPromise) delivery.nativeProgressStreamStartPromise = null;
1224
+ }
1225
+ };
1226
+ const append = async (update) => {
1227
+ const session = delivery.streamSession;
1228
+ if (!session) return false;
1229
+ await appendSlackStream({
1230
+ session,
1231
+ ...update.text ? { text: update.text } : {},
1232
+ ...update.chunks?.length ? { chunks: update.chunks } : {}
1233
+ });
1234
+ const delivered = markDelivered(delivery.nativeProgressStreamThreadTs);
1235
+ return update.chunks?.length ? delivered : true;
1236
+ };
1237
+ return {
1238
+ append,
1239
+ start,
1240
+ waitForStart
910
1241
  };
911
- return params.streamStarted ? buildSlackProgressStreamUpdateChunks(input) : buildSlackProgressStreamStartChunks(input);
912
1242
  }
913
1243
  //#endregion
914
1244
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress.ts
915
1245
  function createSlackProgressRuntime(runtimeParams) {
916
1246
  const { setup, delivery, resetPreviewDeliveryState } = runtimeParams;
917
- const { account, cfg, ctx, hasSlackCustomIdentity, message, prepared, replyPlan, runtime, slackClient, slackIdentity, slackMessageMetadata, slackStreaming, slackStreamFallbackTeamId, shouldUseDraftStream, useStreaming, previewStreamingEnabled } = setup;
1247
+ const { account, cfg, ctx, hasSlackCustomIdentity, message, prepared, replyPlan, runtime, slackClient, slackIdentity, slackMessageMetadata, slackStreaming, shouldUseDraftStream, useStreaming, previewStreamingEnabled } = setup;
918
1248
  const draftStream = shouldUseDraftStream ? createSlackDraftStream({
919
1249
  target: prepared.replyTarget,
920
1250
  cfg,
921
1251
  token: ctx.botToken,
922
1252
  accountId: account.accountId,
923
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {},
1253
+ conversationChannelId: message.channel,
1254
+ eventScope: prepared.eventScope,
924
1255
  ...!hasSlackCustomIdentity && slackIdentity ? { identity: slackIdentity } : {},
925
1256
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
926
1257
  maxChars: Math.min(ctx.textLimit, SLACK_TEXT_LIMIT),
@@ -933,40 +1264,61 @@ function createSlackProgressRuntime(runtimeParams) {
933
1264
  warn: logVerbose
934
1265
  }) : void 0;
935
1266
  let hasStreamedMessage = false;
936
- const streamMode = slackStreaming.draftMode;
1267
+ const isProgressMode = slackStreaming.mode === "progress";
937
1268
  const useNativeProgressStreaming = useStreaming && slackStreaming.mode === "progress";
938
1269
  const progressDraftActive = Boolean(draftStream) || useNativeProgressStreaming;
939
1270
  const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, true, slackStreaming.mode);
940
1271
  let shouldYieldDraftProgress = () => false;
941
1272
  const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, {
942
1273
  draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming,
1274
+ mode: slackStreaming.mode,
943
1275
  previewToolProgressEnabled,
944
1276
  previewStreamingEnabled
945
1277
  });
946
1278
  let previewToolProgressSuppressed = false;
947
- let legacyPreviewToolProgressLines = [];
948
1279
  let nativeTaskState = /* @__PURE__ */ new Map();
949
1280
  let appendRenderedText = "";
950
1281
  let appendSourceText = "";
951
1282
  let nativeProgressCompletionSent = false;
952
1283
  let nativeProgressTerminalStatus = "complete";
953
1284
  let nativeProgressChunkKey;
1285
+ let nativeNarrationRenderedText = "";
1286
+ let nativeNarrationSourceText = "";
1287
+ let nativeStreamOrder = Promise.resolve();
1288
+ const withNativeStreamOrder = (task) => {
1289
+ const run = nativeStreamOrder.then(task, task);
1290
+ nativeStreamOrder = run.catch(() => void 0);
1291
+ return run;
1292
+ };
954
1293
  const progressReceipt = createChannelProgressReceiptTracker();
955
- let progressReceiptCollapsed = false;
956
- let pendingNativeProgressReceipt;
957
1294
  const progressSeed = `${account.accountId}:${message.channel}`;
958
- const useRichProgressDraft = streamMode === "status_final" && resolveChannelProgressDraftRender(account.config) === "rich";
1295
+ const useDraftProgressCard = Boolean(draftStream) && isProgressMode;
959
1296
  const explicitProgressTitle = resolveExplicitSlackProgressTitle(account.config);
960
1297
  const progressDraftMaxLineChars = resolveChannelProgressDraftMaxLineChars(account.config);
961
- const waitForNativeProgressStreamStart = async () => {
962
- if (delivery.streamSession || !delivery.nativeProgressStreamStartPromise) return true;
963
- try {
964
- await delivery.nativeProgressStreamStartPromise;
965
- } catch {
966
- delivery.streamFailed = true;
967
- return false;
968
- }
969
- return !delivery.streamFailed;
1298
+ const progressCard = createSlackDraftProgressCardRuntime({
1299
+ setup: {
1300
+ account,
1301
+ cfg,
1302
+ ctx,
1303
+ prepared,
1304
+ slackClient
1305
+ },
1306
+ draftStream,
1307
+ enabled: useDraftProgressCard,
1308
+ progressReceipt,
1309
+ progressSeed,
1310
+ explicitTitle: explicitProgressTitle,
1311
+ maxLineChars: progressDraftMaxLineChars,
1312
+ getSnapshot: () => progressDraft.getSnapshot(),
1313
+ getThreadTs: () => delivery.usedReplyThreadTs
1314
+ });
1315
+ const nativeTransport = createSlackNativeProgressTransport({
1316
+ setup,
1317
+ delivery
1318
+ });
1319
+ const dropDetachedProgressCards = async () => {
1320
+ if (!useDraftProgressCard) return;
1321
+ await draftStream?.dropDetachedMessages();
970
1322
  };
971
1323
  const appendNativeProgressCompletion = async (isError) => {
972
1324
  const session = delivery.streamSession;
@@ -986,150 +1338,156 @@ function createSlackProgressRuntime(runtimeParams) {
986
1338
  runtime.error?.(danger(`slack-stream: native progress completion failed: ${formatSlackError(err)}`));
987
1339
  }
988
1340
  };
1341
+ const resolveNativeProgressTitle = (snapshot) => combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation);
989
1342
  const buildNativeProgressChunks$1 = (snapshot) => buildNativeProgressChunks({
990
1343
  snapshot,
991
- streamStarted: Boolean(delivery.streamSession),
992
- title: combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation),
1344
+ title: resolveNativeProgressTitle(snapshot),
993
1345
  maxLineChars: progressDraftMaxLineChars
994
1346
  });
995
- const resolveNativeProgressTitle = (snapshot) => combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation);
996
- const markNativeProgressDelivered = (session, threadTs) => {
997
- if (session.delivered) delivery.observedReplyDelivery = true;
998
- if (threadTs) {
999
- delivery.usedReplyThreadTs ??= threadTs;
1000
- delivery.rememberDeliveredThreadTs("block", threadTs);
1001
- }
1002
- };
1003
- const startNativeProgressStream = async (chunks, chunkKey) => {
1004
- const streamThreadTs = replyPlan.nextThreadTs();
1005
- if (!streamThreadTs) {
1006
- logVerbose("slack-stream: no reply thread target for native progress stream start, falling back");
1007
- delivery.streamFailed = true;
1008
- return;
1009
- }
1010
- delivery.nativeProgressStreamThreadTs = streamThreadTs;
1011
- const startPromise = (async () => {
1012
- const session = await startSlackStream({
1013
- client: slackClient,
1014
- channel: message.channel,
1015
- threadTs: streamThreadTs,
1016
- chunks,
1017
- taskDisplayMode: "plan",
1018
- ...slackIdentity ? { identity: slackIdentity } : {},
1019
- teamId: await resolveSlackStreamRecipientTeamId({
1020
- client: slackClient,
1021
- token: ctx.botToken,
1022
- userId: message.user,
1023
- fallbackTeamId: slackStreamFallbackTeamId
1024
- }),
1025
- userId: message.user
1026
- });
1027
- delivery.streamSession = session;
1028
- return session;
1029
- })();
1030
- delivery.nativeProgressStreamStartPromise = startPromise;
1031
- let startedSession;
1032
- try {
1033
- startedSession = await startPromise;
1034
- } finally {
1035
- if (delivery.nativeProgressStreamStartPromise === startPromise) delivery.nativeProgressStreamStartPromise = null;
1036
- }
1037
- if (startedSession) markNativeProgressDelivered(startedSession, streamThreadTs);
1038
- nativeProgressChunkKey = chunkKey;
1039
- replyPlan.markSent();
1347
+ const normalizeProgressText = (text) => text?.replace(/\s+/gu, " ").trim() ?? "";
1348
+ const isRenderedAsProgressTitle = (text) => {
1349
+ const candidate = normalizeProgressText(text);
1350
+ if (!candidate) return false;
1351
+ const title = normalizeProgressText(resolveNativeProgressTitle(progressDraft.getSnapshot()));
1352
+ return title.length > 0 && title.includes(candidate);
1040
1353
  };
1041
- const appendNativeProgressStream = async (chunks, chunkKey) => {
1042
- if (!delivery.streamSession) return;
1043
- await appendSlackStream({
1044
- session: delivery.streamSession,
1045
- chunks
1354
+ const resolveNarrationUpdate = (incoming) => {
1355
+ const next = applyAppendOnlyStreamUpdate({
1356
+ incoming: incoming ?? "",
1357
+ rendered: nativeNarrationRenderedText,
1358
+ source: nativeNarrationSourceText
1046
1359
  });
1047
- markNativeProgressDelivered(delivery.streamSession);
1048
- nativeProgressChunkKey = chunkKey;
1360
+ return {
1361
+ next,
1362
+ delta: next.changed ? next.rendered.slice(nativeNarrationRenderedText.length) : ""
1363
+ };
1049
1364
  };
1050
- const updateNativeProgressStream = async () => {
1365
+ const updateNativeProgressStream = () => withNativeStreamOrder(updateNativeProgressStreamNow);
1366
+ const updateNativeProgressStreamNow = async () => {
1051
1367
  const snapshot = progressDraft.getSnapshot();
1052
1368
  const progressLines = resolveNativeProgressLines(snapshot);
1369
+ const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(snapshot));
1053
1370
  const hasRetirableNativeTasks = [...nativeTaskState.values()].some((task) => task.status !== "complete" && task.status !== "error");
1054
- if (!useNativeProgressStreaming || delivery.streamFailed || progressLines.length === 0 && !snapshot.plan?.length && !snapshot.statusHeadline && !explicitProgressTitle && !hasRetirableNativeTasks) return;
1055
- if (!await waitForNativeProgressStreamStart()) return;
1371
+ if (!useNativeProgressStreaming || delivery.streamFailed || progressLines.length === 0 && !snapshot.plan?.length && !snapshot.statusHeadline && !explicitProgressTitle && !hasRetirableNativeTasks && !narrationUpdate.delta) return false;
1372
+ if (!await nativeTransport.waitForStart()) return false;
1056
1373
  const reconciled = reconcileSlackNativeTaskChunks({
1057
1374
  previousTasks: nativeTaskState,
1058
1375
  chunks: buildNativeProgressChunks$1(snapshot)
1059
1376
  });
1060
- const chunks = reconciled.chunks;
1061
- if (!chunks?.length) return;
1062
- const chunkKey = JSON.stringify(chunks);
1063
- if (chunkKey === nativeProgressChunkKey) return;
1377
+ const chunkKey = JSON.stringify(reconciled.chunks ?? []);
1378
+ const taskChunksChanged = chunkKey !== nativeProgressChunkKey;
1379
+ const chunks = taskChunksChanged ? reconciled.chunks : void 0;
1380
+ if (!chunks?.length && !narrationUpdate.delta) return false;
1064
1381
  try {
1065
- if (!delivery.streamSession) await startNativeProgressStream(chunks, chunkKey);
1066
- else await appendNativeProgressStream(chunks, chunkKey);
1067
- nativeTaskState = reconciled.tasks;
1382
+ const hadSession = Boolean(delivery.streamSession);
1383
+ const streamUpdate = {
1384
+ ...narrationUpdate.delta ? { text: narrationUpdate.delta } : {},
1385
+ ...chunks?.length ? { chunks } : {}
1386
+ };
1387
+ if (!(hadSession ? await nativeTransport.append(streamUpdate) : await nativeTransport.start(streamUpdate))) return false;
1388
+ if (!hadSession) replyPlan.markSent();
1389
+ if (narrationUpdate.next.changed) {
1390
+ nativeNarrationRenderedText = narrationUpdate.next.rendered;
1391
+ nativeNarrationSourceText = narrationUpdate.next.source;
1392
+ }
1393
+ if (taskChunksChanged) {
1394
+ nativeProgressChunkKey = chunkKey;
1395
+ nativeTaskState = reconciled.tasks;
1396
+ }
1397
+ return true;
1068
1398
  } catch (err) {
1069
1399
  runtime.error?.(danger(`slack-stream: native progress stream failed: ${formatSlackError(err)}, falling back`));
1070
1400
  delivery.streamFailed = true;
1401
+ return false;
1402
+ }
1403
+ };
1404
+ const appendNativeNarration = (payload, kind) => withNativeStreamOrder(() => appendNativeNarrationNow(payload, kind));
1405
+ const appendNativeNarrationNow = async (payload, kind) => {
1406
+ if (isRenderedAsProgressTitle(payload.text)) return false;
1407
+ const narrationUpdate = resolveNarrationUpdate(payload.text?.trimEnd());
1408
+ if (!narrationUpdate.delta) return false;
1409
+ await delivery.deliverWithStreaming({
1410
+ payload,
1411
+ kind,
1412
+ streamText: narrationUpdate.delta,
1413
+ appendSeparator: false,
1414
+ taskDisplayMode: "plan"
1415
+ });
1416
+ if (!delivery.streamFailed) {
1417
+ nativeNarrationRenderedText = narrationUpdate.next.rendered;
1418
+ nativeNarrationSourceText = narrationUpdate.next.source;
1071
1419
  }
1420
+ return true;
1072
1421
  };
1073
1422
  const resetProgressTurnState = () => {
1074
1423
  progressReceipt.reset();
1075
- progressReceiptCollapsed = false;
1076
- pendingNativeProgressReceipt = void 0;
1077
- };
1078
- const collapseProgressReceipt = async (params) => {
1079
- progressReceiptCollapsed = await collapseSlackProgressReceipt({
1080
- setup,
1081
- edit: params
1082
- });
1424
+ nativeNarrationRenderedText = "";
1425
+ nativeNarrationSourceText = "";
1083
1426
  };
1084
1427
  const progressDraft = createChannelProgressDraftCompositor({
1085
1428
  entry: account.config,
1086
1429
  mode: slackStreaming.mode,
1087
- active: progressDraftActive && streamMode === "status_final",
1430
+ active: progressDraftActive,
1088
1431
  seed: progressSeed,
1089
- formatLine: escapeSlackMrkdwn,
1432
+ formatLine: formatSlackProgressDraftLine,
1090
1433
  reasoningLinePrefix: "🧠 ",
1091
- commentaryLinePrefix: "💬 ",
1434
+ commentaryLinePrefix: "",
1092
1435
  reasoningGate: previewToolProgressEnabled,
1093
- commentaryItalics: false,
1094
- updateOnLineChange: useNativeProgressStreaming || useRichProgressDraft,
1436
+ commentaryItalics: true,
1437
+ buildProgressEventLine: (input, options) => input.event === "tool" || input.event === "item" || input.event === "command-output" ? buildChannelProgressDraftLineForEntry(account.config, input, options) : buildChannelProgressDraftLine(input, options),
1438
+ updateOnLineChange: useNativeProgressStreaming || useDraftProgressCard,
1095
1439
  update: async (previewText, options) => {
1096
- if (useNativeProgressStreaming) {
1097
- await updateNativeProgressStream();
1098
- return;
1099
- }
1100
- if (!draftStream) return;
1440
+ if (useNativeProgressStreaming) return await updateNativeProgressStream();
1441
+ if (!draftStream) return false;
1101
1442
  const snapshot = progressDraft.getSnapshot();
1102
- const structuredLines = resolveStructuredProgressLines(options?.lines ?? snapshot.lines);
1103
- const richNarration = combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation);
1104
- const richProgressBlocks = useRichProgressDraft ? buildSlackProgressDraftBlocks({
1105
- title: explicitProgressTitle,
1106
- lines: structuredLines,
1107
- plan: snapshot.plan,
1108
- narration: richNarration,
1109
- maxLineChars: progressDraftMaxLineChars
1110
- }) : void 0;
1111
- draftStream.update(useRichProgressDraft && richProgressBlocks ? {
1443
+ progressCard.setFallbackText(previewText);
1444
+ draftStream.update(useDraftProgressCard ? {
1112
1445
  text: previewText,
1113
- blocks: richProgressBlocks
1446
+ blocks: progressCard.resolvePresentation(snapshot, "working")
1114
1447
  } : previewText);
1115
1448
  hasStreamedMessage = true;
1116
1449
  if (options?.flush) await draftStream.flush();
1450
+ return Boolean(draftStream.messageId() && draftStream.channelId());
1117
1451
  }
1118
1452
  });
1119
1453
  const commentaryProgressEnabled = progressDraft.commentaryProgressEnabled;
1454
+ const deliverNativeFinal = (payload, kind) => withNativeStreamOrder(() => deliverNativeFinalNow(payload, kind));
1455
+ const deliverNativeFinalNow = async (payload, kind) => {
1456
+ progressDraft.markFinalReplyStarted();
1457
+ const streamReady = await nativeTransport.waitForStart();
1458
+ const finalThreadTs = delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs;
1459
+ if (payload.isError !== true && streamReady && Boolean(delivery.streamSession) && delivery.isStreamingEligible(payload, { maxTextBytes: 4e3 })) {
1460
+ await appendNativeProgressCompletion(false);
1461
+ await delivery.deliverWithStreaming({
1462
+ payload,
1463
+ kind
1464
+ });
1465
+ } else {
1466
+ await delivery.deliverNormally({
1467
+ payload,
1468
+ kind,
1469
+ forcedThreadTs: finalThreadTs
1470
+ });
1471
+ await appendNativeProgressCompletion(payload.isError === true);
1472
+ }
1473
+ progressDraft.markFinalReplyDelivered();
1474
+ };
1120
1475
  const buildNativeProgressCompletionChunks = (finalInProgressStatus) => {
1121
1476
  const snapshot = progressDraft.getSnapshot();
1122
1477
  const lines = resolveNativeProgressLines(snapshot);
1478
+ const sessionUrl = progressCard.resolveSessionUrl();
1123
1479
  const hasRetirableNativeTasks = [...nativeTaskState.values()].some((task) => task.status !== "complete" && task.status !== "error");
1124
- if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks) return;
1480
+ if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !snapshot.diffStat && !sessionUrl) return;
1125
1481
  return reconcileSlackNativeTaskChunks({
1126
1482
  previousTasks: nativeTaskState,
1127
1483
  chunks: buildSlackProgressStreamCompletionChunks({
1128
- title: resolveNativeProgressTitle(snapshot),
1484
+ title: resolveNativeProgressTitle(snapshot) ?? (lines.length === 0 && !snapshot.plan?.length ? "Working" : void 0),
1129
1485
  lines,
1130
1486
  plan: resolveNativeProgressPlan(snapshot),
1131
1487
  maxLineChars: progressDraftMaxLineChars,
1132
- finalInProgressStatus
1488
+ finalInProgressStatus,
1489
+ diffStat: snapshot.diffStat,
1490
+ sessionUrl
1133
1491
  })
1134
1492
  }).chunks;
1135
1493
  };
@@ -1144,12 +1502,6 @@ function createSlackProgressRuntime(runtimeParams) {
1144
1502
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1145
1503
  });
1146
1504
  delivery.acknowledgeStoppedStreamedDeliveries(session, stopResult?.messageId);
1147
- if (pendingNativeProgressReceipt && stopResult?.messageId) await collapseProgressReceipt({
1148
- channelId: session.channel,
1149
- messageId: stopResult.messageId,
1150
- text: pendingNativeProgressReceipt,
1151
- threadTs: session.threadTs
1152
- });
1153
1505
  } catch (err) {
1154
1506
  const error = formatSlackError(err);
1155
1507
  delivery.emitAcknowledgedStreamedDeliveries();
@@ -1162,16 +1514,13 @@ function createSlackProgressRuntime(runtimeParams) {
1162
1514
  delivery.streamFailed = false;
1163
1515
  };
1164
1516
  const pushPlanProgress = async (steps, explanation) => {
1165
- if (streamMode === "status_final") {
1166
- await progressDraft.pushPlanProgress(steps, { explanation });
1167
- return;
1168
- }
1169
- if (previewToolProgressSuppressed || !draftStream) return;
1517
+ if (isProgressMode) return await progressDraft.pushPlanProgress(steps, { explanation });
1518
+ if (previewToolProgressSuppressed || !draftStream) return false;
1170
1519
  const text = formatChannelProgressDraftText({
1171
1520
  entry: account.config,
1172
- lines: legacyPreviewToolProgressLines,
1521
+ lines: [...progressDraft.getSnapshot().lines],
1173
1522
  seed: progressSeed,
1174
- formatLine: escapeSlackMrkdwn,
1523
+ formatLine: formatSlackProgressDraftLine,
1175
1524
  narration: explanation,
1176
1525
  plan: steps
1177
1526
  });
@@ -1179,37 +1528,25 @@ function createSlackProgressRuntime(runtimeParams) {
1179
1528
  draftStream.update(text);
1180
1529
  hasStreamedMessage = true;
1181
1530
  }
1531
+ return false;
1182
1532
  };
1183
1533
  const pushPreviewProgress = async (line, options) => {
1184
- if (!draftStream && !useNativeProgressStreaming) return;
1185
- if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
1534
+ if (!draftStream && !useNativeProgressStreaming) return false;
1535
+ if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return false;
1186
1536
  const normalized = line?.text.replace(/\s+/g, " ").trim();
1187
- if (streamMode === "status_final") {
1188
- if (!line || !normalized) {
1189
- await progressDraft.noteActivity();
1190
- return;
1191
- }
1192
- await progressDraft.pushToolProgress(line, options);
1193
- return;
1537
+ if (isProgressMode) {
1538
+ if (!line || !normalized) return await progressDraft.noteActivity();
1539
+ return await progressDraft.pushToolProgress(line, options);
1194
1540
  }
1195
- if (!line || !normalized || !draftStream || !previewToolProgressEnabled || previewToolProgressSuppressed) return;
1196
- const nextLines = mergeChannelProgressDraftLine(legacyPreviewToolProgressLines, line, { maxLines: resolveChannelProgressDraftMaxLines(account.config) });
1197
- if (nextLines === legacyPreviewToolProgressLines) return;
1198
- legacyPreviewToolProgressLines = nextLines;
1199
- draftStream.update(formatChannelProgressDraftText({
1200
- entry: account.config,
1201
- lines: legacyPreviewToolProgressLines,
1202
- seed: progressSeed,
1203
- formatLine: escapeSlackMrkdwn
1204
- }));
1205
- hasStreamedMessage = true;
1541
+ if (!line || !normalized || !draftStream || !previewToolProgressEnabled) return false;
1542
+ return await progressDraft.pushToolProgress(line, options);
1206
1543
  };
1207
1544
  const updateDraftFromPartial = (text) => {
1208
1545
  const trimmed = text?.trimEnd();
1209
- if (!trimmed) return;
1210
- if (streamMode === "append") {
1546
+ if (!trimmed) return false;
1547
+ if (slackStreaming.mode === "block") {
1211
1548
  previewToolProgressSuppressed = true;
1212
- legacyPreviewToolProgressLines = [];
1549
+ progressDraft.suppress();
1213
1550
  const next = applyAppendOnlyStreamUpdate({
1214
1551
  incoming: trimmed,
1215
1552
  rendered: appendRenderedText,
@@ -1217,32 +1554,34 @@ function createSlackProgressRuntime(runtimeParams) {
1217
1554
  });
1218
1555
  appendRenderedText = next.rendered;
1219
1556
  appendSourceText = next.source;
1220
- if (!next.changed) return;
1557
+ if (!next.changed) return false;
1221
1558
  draftStream?.update(next.rendered);
1222
1559
  hasStreamedMessage = true;
1223
- return;
1560
+ return false;
1224
1561
  }
1225
- if (streamMode === "status_final") return;
1562
+ if (isProgressMode) return false;
1226
1563
  previewToolProgressSuppressed = true;
1227
- legacyPreviewToolProgressLines = [];
1564
+ progressDraft.suppress();
1228
1565
  draftStream?.update(trimmed);
1229
1566
  hasStreamedMessage = true;
1567
+ return false;
1230
1568
  };
1231
1569
  const pushReasoningProgress = async (payload) => {
1232
- if (!payload?.text) return;
1233
- if (streamMode !== "status_final") {
1570
+ if (!payload?.text) return false;
1571
+ if (!isProgressMode) {
1234
1572
  const normalized = progressDraft.mergeReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true }).replace(/^_(.*)_$/su, "$1").trim();
1235
- if (!normalized) return;
1236
- await pushPreviewProgress({
1573
+ if (!normalized) return false;
1574
+ const visible = await pushPreviewProgress({
1237
1575
  id: "reasoning",
1238
1576
  kind: "item",
1239
1577
  text: normalized,
1240
1578
  label: "Reasoning"
1241
1579
  });
1242
- return;
1580
+ progressDraft.mergeReasoningProgress(normalized, { snapshot: true });
1581
+ return visible;
1243
1582
  }
1244
1583
  progressReceipt.noteReasoning();
1245
- await progressDraft.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
1584
+ return await progressDraft.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
1246
1585
  };
1247
1586
  const resetDraftDeliveryState = () => {
1248
1587
  hasStreamedMessage = false;
@@ -1250,28 +1589,56 @@ function createSlackProgressRuntime(runtimeParams) {
1250
1589
  appendSourceText = "";
1251
1590
  };
1252
1591
  const resetDraftProgressState = () => {
1253
- progressDraft.resetReasoningProgress();
1254
1592
  previewToolProgressSuppressed = false;
1255
- legacyPreviewToolProgressLines = [];
1593
+ progressDraft.reset();
1256
1594
  };
1257
1595
  const beginNewProgressTurn = async (options) => {
1596
+ const priorSnapshot = progressDraft.getSnapshot();
1597
+ const priorFallbackText = progressCard.resolveText(priorSnapshot);
1258
1598
  const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent ? buildNativeProgressCompletionChunks(nativeProgressTerminalStatus) : void 0;
1259
1599
  if (!progressDraft.beginNewTurn(options)) return false;
1260
1600
  if (useNativeProgressStreaming) await finishNativeProgressTurn(completionChunks);
1261
- else draftStream?.forceNewMessage();
1601
+ else {
1602
+ await progressCard.finalize("success", priorSnapshot, priorFallbackText);
1603
+ draftStream?.forceNewMessage();
1604
+ await dropDetachedProgressCards();
1605
+ }
1262
1606
  resetProgressTurnState();
1263
1607
  nativeTaskState = /* @__PURE__ */ new Map();
1264
1608
  nativeProgressCompletionSent = false;
1265
1609
  nativeProgressTerminalStatus = "complete";
1266
1610
  nativeProgressChunkKey = void 0;
1611
+ progressCard.reset();
1267
1612
  resetPreviewDeliveryState();
1268
1613
  delivery.resetDeliveryTracker();
1269
- progressReceiptCollapsed = false;
1270
1614
  return true;
1271
1615
  };
1616
+ const onDraftBoundary = !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1617
+ if (isProgressMode) {
1618
+ await beginNewProgressTurn();
1619
+ return;
1620
+ }
1621
+ if (hasStreamedMessage) draftStream?.forceNewMessage();
1622
+ resetDraftDeliveryState();
1623
+ resetDraftProgressState();
1624
+ };
1625
+ const onQueuedFollowupAdmitted = !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1626
+ await draftStream?.flush();
1627
+ resetPreviewDeliveryState();
1628
+ if (isProgressMode) await beginNewProgressTurn({ force: true });
1629
+ else draftStream?.forceNewMessage();
1630
+ delivery.resetDeliveryTracker();
1631
+ resetDraftDeliveryState();
1632
+ resetDraftProgressState();
1633
+ };
1634
+ const onQueuedFollowupSettled = !useDraftProgressCard ? void 0 : async () => {
1635
+ if (!progressCard.hasTerminalized) await draftStream?.clear();
1636
+ await dropDetachedProgressCards();
1637
+ };
1272
1638
  return {
1273
1639
  draftStream,
1274
- streamMode,
1640
+ isProgressMode,
1641
+ useDraftProgressCard,
1275
1642
  useNativeProgressStreaming,
1276
1643
  progressDraftActive,
1277
1644
  previewToolProgressEnabled,
@@ -1279,15 +1646,6 @@ function createSlackProgressRuntime(runtimeParams) {
1279
1646
  progressDraft,
1280
1647
  commentaryProgressEnabled,
1281
1648
  progressReceipt,
1282
- get progressReceiptCollapsed() {
1283
- return progressReceiptCollapsed;
1284
- },
1285
- get pendingNativeProgressReceipt() {
1286
- return pendingNativeProgressReceipt;
1287
- },
1288
- set pendingNativeProgressReceipt(value) {
1289
- pendingNativeProgressReceipt = value;
1290
- },
1291
1649
  get nativeProgressCompletionSent() {
1292
1650
  return nativeProgressCompletionSent;
1293
1651
  },
@@ -1297,33 +1655,17 @@ function createSlackProgressRuntime(runtimeParams) {
1297
1655
  get nativeProgressTerminalStatus() {
1298
1656
  return nativeProgressTerminalStatus;
1299
1657
  },
1300
- appendNativeProgressCompletion,
1301
- beginNewProgressTurn,
1658
+ appendNativeNarration,
1302
1659
  buildNativeProgressCompletionChunks,
1303
- collapseProgressReceipt,
1304
- onDraftBoundary: !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1305
- if (streamMode === "status_final") {
1306
- await beginNewProgressTurn();
1307
- return;
1308
- }
1309
- if (hasStreamedMessage) draftStream?.forceNewMessage();
1310
- resetDraftDeliveryState();
1311
- resetDraftProgressState();
1312
- },
1313
- onQueuedFollowupAdmitted: !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1314
- await draftStream?.flush();
1315
- resetPreviewDeliveryState();
1316
- if (streamMode === "status_final") await beginNewProgressTurn({ force: true });
1317
- else draftStream?.forceNewMessage();
1318
- delivery.resetDeliveryTracker();
1319
- resetDraftDeliveryState();
1320
- resetDraftProgressState();
1321
- },
1660
+ deliverNativeFinal,
1661
+ dropDetachedProgressCards,
1662
+ finalizeDraftProgressCard: progressCard.finalize,
1663
+ onDraftBoundary,
1664
+ onQueuedFollowupAdmitted,
1665
+ onQueuedFollowupSettled,
1322
1666
  pushPlanProgress,
1323
- pushPreviewProgress,
1324
1667
  pushReasoningProgress,
1325
1668
  updateDraftFromPartial,
1326
- waitForNativeProgressStreamStart,
1327
1669
  setShouldYieldDraftProgress: (value) => {
1328
1670
  shouldYieldDraftProgress = value;
1329
1671
  },
@@ -1398,7 +1740,7 @@ async function createSlackDispatchSetup(prepared) {
1398
1740
  sessionKey: inboundLastRouteSessionKey,
1399
1741
  deliveryContext: {
1400
1742
  channel: "slack",
1401
- to: `user:${message.user}`,
1743
+ to: prepared.ctxPayload.OriginatingTo ?? prepared.ctxPayload.To ?? `user:${message.user}`,
1402
1744
  accountId: route.accountId,
1403
1745
  threadId: prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId
1404
1746
  },
@@ -1434,6 +1776,7 @@ async function createSlackDispatchSetup(prepared) {
1434
1776
  const messageTs = message.ts ?? message.event_ts;
1435
1777
  const incomingThreadTs = message.thread_ts;
1436
1778
  let didSetStatus = false;
1779
+ let didAddTypingReaction = false;
1437
1780
  const statusReactionsEnabled = prepared.ctxPayload.InboundEventKind !== "room_event" && Boolean(prepared.ackReactionPromise) && Boolean(reactionMessageTs) && cfg.messages?.statusReactions?.enabled === true;
1438
1781
  const statusReactions = createStatusReactionController({
1439
1782
  enabled: statusReactionsEnabled,
@@ -1480,6 +1823,7 @@ async function createSlackDispatchSetup(prepared) {
1480
1823
  });
1481
1824
  const typingTarget = statusThreadTs ? `${message.channel}/${statusThreadTs}` : message.channel;
1482
1825
  const typingReaction = ctx.typingReaction;
1826
+ const threadStatusGate = { hasVisibleOutput: () => false };
1483
1827
  const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
1484
1828
  cfg,
1485
1829
  agentId: route.agentId,
@@ -1491,35 +1835,44 @@ async function createSlackDispatchSetup(prepared) {
1491
1835
  },
1492
1836
  typing: {
1493
1837
  start: async () => {
1494
- didSetStatus = true;
1495
- await ctx.setSlackThreadStatus({
1496
- channelId: message.channel,
1497
- threadTs: statusThreadTs,
1498
- status: "is typing...",
1499
- eventScope: prepared.eventScope
1500
- });
1501
- if (typingReaction && message.ts) await reactSlackMessage(message.channel, message.ts, typingReaction, {
1502
- token: ctx.botToken,
1503
- client: slackClient
1504
- }).catch((err) => {
1505
- logVerbose(`slack send: typing reaction failed: ${formatSlackError(err)}`);
1506
- });
1838
+ if (!threadStatusGate.hasVisibleOutput()) {
1839
+ didSetStatus = true;
1840
+ await ctx.setSlackThreadStatus({
1841
+ channelId: message.channel,
1842
+ threadTs: statusThreadTs,
1843
+ status: "is typing...",
1844
+ eventScope: prepared.eventScope
1845
+ });
1846
+ }
1847
+ if (typingReaction && message.ts) {
1848
+ didAddTypingReaction = true;
1849
+ await reactSlackMessage(message.channel, message.ts, typingReaction, {
1850
+ token: ctx.botToken,
1851
+ client: slackClient
1852
+ }).catch((err) => {
1853
+ logVerbose(`slack send: typing reaction failed: ${formatSlackError(err)}`);
1854
+ });
1855
+ }
1507
1856
  },
1508
1857
  stop: async () => {
1509
- if (!didSetStatus) return;
1510
- didSetStatus = false;
1511
- await ctx.setSlackThreadStatus({
1512
- channelId: message.channel,
1513
- threadTs: statusThreadTs,
1514
- status: "",
1515
- eventScope: prepared.eventScope
1516
- });
1517
- if (typingReaction && message.ts) await removeSlackReaction(message.channel, message.ts, typingReaction, {
1518
- token: ctx.botToken,
1519
- client: slackClient
1520
- }).catch((err) => {
1521
- logVerbose(`slack send: typing reaction removal failed: ${formatSlackError(err)}`);
1522
- });
1858
+ if (didSetStatus) {
1859
+ didSetStatus = false;
1860
+ await ctx.setSlackThreadStatus({
1861
+ channelId: message.channel,
1862
+ threadTs: statusThreadTs,
1863
+ status: "",
1864
+ eventScope: prepared.eventScope
1865
+ });
1866
+ }
1867
+ if (didAddTypingReaction && typingReaction && message.ts) {
1868
+ didAddTypingReaction = false;
1869
+ await removeSlackReaction(message.channel, message.ts, typingReaction, {
1870
+ token: ctx.botToken,
1871
+ client: slackClient
1872
+ }).catch((err) => {
1873
+ logVerbose(`slack send: typing reaction removal failed: ${formatSlackError(err)}`);
1874
+ });
1875
+ }
1523
1876
  },
1524
1877
  onStartError: (err) => {
1525
1878
  logTypingFailure({
@@ -1541,10 +1894,7 @@ async function createSlackDispatchSetup(prepared) {
1541
1894
  }
1542
1895
  }
1543
1896
  });
1544
- const slackStreaming = resolveSlackStreamingConfig({
1545
- streaming: account.config.streaming,
1546
- nativeStreaming: resolveChannelStreamingNativeTransport(account.config)
1547
- });
1897
+ const slackStreaming = resolveSlackStreamingConfig({ streaming: account.config.streaming });
1548
1898
  const streamThreadHint = forcedReplyThreadTs ?? resolveSlackStreamingThreadHint({
1549
1899
  replyToMode: replyDeliveryMode,
1550
1900
  incomingThreadTs,
@@ -1553,7 +1903,7 @@ async function createSlackDispatchSetup(prepared) {
1553
1903
  });
1554
1904
  const hookRunner = getGlobalHookRunner();
1555
1905
  const allowPreHookProviderStreaming = !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false));
1556
- const previewStreamingEnabled = allowPreHookProviderStreaming && !sourceRepliesAreToolOnly && shouldEnableSlackPreviewStreaming({ mode: slackStreaming.mode });
1906
+ const previewStreamingEnabled = allowPreHookProviderStreaming && !sourceRepliesAreToolOnly && slackStreaming.mode !== "off";
1557
1907
  const hasSlackCustomIdentity = Boolean(slackIdentity?.username || slackIdentity?.iconUrl || slackIdentity?.iconEmoji);
1558
1908
  const useStreaming = shouldUseStreaming({
1559
1909
  streamingEnabled: !sourceRepliesAreToolOnly && (allowPreHookProviderStreaming || slackStreaming.mode !== "progress") && isSlackStreamingEnabled({
@@ -1563,10 +1913,7 @@ async function createSlackDispatchSetup(prepared) {
1563
1913
  }),
1564
1914
  threadTs: streamThreadHint
1565
1915
  });
1566
- const shouldUseDraftStream = shouldInitializeSlackDraftStream({
1567
- previewStreamingEnabled,
1568
- useStreaming
1569
- });
1916
+ const shouldUseDraftStream = previewStreamingEnabled && !useStreaming;
1570
1917
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1571
1918
  const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1572
1919
  useStreaming,
@@ -1604,6 +1951,7 @@ async function createSlackDispatchSetup(prepared) {
1604
1951
  statusReactionsEnabled,
1605
1952
  statusReactions,
1606
1953
  hasRepliedRef,
1954
+ threadStatusGate,
1607
1955
  replyPlan,
1608
1956
  onModelSelected,
1609
1957
  replyPipeline,
@@ -1752,7 +2100,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1752
2100
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1753
2101
  ...messageSentDeliveryHookContext,
1754
2102
  deferMessageSentHooks: true,
1755
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {}
2103
+ eventScope: prepared.eventScope
1756
2104
  });
1757
2105
  markSlackStreamFallbackDelivered(session);
1758
2106
  if (!session.stopped) try {
@@ -1800,7 +2148,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1800
2148
  ...slackIdentity ? { identity: slackIdentity } : {},
1801
2149
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1802
2150
  ...messageSentDeliveryHookContext,
1803
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {}
2151
+ eventScope: prepared.eventScope
1804
2152
  });
1805
2153
  state.observedReplyDelivery = true;
1806
2154
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
@@ -1834,12 +2182,16 @@ function createSlackStreamingDeliveryRuntime(setup) {
1834
2182
  rememberDeliveredThreadTs(params.kind, params.session.threadTs);
1835
2183
  return true;
1836
2184
  };
2185
+ const isStreamingEligible = (payload, options) => {
2186
+ const reply = resolveSendableOutboundReplyParts(payload);
2187
+ const renderPlan = resolveSlackReplyRenderPlan(payload);
2188
+ const plannedBlocks = renderPlan.mode === "single" ? renderPlan.blocks : renderPlan.blockPart?.blocks;
2189
+ return !state.streamFailed && !reply.hasMedia && renderPlan.mode !== "split" && !plannedBlocks?.length && !readSlackReplyBlocks(payload)?.length && reply.hasText && (!options?.maxTextBytes || countSlackTextUtf8Bytes(reply.trimmedText) <= options.maxTextBytes);
2190
+ };
1837
2191
  const deliverWithStreaming = async (params) => {
1838
2192
  if (params.payload.isReasoning === true) return;
1839
2193
  const reply = resolveSendableOutboundReplyParts(params.payload);
1840
- const renderPlan = resolveSlackReplyRenderPlan(params.payload);
1841
- const plannedBlocks = renderPlan.mode === "single" ? renderPlan.blocks : renderPlan.blockPart?.blocks;
1842
- if (state.streamFailed || reply.hasMedia || renderPlan.mode === "split" || Boolean(plannedBlocks?.length) || readSlackReplyBlocks(params.payload)?.length || !reply.hasText) {
2194
+ if (!isStreamingEligible(params.payload)) {
1843
2195
  await deliverNormally({
1844
2196
  payload: params.payload,
1845
2197
  kind: params.kind,
@@ -1847,7 +2199,8 @@ function createSlackStreamingDeliveryRuntime(setup) {
1847
2199
  });
1848
2200
  return;
1849
2201
  }
1850
- const text = reply.trimmedText;
2202
+ const text = params.streamText ?? reply.trimmedText;
2203
+ const hookContent = reply.trimmedText;
1851
2204
  let plannedThreadTs;
1852
2205
  try {
1853
2206
  if (!state.streamSession && state.nativeProgressStreamStartPromise) await state.nativeProgressStreamStartPromise;
@@ -1885,6 +2238,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1885
2238
  channel: message.channel,
1886
2239
  threadTs: streamThreadTs,
1887
2240
  text,
2241
+ ...params.taskDisplayMode ? { taskDisplayMode: params.taskDisplayMode } : {},
1888
2242
  ...slackIdentity ? { identity: slackIdentity } : {},
1889
2243
  teamId: await resolveSlackStreamRecipientTeamId({
1890
2244
  client: slackClient,
@@ -1899,7 +2253,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1899
2253
  state.observedReplyDelivery = true;
1900
2254
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
1901
2255
  }
1902
- if (text) rememberStreamedDelivery(params.kind, text, state.streamSession);
2256
+ if (hookContent) rememberStreamedDelivery(params.kind, hookContent, state.streamSession);
1903
2257
  rememberDeliveredThreadTs(params.kind, streamThreadTs);
1904
2258
  replyPlan.markSent();
1905
2259
  deliveryTracker.markDelivered({
@@ -1919,13 +2273,13 @@ function createSlackStreamingDeliveryRuntime(setup) {
1919
2273
  logVerbose("slack-stream: suppressed duplicate append payload");
1920
2274
  return;
1921
2275
  }
1922
- if (text) recordStreamedDelivery(params.kind, text);
2276
+ if (hookContent) recordStreamedDelivery(params.kind, hookContent);
1923
2277
  await appendSlackStream({
1924
2278
  session: state.streamSession,
1925
- text: "\n" + text
2279
+ text: `${params.appendSeparator === false ? "" : "\n"}${text}`
1926
2280
  });
1927
2281
  refreshStreamedAcknowledgements(state.streamSession);
1928
- if (state.streamSession.delivered) {
2282
+ if (state.streamSession.delivered && state.streamSession.pendingText.length === 0) {
1929
2283
  state.observedReplyDelivery = true;
1930
2284
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
1931
2285
  }
@@ -2000,6 +2354,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
2000
2354
  emitAcknowledgedStreamedDeliveries,
2001
2355
  emitFailedPendingStreamedDeliveries,
2002
2356
  hasDelivered: (params) => deliveryTracker.hasDelivered(params),
2357
+ isStreamingEligible,
2003
2358
  markPreviewPayloadDelivered,
2004
2359
  rememberDeliveredThreadTs,
2005
2360
  reconcileCounts,
@@ -2024,47 +2379,66 @@ async function dispatchPreparedSlackMessage(prepared) {
2024
2379
  }
2025
2380
  });
2026
2381
  const draftStream = progress.draftStream;
2382
+ setup.threadStatusGate.hasVisibleOutput = () => delivery.observedReplyDelivery || draftPreviewCommitted.value || Boolean(draftStream?.messageId());
2383
+ const failureNoticeThreadTs = message.thread_ts;
2384
+ const failureNoticeTeamId = prepared.eventScope?.teamId;
2385
+ let sawTerminalFailurePayload = false;
2386
+ let pendingFailureNotice;
2387
+ const filterPassiveThreadFailure = (payload) => {
2388
+ if (payload.isError !== true || prepared.ctxPayload.ChatType !== "channel" || isReplyPayloadNonTerminalToolErrorWarning(payload)) return payload;
2389
+ sawTerminalFailurePayload = true;
2390
+ if (delivery.observedReplyDelivery || draftPreviewCommitted.value) return payload;
2391
+ const explicitlyAddressed = prepared.ctxPayload.ExplicitlyMentionedBot === true || prepared.ctxPayload.MentionSource === "explicit_bot" || prepared.ctxPayload.MentionSource === "subteam" || prepared.ctxPayload.MentionSource === "mention_pattern" || prepared.ctxPayload.MentionSource === "command_bypass" || prepared.ctxPayload.CommandTurn?.kind !== void 0 && prepared.ctxPayload.CommandTurn.kind !== "normal" && prepared.ctxPayload.CommandTurn.authorized;
2392
+ const noticeThreadTs = failureNoticeThreadTs ?? (explicitlyAddressed ? statusThreadTs : void 0);
2393
+ const notice = {
2394
+ accountId: account.accountId,
2395
+ channelId: message.channel,
2396
+ ...noticeThreadTs ? { threadTs: noticeThreadTs } : {},
2397
+ failureText: payload.text ?? "",
2398
+ ...failureNoticeTeamId ? { teamId: failureNoticeTeamId } : {}
2399
+ };
2400
+ if (failureNoticeThreadTs && !explicitlyAddressed && prepared.ctxPayload.MentionSource !== "implicit_thread" && !hasSlackThreadParticipation(notice.accountId, notice.channelId, failureNoticeThreadTs, failureNoticeTeamId)) {
2401
+ logVerbose("slack: suppressed passive failure before thread participation");
2402
+ return null;
2403
+ }
2404
+ if (!explicitlyAddressed && hasSlackThreadFailureNotice(notice)) {
2405
+ logVerbose("slack: suppressed repeated passive channel or thread failure");
2406
+ return null;
2407
+ }
2408
+ pendingFailureNotice = notice;
2409
+ return payload;
2410
+ };
2027
2411
  const deliverSlackPayload = async (payload, info) => {
2028
2412
  if (payload.isReasoning === true) return { visibleReplySent: false };
2029
- if (info.kind === "final" && slackStreaming.mode === "progress" && progress.streamMode === "status_final") {
2030
- const hadProgressDraft = progress.progressDraft.hasStarted;
2031
- progress.progressDraft.markFinalReplyStarted();
2413
+ if (info.kind === "final" && slackStreaming.mode === "progress" && progress.isProgressMode) {
2032
2414
  if (progress.useNativeProgressStreaming) {
2033
- await progress.waitForNativeProgressStreamStart();
2034
- const finalThreadTs = delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs;
2415
+ await progress.deliverNativeFinal(payload, info.kind);
2416
+ return;
2417
+ }
2418
+ progress.progressDraft.markFinalReplyStarted();
2419
+ if (progress.useDraftProgressCard) {
2035
2420
  await delivery.deliverNormally({
2036
2421
  payload,
2037
2422
  kind: info.kind,
2038
- forcedThreadTs: finalThreadTs
2423
+ forcedThreadTs: delivery.usedReplyThreadTs
2039
2424
  });
2040
- await progress.appendNativeProgressCompletion(payload.isError === true);
2425
+ if (!await progress.finalizeDraftProgressCard(payload.isError === true ? "error" : "success")) await draftStream?.clear();
2041
2426
  progress.progressDraft.markFinalReplyDelivered();
2042
- if (!payload.isError && hadProgressDraft && delivery.streamSession) progress.pendingNativeProgressReceipt = progress.progressReceipt.buildSummaryLine();
2043
2427
  return;
2044
2428
  }
2045
- if (hadProgressDraft) try {
2046
- await draftStream?.flush();
2047
- } catch (err) {
2048
- logVerbose(`slack: progress draft flush before final failed (${formatSlackError(err)})`);
2049
- }
2050
- const receiptChannelId = hadProgressDraft ? draftStream?.channelId() : void 0;
2051
- const receiptMessageId = hadProgressDraft ? draftStream?.messageId() : void 0;
2052
- const draftThreadTs = hadProgressDraft ? delivery.usedReplyThreadTs ?? statusThreadTs : void 0;
2053
- await delivery.deliverNormally({
2054
- payload,
2055
- kind: info.kind,
2056
- ...draftThreadTs ? { forcedThreadTs: draftThreadTs } : {}
2057
- });
2058
- progress.progressDraft.markFinalReplyDelivered();
2059
- if (!payload.isError && receiptChannelId && receiptMessageId && !progress.progressReceiptCollapsed) await progress.collapseProgressReceipt({
2060
- channelId: receiptChannelId,
2061
- messageId: receiptMessageId,
2062
- text: progress.progressReceipt.buildSummaryLine(),
2063
- threadTs: delivery.usedReplyThreadTs ?? statusThreadTs
2064
- });
2065
- return;
2066
2429
  }
2067
2430
  if (progress.useNativeProgressStreaming) {
2431
+ if (info.kind !== "final" && payload.isError !== true) {
2432
+ if (!delivery.isStreamingEligible(payload)) {
2433
+ await delivery.deliverNormally({
2434
+ payload,
2435
+ kind: info.kind,
2436
+ forcedThreadTs: delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs
2437
+ });
2438
+ return;
2439
+ }
2440
+ return await progress.appendNativeNarration(payload, info.kind) ? void 0 : { visibleReplySent: false };
2441
+ }
2068
2442
  await delivery.deliverNormally({
2069
2443
  payload,
2070
2444
  kind: info.kind,
@@ -2087,25 +2461,28 @@ async function dispatchPreparedSlackMessage(prepared) {
2087
2461
  const requiresSeparateFallbackDelivery = replyRenderPlan.mode === "split";
2088
2462
  const trimmedFinalText = replyRenderPlan.mode === "single" ? replyRenderPlan.text.trim() : replyRenderPlan.fallbackText.trim();
2089
2463
  const previewFinalText = replyRenderPlan.mode === "single" && replyRenderPlan.textIsSlackMrkdwn ? trimmedFinalText : normalizeSlackOutboundText((replySourceText ?? "").trim());
2464
+ const previewFinalTextFitsEdit = countSlackTextUtf8Bytes(previewFinalText) <= SLACK_EDIT_TEXT_MAX_BYTES;
2090
2465
  const shouldRestoreTtsSupplementTextForPreviewFallback = Boolean(ttsSupplement) && ttsSupplement?.visibleTextAlreadyDelivered !== true && Boolean(draftStream) && !draftPreviewCommitted.value && !delivery.observedFinalReplyDelivery && previewStreamingEnabled && !payload.text?.trim();
2091
- if (info.kind === "final" && ttsSupplement && draftStream && !hasSlackCustomIdentity && !draftPreviewCommitted.value && !delivery.observedFinalReplyDelivery && previewStreamingEnabled && !payload.isError && !requiresSeparateFallbackDelivery && trimmedFinalText.length > 0) {
2466
+ if (info.kind === "final" && ttsSupplement && draftStream && !hasSlackCustomIdentity && !draftPreviewCommitted.value && !delivery.observedFinalReplyDelivery && previewStreamingEnabled && !payload.isError && !requiresSeparateFallbackDelivery && previewFinalTextFitsEdit && trimmedFinalText.length > 0) {
2467
+ await draftStream.flush();
2092
2468
  const channelId = draftStream.channelId();
2093
2469
  const messageId = draftStream.messageId();
2094
2470
  if (channelId && messageId) {
2095
2471
  const finalThreadTs = delivery.usedReplyThreadTs ?? statusThreadTs;
2096
- await draftStream.flush();
2097
2472
  await draftStream.seal();
2098
2473
  try {
2099
- await finalizeSlackPreviewEdit({
2100
- client: slackClient,
2101
- token: ctx.botToken,
2102
- accountId: account.accountId,
2103
- channelId,
2104
- messageId,
2105
- text: previewFinalText,
2106
- ...slackBlocks?.length ? { blocks: slackBlocks } : {},
2107
- threadTs: finalThreadTs
2108
- });
2474
+ if (!await draftStream.finalizeMessage(messageId, async () => {
2475
+ await finalizeSlackPreviewEdit({
2476
+ client: slackClient,
2477
+ token: ctx.botToken,
2478
+ accountId: account.accountId,
2479
+ channelId,
2480
+ messageId,
2481
+ text: previewFinalText,
2482
+ ...slackBlocks?.length ? { blocks: slackBlocks } : {},
2483
+ threadTs: finalThreadTs
2484
+ });
2485
+ })) throw new Error("Slack preview moved below a newer conversation message");
2109
2486
  } catch (err) {
2110
2487
  logVerbose(`slack: preview final edit failed; falling back to standard send (${formatSlackError(err)})`);
2111
2488
  await draftStream.discardPending();
@@ -2139,6 +2516,7 @@ async function dispatchPreparedSlackMessage(prepared) {
2139
2516
  payload,
2140
2517
  threadTs: finalThreadTs
2141
2518
  });
2519
+ progress.progressDraft.markFinalReplyDelivered();
2142
2520
  return;
2143
2521
  }
2144
2522
  }
@@ -2161,7 +2539,7 @@ async function dispatchPreparedSlackMessage(prepared) {
2161
2539
  }
2162
2540
  } : void 0,
2163
2541
  buildFinalEdit: () => {
2164
- if (hasSlackCustomIdentity || !previewStreamingEnabled || reply.hasMedia && !ttsSupplement || payload.isError || requiresSeparateFallbackDelivery || trimmedFinalText.length === 0 && !slackBlocks?.length) return;
2542
+ if (hasSlackCustomIdentity || !previewStreamingEnabled || reply.hasMedia && !ttsSupplement || payload.isError || requiresSeparateFallbackDelivery || !previewFinalTextFitsEdit || trimmedFinalText.length === 0 && !slackBlocks?.length) return;
2165
2543
  return {
2166
2544
  text: previewFinalText,
2167
2545
  blocks: slackBlocks,
@@ -2174,16 +2552,18 @@ async function dispatchPreparedSlackMessage(prepared) {
2174
2552
  payload,
2175
2553
  threadTs: edit.threadTs
2176
2554
  })) return;
2177
- await finalizeSlackPreviewEdit({
2178
- client: slackClient,
2179
- token: ctx.botToken,
2180
- accountId: account.accountId,
2181
- channelId: preview.channelId,
2182
- messageId: preview.messageId,
2183
- text: edit.text,
2184
- ...edit.blocks?.length ? { blocks: edit.blocks } : {},
2185
- threadTs: edit.threadTs
2186
- });
2555
+ if (!await draftStream?.finalizeMessage(preview.messageId, async () => {
2556
+ await finalizeSlackPreviewEdit({
2557
+ client: slackClient,
2558
+ token: ctx.botToken,
2559
+ accountId: account.accountId,
2560
+ channelId: preview.channelId,
2561
+ messageId: preview.messageId,
2562
+ text: edit.text,
2563
+ ...edit.blocks?.length ? { blocks: edit.blocks } : {},
2564
+ threadTs: edit.threadTs
2565
+ });
2566
+ })) throw new Error("Slack preview moved below a newer conversation message");
2187
2567
  if (!ttsSupplement) emitSlackMessageSentHooks({
2188
2568
  ...messageSentHookContext,
2189
2569
  to: messageSentHookTarget,
@@ -2235,8 +2615,10 @@ async function dispatchPreparedSlackMessage(prepared) {
2235
2615
  });
2236
2616
  }
2237
2617
  });
2618
+ if (info.kind === "final") progress.progressDraft.markFinalReplyDelivered();
2238
2619
  };
2239
2620
  let dispatchError;
2621
+ let agentRunFailed = false;
2240
2622
  let queuedFinal = false;
2241
2623
  let counts = {};
2242
2624
  try {
@@ -2251,6 +2633,10 @@ async function dispatchPreparedSlackMessage(prepared) {
2251
2633
  ctxPayload: prepared.ctxPayload,
2252
2634
  dispatcherOptions: {
2253
2635
  ...replyPipeline,
2636
+ transformReplyPayload: (payload) => {
2637
+ const transformed = replyPipeline.transformReplyPayload ? replyPipeline.transformReplyPayload(payload) : payload;
2638
+ return transformed ? filterPassiveThreadFailure(transformed) : null;
2639
+ },
2254
2640
  humanDelay: resolveHumanDelayConfig(cfg, route.agentId)
2255
2641
  },
2256
2642
  delivery: {
@@ -2272,102 +2658,62 @@ async function dispatchPreparedSlackMessage(prepared) {
2272
2658
  commentaryProgressEnabled: progress.commentaryProgressEnabled ? true : void 0,
2273
2659
  progressPreambleEnabled: progress.progressDraftActive && slackStreaming.mode === "progress" ? true : void 0,
2274
2660
  commentaryPayloadsEnabled: progress.commentaryProgressEnabled ? true : void 0,
2661
+ shouldDeliverCommentaryPayloads: progress.commentaryProgressEnabled ? progress.shouldYieldDraftProgress : void 0,
2275
2662
  onVerboseProgressVisibility: progress.commentaryProgressEnabled ? (isActive) => {
2276
2663
  progress.setShouldYieldDraftProgress(isActive);
2277
2664
  } : void 0,
2278
2665
  allowProgressCallbacksWhenSourceDeliverySuppressed: sourceReplyDeliveryMode === "message_tool_only" && statusReactionsEnabled ? true : void 0,
2279
2666
  allowToolLifecycleWhenProgressHidden: statusReactionsEnabled ? true : void 0,
2280
2667
  onPartialReply: useStreaming ? void 0 : !previewStreamingEnabled ? void 0 : async (payload) => {
2281
- progress.updateDraftFromPartial(payload.text);
2668
+ return progress.updateDraftFromPartial(payload.text);
2282
2669
  },
2283
- onAssistantMessageStart: progress.onDraftBoundary,
2670
+ onAssistantMessageStart: progress.onDraftBoundary ? async () => {
2671
+ await progress.onDraftBoundary?.();
2672
+ return false;
2673
+ } : void 0,
2284
2674
  onReasoningEnd: async () => {
2285
2675
  progress.progressReceipt.closeReasoning();
2286
2676
  await progress.onDraftBoundary?.();
2677
+ return false;
2287
2678
  },
2288
2679
  onQueuedFollowupAdmitted: progress.onQueuedFollowupAdmitted,
2680
+ onQueuedFollowupSettled: progress.onQueuedFollowupSettled,
2289
2681
  onReasoningStream: statusReactionsEnabled || progress.previewToolProgressEnabled ? async (payload) => {
2290
- await progress.pushReasoningProgress(payload);
2291
- if (!statusReactionsEnabled) return;
2682
+ const visible = await progress.pushReasoningProgress(payload);
2683
+ if (!statusReactionsEnabled) return visible;
2292
2684
  await statusReactions.setThinking();
2685
+ return visible;
2293
2686
  } : void 0,
2294
2687
  onToolStart: async (payload) => {
2295
2688
  if (statusReactionsEnabled) await statusReactions.setTool(payload.name);
2296
2689
  if (payload.phase === "start") progress.progressReceipt.noteToolCall(payload.name);
2297
- await progress.pushPreviewProgress(buildChannelProgressDraftLineForEntry(account.config, {
2298
- event: "tool",
2299
- itemId: payload.itemId,
2300
- toolCallId: payload.toolCallId,
2301
- name: payload.name,
2302
- phase: payload.phase,
2303
- args: payload.args
2304
- }, payload.detailMode ? { detailMode: payload.detailMode } : void 0), { toolName: payload.name });
2690
+ return await progress.progressDraft.pushToolEvent(payload);
2305
2691
  },
2306
2692
  onItemEvent: async (payload) => {
2307
- if (progress.streamMode === "status_final" && payload.kind === "preamble") {
2308
- if (progress.shouldYieldDraftProgress()) return;
2309
- await progress.progressDraft.pushPreambleHeadline(payload.progressText, { itemId: payload.itemId });
2693
+ if (progress.isProgressMode && payload.kind === "preamble") {
2694
+ if (progress.shouldYieldDraftProgress()) return false;
2695
+ const headlineVisible = await progress.progressDraft.pushPreambleHeadline(payload.progressText, { itemId: payload.itemId });
2310
2696
  if (progress.commentaryProgressEnabled) {
2311
- if (await progress.progressDraft.pushCommentaryProgress(payload.progressText, { itemId: payload.itemId })) progress.progressReceipt.noteCommentary(payload.itemId, payload.progressText);
2697
+ const accepted = await progress.progressDraft.pushCommentaryProgress(payload.progressText, { itemId: payload.itemId });
2698
+ if (accepted) progress.progressReceipt.noteCommentary(payload.itemId, payload.progressText);
2699
+ return accepted || headlineVisible;
2312
2700
  }
2313
- return;
2701
+ return headlineVisible;
2314
2702
  }
2315
- await progress.pushPreviewProgress(buildChannelProgressDraftLineForEntry(account.config, {
2316
- event: "item",
2317
- itemId: payload.itemId,
2318
- toolCallId: payload.toolCallId,
2319
- itemKind: payload.kind,
2320
- title: payload.title,
2321
- name: payload.name,
2322
- phase: payload.phase,
2323
- status: payload.status,
2324
- summary: payload.summary,
2325
- progressText: payload.progressText,
2326
- meta: payload.meta
2327
- }));
2703
+ return await progress.progressDraft.pushItemEvent(payload);
2328
2704
  },
2329
2705
  onPlanUpdate: async (payload) => {
2330
- if (payload.phase !== "update") return;
2331
- await progress.pushPlanProgress(payload.steps, payload.explanation);
2706
+ if (payload.phase !== "update") return false;
2707
+ return await progress.pushPlanProgress(payload.steps, payload.explanation);
2332
2708
  },
2333
2709
  onApprovalEvent: async (payload) => {
2334
- if (payload.phase !== "requested") return;
2335
- await progress.pushPreviewProgress(buildChannelProgressDraftLine({
2336
- event: "approval",
2337
- phase: payload.phase,
2338
- title: payload.title,
2339
- command: payload.command,
2340
- reason: payload.reason,
2341
- message: payload.message
2342
- }));
2710
+ return await progress.progressDraft.pushApprovalEvent(payload);
2343
2711
  },
2344
2712
  onCommandOutput: async (payload) => {
2345
- if (payload.phase !== "end") return;
2346
- await progress.pushPreviewProgress(buildChannelProgressDraftLine({
2347
- event: "command-output",
2348
- itemId: payload.itemId,
2349
- toolCallId: payload.toolCallId,
2350
- phase: payload.phase,
2351
- title: payload.title,
2352
- name: payload.name,
2353
- status: payload.status,
2354
- exitCode: payload.exitCode
2355
- }));
2713
+ return await progress.progressDraft.pushCommandOutputEvent(payload);
2356
2714
  },
2357
2715
  onPatchSummary: async (payload) => {
2358
- if (payload.phase !== "end") return;
2359
- await progress.pushPreviewProgress(buildChannelProgressDraftLine({
2360
- event: "patch",
2361
- itemId: payload.itemId,
2362
- toolCallId: payload.toolCallId,
2363
- phase: payload.phase,
2364
- title: payload.title,
2365
- name: payload.name,
2366
- added: payload.added,
2367
- modified: payload.modified,
2368
- deleted: payload.deleted,
2369
- summary: payload.summary
2370
- }));
2716
+ return await progress.progressDraft.pushPatchEvent(payload);
2371
2717
  }
2372
2718
  }
2373
2719
  });
@@ -2375,17 +2721,25 @@ async function dispatchPreparedSlackMessage(prepared) {
2375
2721
  const result = turnResult.dispatchResult;
2376
2722
  queuedFinal = result.queuedFinal;
2377
2723
  counts = result.counts;
2724
+ const agentRunOutcome = readAgentRunTerminalOutcome(result);
2725
+ agentRunFailed = agentRunOutcome === "failed";
2726
+ if (agentRunOutcome === "completed" && !sawTerminalFailurePayload && prepared.ctxPayload.ChatType === "channel") clearSlackThreadFailureNotice({
2727
+ accountId: account.accountId,
2728
+ channelId: message.channel,
2729
+ ...failureNoticeThreadTs ? { threadTs: failureNoticeThreadTs } : {},
2730
+ ...failureNoticeTeamId ? { teamId: failureNoticeTeamId } : {}
2731
+ });
2378
2732
  }
2379
2733
  } catch (err) {
2380
2734
  dispatchError = err;
2381
2735
  } finally {
2382
2736
  progress.progressDraft.cancel();
2383
- await draftStream?.discardPending();
2737
+ if (!progress.useDraftProgressCard) await draftStream?.discardPending();
2384
2738
  }
2385
2739
  let streamFallbackDelivered = false;
2386
2740
  const finalStream = delivery.streamSession;
2387
2741
  if (finalStream && !finalStream.stopped) try {
2388
- const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2742
+ const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError || agentRunFailed ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2389
2743
  if (completionChunks?.length) progress.nativeProgressCompletionSent = true;
2390
2744
  const stopResult = await stopSlackStream({
2391
2745
  session: finalStream,
@@ -2393,12 +2747,6 @@ async function dispatchPreparedSlackMessage(prepared) {
2393
2747
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2394
2748
  });
2395
2749
  delivery.acknowledgeStoppedStreamedDeliveries(finalStream, stopResult?.messageId);
2396
- if (progress.pendingNativeProgressReceipt && stopResult?.messageId && !progress.progressReceiptCollapsed) await progress.collapseProgressReceipt({
2397
- channelId: finalStream.channel,
2398
- messageId: stopResult.messageId,
2399
- text: progress.pendingNativeProgressReceipt,
2400
- threadTs: finalStream.threadTs
2401
- });
2402
2750
  } catch (err) {
2403
2751
  if (err instanceof SlackStreamNotDeliveredError) {
2404
2752
  streamFallbackDelivered = await delivery.deliverPendingStreamFallback(finalStream, err);
@@ -2420,19 +2768,25 @@ async function dispatchPreparedSlackMessage(prepared) {
2420
2768
  observedReplyDelivery: delivery.observedReplyDelivery,
2421
2769
  fallbackDelivered: streamFallbackDelivered
2422
2770
  });
2423
- if (statusReactionsEnabled) if (dispatchError) await statusReactions.setError();
2424
- else if (anyReplyDelivered) {
2771
+ if (pendingFailureNotice && anyReplyDelivered) recordSlackThreadFailureNotice(pendingFailureNotice);
2772
+ if (dispatchError || agentRunFailed) await progress.finalizeDraftProgressCard("error");
2773
+ await progress.dropDetachedProgressCards();
2774
+ if (statusReactionsEnabled) if (dispatchError || agentRunFailed) {
2775
+ await statusReactions.setError();
2776
+ statusReactions.restoreInitial();
2777
+ } else if (anyReplyDelivered) {
2425
2778
  await statusReactions.setDone();
2426
2779
  statusReactions.restoreInitial();
2427
2780
  } else await statusReactions.restoreInitial();
2428
2781
  const participationThreadTs = delivery.usedReplyThreadTs ?? statusThreadTs;
2429
2782
  if (anyReplyDelivered && participationThreadTs) recordSlackThreadParticipation(account.accountId, message.channel, participationThreadTs, {
2430
2783
  agentId: route.agentId,
2431
- ...prepared.eventScope ? { teamId: prepared.eventScope.teamId } : {}
2784
+ teamId: prepared.eventScope?.teamId
2432
2785
  });
2433
2786
  if (dispatchError) throw toErrorObject(dispatchError, "Slack dispatch failed");
2434
- if (!anyReplyDelivered && !draftPreviewCommitted.value) {
2787
+ if (!anyReplyDelivered && !draftPreviewCommitted.value && !(agentRunFailed && progress.useDraftProgressCard)) {
2435
2788
  await draftStream?.clear();
2789
+ await progress.dropDetachedProgressCards();
2436
2790
  return;
2437
2791
  }
2438
2792
  if (shouldLogVerbose()) {
@@ -2442,19 +2796,13 @@ async function dispatchPreparedSlackMessage(prepared) {
2442
2796
  }
2443
2797
  //#endregion
2444
2798
  //#region extensions/slack/src/agent-context.ts
2445
- function asRecord(value) {
2446
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2447
- }
2448
- function trimmedString(value) {
2449
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
2450
- }
2451
2799
  function normalizeEntity(value) {
2452
- const entity = asRecord(value);
2453
- const type = trimmedString(entity?.type);
2800
+ const entity = asOptionalRecord(value);
2801
+ const type = normalizeOptionalString(entity?.type);
2454
2802
  if (!entity || !type) return;
2455
- const teamId = trimmedString(entity.team_id);
2803
+ const teamId = normalizeOptionalString(entity.team_id);
2456
2804
  if (type === "slack#/types/channel_id" || type === "slack#/types/canvas_id" || type === "slack#/types/list_id") {
2457
- const entityValue = trimmedString(entity.value);
2805
+ const entityValue = normalizeOptionalString(entity.value);
2458
2806
  return entityValue ? {
2459
2807
  type,
2460
2808
  value: entityValue,
@@ -2462,9 +2810,9 @@ function normalizeEntity(value) {
2462
2810
  } : void 0;
2463
2811
  }
2464
2812
  if (type !== "slack#/types/message_context") return;
2465
- const message = asRecord(entity.value);
2466
- const channelId = trimmedString(message?.channel_id);
2467
- const messageTs = trimmedString(message?.message_ts);
2813
+ const message = asOptionalRecord(entity.value);
2814
+ const channelId = normalizeOptionalString(message?.channel_id);
2815
+ const messageTs = normalizeOptionalString(message?.message_ts);
2468
2816
  return channelId && messageTs ? {
2469
2817
  type,
2470
2818
  value: {
@@ -2475,10 +2823,10 @@ function normalizeEntity(value) {
2475
2823
  } : void 0;
2476
2824
  }
2477
2825
  function isSlackAppContext(value) {
2478
- return Boolean(asRecord(value));
2826
+ return Boolean(asOptionalRecord(value));
2479
2827
  }
2480
2828
  function normalizeSlackAppContextEntities(value) {
2481
- const context = asRecord(value);
2829
+ const context = asOptionalRecord(value);
2482
2830
  if (!Array.isArray(context?.entities)) return [];
2483
2831
  return context.entities.flatMap((entity) => {
2484
2832
  const normalized = normalizeEntity(entity);
@@ -2487,13 +2835,15 @@ function normalizeSlackAppContextEntities(value) {
2487
2835
  }
2488
2836
  //#endregion
2489
2837
  //#region extensions/slack/src/monitor/message-handler/preflight-audio.ts
2490
- const SLACK_DEFAULT_ECHO_TRANSCRIPT_FORMAT = "📝 \"{transcript}\"";
2491
- const loadSlackPreflightAudioRuntime = createLazyRuntimeModule(() => import("./preflight-audio.runtime-D5Bgags0.js"));
2492
2838
  function isSlackAudioFile(file) {
2493
2839
  if (file.subtype === "slack_audio") return true;
2494
2840
  if ((file.mimetype?.split(";")[0]?.trim().toLowerCase())?.startsWith("audio/")) return true;
2495
2841
  return Boolean(mimeTypeFromFilePath(file.name)?.startsWith("audio/"));
2496
2842
  }
2843
+ const slackPreflightAudio = createChannelPreflightAudio({
2844
+ channel: "slack",
2845
+ isAudio: isSlackAudioFile
2846
+ });
2497
2847
  function findCaptionlessSlackAudioFile(message) {
2498
2848
  if (message.text?.trim()) return;
2499
2849
  return message.files?.slice(0, 8).find(isSlackAudioFile);
@@ -2501,74 +2851,30 @@ function findCaptionlessSlackAudioFile(message) {
2501
2851
  function formatSlackAudioTranscriptForAgent(params) {
2502
2852
  return [`[Audio transcript (machine-generated, untrusted)]: ${JSON.stringify(params.transcript)}`, params.rawBody].filter(Boolean).join("\n");
2503
2853
  }
2504
- function suppressSlackPreflightAudioEcho(cfg) {
2505
- const audio = cfg.tools?.media?.audio;
2506
- if (!audio?.echoTranscript) return cfg;
2507
- return {
2508
- ...cfg,
2509
- tools: {
2510
- ...cfg.tools,
2511
- media: {
2512
- ...cfg.tools?.media,
2513
- audio: {
2514
- ...audio,
2515
- echoTranscript: false
2516
- }
2517
- }
2518
- }
2519
- };
2520
- }
2521
2854
  async function resolveSlackPreflightAudioTranscript(params) {
2522
2855
  const mediaIndex = params.media.findIndex((entry) => entry.contentType?.toLowerCase().startsWith("audio/"));
2523
2856
  if (mediaIndex < 0) return null;
2524
- try {
2525
- const { transcribeFirstAudio } = await loadSlackPreflightAudioRuntime();
2526
- const transcript = await transcribeFirstAudio({
2527
- ctx: {
2528
- media: [...params.media],
2529
- Provider: "slack",
2530
- Surface: "slack",
2531
- OriginatingChannel: "slack",
2532
- OriginatingTo: params.originatingTo,
2533
- AccountId: params.accountId,
2534
- MessageThreadId: params.messageThreadId,
2535
- ChatType: "channel",
2536
- SessionKey: params.sessionKey
2537
- },
2538
- cfg: suppressSlackPreflightAudioEcho(params.cfg)
2539
- });
2540
- return transcript ? {
2541
- transcript,
2542
- mediaIndex
2543
- } : null;
2544
- } catch (err) {
2545
- logVerbose(`slack: audio preflight transcription failed: ${String(err)}`);
2546
- return null;
2547
- }
2548
- }
2549
- function formatSlackAudioTranscriptEcho(transcript, format) {
2550
- return format.replace("{transcript}", () => transcript);
2857
+ const transcript = await slackPreflightAudio.resolve({ request: {
2858
+ ctx: {
2859
+ media: [...params.media],
2860
+ Provider: "slack",
2861
+ Surface: "slack",
2862
+ OriginatingChannel: "slack",
2863
+ OriginatingTo: params.originatingTo,
2864
+ AccountId: params.accountId,
2865
+ MessageThreadId: params.messageThreadId,
2866
+ ChatType: "channel",
2867
+ SessionKey: params.sessionKey
2868
+ },
2869
+ cfg: params.cfg
2870
+ } });
2871
+ return transcript ? {
2872
+ transcript,
2873
+ mediaIndex
2874
+ } : null;
2551
2875
  }
2552
2876
  async function sendSlackPreflightAudioTranscriptEcho(params) {
2553
- const audio = params.cfg.tools?.media?.audio;
2554
- if (!audio?.echoTranscript) return;
2555
- const text = formatSlackAudioTranscriptEcho(params.transcript, audio.echoFormat ?? SLACK_DEFAULT_ECHO_TRANSCRIPT_FORMAT);
2556
- try {
2557
- const { sendDurableMessageBatch } = await loadSlackPreflightAudioRuntime();
2558
- const send = await sendDurableMessageBatch({
2559
- cfg: params.cfg,
2560
- channel: "slack",
2561
- to: params.originatingTo,
2562
- accountId: params.accountId,
2563
- threadId: params.messageThreadId,
2564
- payloads: [{ text }],
2565
- bestEffort: true,
2566
- durability: "best_effort"
2567
- });
2568
- if (send.status === "failed") throw send.error;
2569
- } catch (err) {
2570
- logVerbose(`slack: audio transcript echo failed: ${String(err)}`);
2571
- }
2877
+ await slackPreflightAudio.send(params);
2572
2878
  }
2573
2879
  async function discardSlackPreflightMedia(media) {
2574
2880
  await Promise.allSettled((media ?? []).map((entry) => fs.rm(entry.path, { force: true })));
@@ -2578,7 +2884,7 @@ async function discardSlackPreflightMedia(media) {
2578
2884
  const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4;
2579
2885
  const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20;
2580
2886
  const SLACK_USER_MENTION_RE$1 = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
2581
- const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-D3NuJvva.js").then((n) => n.g));
2887
+ const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-BTDXwwE3.js").then((n) => n._));
2582
2888
  function collectUniqueSlackMentionIds$1(texts) {
2583
2889
  const seen = /* @__PURE__ */ new Set();
2584
2890
  const mentionIds = [];
@@ -2639,7 +2945,14 @@ async function resolveSlackMessageContent(params) {
2639
2945
  const mergedMedia = [...media ?? [], ...attachmentContent?.media ?? []];
2640
2946
  const effectiveDirectMedia = mergedMedia.length > 0 ? mergedMedia : null;
2641
2947
  const mediaPlaceholder = effectiveDirectMedia ? effectiveDirectMedia.map((item) => item.placeholder).join(" ") : void 0;
2642
- const fallbackFiles = ownFiles ?? [];
2948
+ const fallbackFileIds = /* @__PURE__ */ new Set();
2949
+ const fallbackFiles = [...ownFiles ?? [], ...attachmentContent?.files ?? []].filter((file) => {
2950
+ const fileId = normalizeOptionalString(file.id);
2951
+ if (!fileId) return true;
2952
+ if (fallbackFileIds.has(fileId)) return false;
2953
+ fallbackFileIds.add(fileId);
2954
+ return true;
2955
+ });
2643
2956
  const fileOnlyFallback = !mediaPlaceholder && fallbackFiles.length > 0 ? fallbackFiles.slice(0, 8).map((file) => formatSlackFileReference(file)).join(", ") : void 0;
2644
2957
  const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : void 0;
2645
2958
  let botAttachmentText;
@@ -2678,13 +2991,18 @@ async function resolveSlackMessageContent(params) {
2678
2991
  renderedMentions.set(result.userId, result.rendered);
2679
2992
  }
2680
2993
  }
2681
- const rawBody = [
2994
+ let rawBody = [
2682
2995
  renderSlackUserMentions(textParts[0], renderedMentions),
2683
2996
  renderSlackUserMentions(textParts[1], renderedMentions),
2684
2997
  renderSlackUserMentions(textParts[2], renderedMentions),
2685
2998
  mediaPlaceholder,
2686
2999
  fileOnlyPlaceholder
2687
3000
  ].filter(Boolean).join("\n") || "";
3001
+ const unavailableImageCount = attachmentContent?.unavailableImageCount ?? 0;
3002
+ if (unavailableImageCount > 0) rawBody = formatInboundMediaUnavailableText({
3003
+ body: rawBody,
3004
+ notice: `[slack ${unavailableImageCount > 1 ? `${unavailableImageCount} forwarded images` : "forwarded image"} unavailable]`
3005
+ });
2688
3006
  if (!rawBody) return null;
2689
3007
  return {
2690
3008
  rawBody,
@@ -2783,10 +3101,11 @@ function normalizeSlackRouteBindingPeer(peer) {
2783
3101
  return;
2784
3102
  }
2785
3103
  })();
2786
- if (!target || !slackTargetKindMatchesPeer(peer.kind, target.kind) || target.id === peer.id) return peer;
2787
- return {
3104
+ if (!target || !slackTargetKindMatchesPeer(peer.kind, target.kind)) return peer;
3105
+ const normalizedId = target.teamId ? `team:${target.teamId}:${target.kind}:${target.id}` : target.id;
3106
+ return normalizedId === peer.id ? peer : {
2788
3107
  ...peer,
2789
- id: target.id
3108
+ id: normalizedId
2790
3109
  };
2791
3110
  }
2792
3111
  function normalizeSlackRouteBindingConfig(cfg) {
@@ -2821,12 +3140,7 @@ function normalizeSlackRouteBindingConfig(cfg) {
2821
3140
  return normalizedCfg;
2822
3141
  }
2823
3142
  function resolveSlackBaseConversationId(params) {
2824
- const raw = params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel;
2825
- return params.eventScope ? `team:${encodeURIComponent(params.eventScope.teamId)}:${raw}` : raw;
2826
- }
2827
- function qualifySlackPeerId(params) {
2828
- if (!params.eventScope) return params.id;
2829
- return `team:${encodeURIComponent(params.eventScope.teamId)}:${params.kind}:${encodeURIComponent(params.id)}`;
3143
+ return qualifySlackConversationId(params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel, params.eventScope);
2830
3144
  }
2831
3145
  function resolveSlackInitialAgentRoute(params) {
2832
3146
  const route = resolveAgentRoute({
@@ -2836,7 +3150,7 @@ function resolveSlackInitialAgentRoute(params) {
2836
3150
  teamId: params.eventScope?.teamId || params.ctx.teamId || void 0,
2837
3151
  peer: {
2838
3152
  kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
2839
- id: qualifySlackPeerId({
3153
+ id: qualifySlackRoutePeerId({
2840
3154
  id: params.isDirectMessage ? params.message.user ?? "unknown" : params.message.channel,
2841
3155
  kind: params.isDirectMessage ? "user" : "channel",
2842
3156
  eventScope: params.eventScope
@@ -2844,8 +3158,11 @@ function resolveSlackInitialAgentRoute(params) {
2844
3158
  }
2845
3159
  });
2846
3160
  if (!params.eventScope || !params.isDirectMessage || route.dmScope !== "main") return route;
2847
- const partition = `account:${encodeURIComponent(params.account.accountId).toLowerCase()}:team:${encodeURIComponent(params.eventScope.teamId).toLowerCase()}`;
2848
- const sessionKey = `${route.sessionKey}:${partition}`;
3161
+ const sessionKey = resolveSlackEnterpriseMainDmSessionKey({
3162
+ baseSessionKey: route.sessionKey,
3163
+ accountId: params.account.accountId,
3164
+ eventScope: params.eventScope
3165
+ });
2849
3166
  return {
2850
3167
  ...route,
2851
3168
  sessionKey,
@@ -2953,8 +3270,12 @@ function isSlackThreadAuthorCurrentBot(params) {
2953
3270
  return false;
2954
3271
  }
2955
3272
  function resolveSlackThreadHistoryFilterPolicy(params) {
2956
- if (!params.includeBotStarterAsRootContext || !params.starterTs) return {};
2957
- return { retainCurrentBotRootTs: params.starterTs };
3273
+ if (params.retainCurrentBotHistory) return { currentBot: "all" };
3274
+ if (!params.includeBotStarterAsRootContext || !params.starterTs) return { currentBot: "omit" };
3275
+ return {
3276
+ currentBot: "root-only",
3277
+ rootTs: params.starterTs
3278
+ };
2958
3279
  }
2959
3280
  function applySlackThreadHistoryFilterPolicy(params) {
2960
3281
  const kept = [];
@@ -2967,7 +3288,7 @@ function applySlackThreadHistoryFilterPolicy(params) {
2967
3288
  kept.push(entry);
2968
3289
  continue;
2969
3290
  }
2970
- if (params.policy.retainCurrentBotRootTs && entry.ts === params.policy.retainCurrentBotRootTs) kept.push(entry);
3291
+ if (params.policy.currentBot === "all" || params.policy.currentBot === "root-only" && entry.ts === params.policy.rootTs) kept.push(entry);
2971
3292
  else omittedCurrentBot += 1;
2972
3293
  }
2973
3294
  return {
@@ -2996,11 +3317,11 @@ function formatSlackThreadLabelSnippet(text) {
2996
3317
  }
2997
3318
  //#endregion
2998
3319
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
2999
- const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-D3NuJvva.js").then((n) => n.g));
3320
+ const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-BTDXwwE3.js").then((n) => n._));
3000
3321
  const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
3001
3322
  function resolveSlackThreadSessionFreshness(params) {
3002
3323
  return params.ctx.channelRuntime?.session?.resolveEntryResetFreshness?.({
3003
- defaultAgentId: resolveDefaultAgentId(params.ctx.cfg),
3324
+ agentId: params.agentId,
3004
3325
  storePath: params.storePath,
3005
3326
  sessionKey: params.sessionKey,
3006
3327
  sessionCfg: params.ctx.cfg.session,
@@ -3066,6 +3387,7 @@ async function resolveSlackThreadContextData(params) {
3066
3387
  let threadStarterMedia = null;
3067
3388
  const threadSessionFreshness = params.isThreadReply && params.threadTs ? resolveSlackThreadSessionFreshness({
3068
3389
  ctx: params.ctx,
3390
+ agentId: params.agentId,
3069
3391
  storePath: params.storePath,
3070
3392
  sessionKey: params.sessionKey
3071
3393
  }) : void 0;
@@ -3073,7 +3395,9 @@ async function resolveSlackThreadContextData(params) {
3073
3395
  storePath: params.storePath,
3074
3396
  sessionKey: params.sessionKey
3075
3397
  }) : void 0;
3076
- const shouldSeedInitialThreadContext = Boolean(params.isThreadReply && params.threadTs && (threadSessionFreshness ? threadSessionFreshness.state !== "fresh" : threadSessionPreviousTimestamp === void 0));
3398
+ const isMissingThreadSession = threadSessionFreshness ? threadSessionFreshness.state === "missing" : threadSessionPreviousTimestamp === void 0;
3399
+ const isOutboundOnlyThreadSession = threadSessionFreshness !== void 0 && threadSessionFreshness.state !== "missing" && threadSessionFreshness.entry.lastInteractionAt === void 0 && threadSessionFreshness.entry.updatedAt !== 0;
3400
+ const shouldSeedInitialThreadContext = Boolean(params.isThreadReply && params.threadTs && (threadSessionFreshness ? threadSessionFreshness.state !== "fresh" || isOutboundOnlyThreadSession : threadSessionPreviousTimestamp === void 0));
3077
3401
  const shouldLoadInitialThreadHistory = shouldSeedInitialThreadContext || params.forceInitialHistory === true;
3078
3402
  if (!params.isThreadReply || !params.threadTs) return {
3079
3403
  threadStarterBody,
@@ -3151,7 +3475,8 @@ async function resolveSlackThreadContextData(params) {
3151
3475
  history: threadHistoryWithBotRoot,
3152
3476
  policy: resolveSlackThreadHistoryFilterPolicy({
3153
3477
  includeBotStarterAsRootContext,
3154
- starterTs: currentBotRootTs
3478
+ starterTs: currentBotRootTs,
3479
+ retainCurrentBotHistory: params.isGroupDm && (isMissingThreadSession || isOutboundOnlyThreadSession)
3155
3480
  }),
3156
3481
  identity: botIdentity
3157
3482
  });
@@ -3298,6 +3623,13 @@ const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4;
3298
3623
  const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
3299
3624
  const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1e3;
3300
3625
  const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3e3;
3626
+ const SLACK_CHANNEL_ACCESS_DOCS_URL = "https://docs.openclaw.ai/channels/slack#access-control-and-routing";
3627
+ function resolveSlackGroupSessionSubject(params) {
3628
+ const channelName = normalizeOptionalString(params.channelName);
3629
+ const workspaceName = normalizeOptionalString(params.workspaceName);
3630
+ if (channelName && workspaceName) return `${workspaceName} #${channelName}`;
3631
+ return `Slack Channel (Workspace ID: ${params.workspaceId}, Channel ID: ${params.channelId})`;
3632
+ }
3301
3633
  function recordString(record, key) {
3302
3634
  return normalizeOptionalString(record?.[key]);
3303
3635
  }
@@ -3504,6 +3836,8 @@ async function resolveSlackConversationContext(params) {
3504
3836
  const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
3505
3837
  const isRoomish = isRoom || isGroupDm;
3506
3838
  const channelConfig = isRoom ? resolveSlackChannelConfig({
3839
+ teamId: params.eventScope?.teamId ?? ctx.teamId,
3840
+ allowUnscoped: ctx.installationIdentity?.kind !== "enterprise",
3507
3841
  channelId: message.channel,
3508
3842
  channelName,
3509
3843
  channels: ctx.channelsConfig,
@@ -3545,14 +3879,38 @@ async function authorizeSlackInboundMessage(params) {
3545
3879
  return null;
3546
3880
  }
3547
3881
  if (!ctx.isChannelAllowed({
3882
+ teamId: params.eventScope?.teamId ?? ctx.teamId,
3548
3883
  channelId: message.channel,
3549
3884
  channelName,
3550
3885
  channelType: resolvedChannelType
3551
3886
  })) {
3887
+ if (conversation.isRoom && ctx.groupPolicy === "allowlist" && params.explicitBotMention && !isBotMessage && message.user) {
3888
+ let subject = "This OpenClaw bot";
3889
+ if (ctx.botUserId) try {
3890
+ const botName = normalizeOptionalString((await ctx.resolveUserName(ctx.botUserId, params.eventScope))?.name);
3891
+ if (botName) subject = escapeSlackMrkdwn(botName);
3892
+ } catch (error) {
3893
+ logVerbose(`slack allowlist denial bot-name lookup failed: ${formatSlackError(error)}`);
3894
+ }
3895
+ try {
3896
+ await (params.eventScope?.client ?? ctx.app.client).chat.postEphemeral({
3897
+ token: ctx.botToken,
3898
+ channel: message.channel,
3899
+ user: message.user,
3900
+ text: `${subject} can’t reply here because this channel isn’t in its OpenClaw channel allowlist. Ask the OpenClaw owner to allow this channel. <${SLACK_CHANNEL_ACCESS_DOCS_URL}|Learn how to configure Slack channel access.>`
3901
+ });
3902
+ params.onVisibleDrop?.();
3903
+ } catch (error) {
3904
+ ctx.runtime.error?.(`slack allowlist denial notice failed for channel ${message.channel}: ${formatSlackError(error)}`);
3905
+ }
3906
+ }
3552
3907
  logVerbose("slack: drop message (channel not allowed)");
3553
3908
  return null;
3554
3909
  }
3555
- const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, { includePairingStore: isDirectMessage });
3910
+ const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, {
3911
+ includePairingStore: isDirectMessage,
3912
+ eventScope: params.eventScope
3913
+ });
3556
3914
  if (isDirectMessage) {
3557
3915
  const directUserId = message.user;
3558
3916
  if (!directUserId) {
@@ -3563,6 +3921,7 @@ async function authorizeSlackInboundMessage(params) {
3563
3921
  ctx,
3564
3922
  accountId: account.accountId,
3565
3923
  senderId: directUserId,
3924
+ eventScope: params.eventScope,
3566
3925
  allowFromLower,
3567
3926
  resolveSenderName: (userId) => ctx.resolveUserName(userId, params.eventScope),
3568
3927
  sendPairingReply: async (text) => {
@@ -3570,7 +3929,8 @@ async function authorizeSlackInboundMessage(params) {
3570
3929
  cfg: ctx.cfg,
3571
3930
  token: ctx.botToken,
3572
3931
  client: params.eventScope?.client ?? ctx.app.client,
3573
- accountId: account.accountId
3932
+ accountId: account.accountId,
3933
+ eventScope: params.eventScope
3574
3934
  });
3575
3935
  },
3576
3936
  onDisabled: () => {
@@ -3590,10 +3950,10 @@ async function authorizeSlackInboundMessage(params) {
3590
3950
  async function prepareSlackMessage(params) {
3591
3951
  const { ctx, account, message, opts } = params;
3592
3952
  const slackClient = opts.eventScope?.client ?? ctx.app.client;
3593
- const threadStarterWorkspaceScope = opts.eventScope ? {
3953
+ const threadStarterWorkspaceScope = {
3594
3954
  accountId: account.accountId,
3595
- teamId: opts.eventScope.teamId
3596
- } : void 0;
3955
+ teamId: opts.eventScope?.teamId ?? ctx.teamId
3956
+ };
3597
3957
  const cfg = ctx.cfg;
3598
3958
  const conversation = await resolveSlackConversationContext({
3599
3959
  ctx,
@@ -3602,16 +3962,20 @@ async function prepareSlackMessage(params) {
3602
3962
  eventScope: opts.eventScope
3603
3963
  });
3604
3964
  const { channelInfo, channelName, isDirectMessage, isGroupDm, isRoom, isRoomish, channelConfig, allowBotsMode, isBotMessage } = conversation;
3965
+ const messageText = message.text ?? "";
3966
+ const mentionMetadata = collectSlackMentionMetadata(messageText);
3967
+ const normalizedBotUserId = normalizeSlackId(ctx.botUserId);
3605
3968
  const authorization = await authorizeSlackInboundMessage({
3606
3969
  ctx,
3607
3970
  account,
3608
3971
  message,
3609
3972
  conversation,
3610
- eventScope: opts.eventScope
3973
+ explicitBotMention: opts.source === "app_mention" || Boolean(normalizedBotUserId && mentionMetadata.mentionedUserIds.includes(normalizedBotUserId)),
3974
+ eventScope: opts.eventScope,
3975
+ onVisibleDrop: opts.onVisibleDrop
3611
3976
  });
3612
3977
  if (!authorization) return null;
3613
3978
  const { senderId, allowFromLower } = authorization;
3614
- const messageText = message.text ?? "";
3615
3979
  let resolvedSenderName = normalizeOptionalString(message.username);
3616
3980
  const resolveSenderName = async () => {
3617
3981
  if (resolvedSenderName) return resolvedSenderName;
@@ -3625,7 +3989,6 @@ async function prepareSlackMessage(params) {
3625
3989
  resolvedSenderName = message.user ?? message.bot_id ?? "unknown";
3626
3990
  return resolvedSenderName;
3627
3991
  };
3628
- const mentionMetadata = collectSlackMentionMetadata(messageText);
3629
3992
  const { mentionedUserIds, mentionedSubteamIds, hasAnyMention } = mentionMetadata;
3630
3993
  const messageAssistantThreadContext = resolveSlackMessageAssistantThreadContext(message);
3631
3994
  const assistantContextLookupChannelId = messageAssistantThreadContext?.assistantChannelId ?? message.channel;
@@ -3688,7 +4051,11 @@ async function prepareSlackMessage(params) {
3688
4051
  }));
3689
4052
  const buildPolicyMentionRegexes = (agentId) => resolveCachedMentionRegexes(ctx, agentId, {
3690
4053
  provider: "slack",
3691
- conversationId: message.channel,
4054
+ conversationId: formatSlackTarget({
4055
+ teamId: opts.eventScope?.teamId,
4056
+ kind: "channel",
4057
+ id: message.channel
4058
+ }),
3692
4059
  providerPolicy: account.config.mentionPatterns
3693
4060
  });
3694
4061
  let mentionRegexes = buildPolicyMentionRegexes(routing.route.agentId);
@@ -3729,13 +4096,13 @@ async function prepareSlackMessage(params) {
3729
4096
  name: void 0
3730
4097
  });
3731
4098
  let implicitMentionKinds = [];
3732
- if (!isDirectMessage && ctx.botUserId && message.thread_ts && !wasMentioned) {
3733
- const replyToBotKinds = implicitMentionKindWhen("reply_to_bot", message.parent_user_id === ctx.botUserId);
4099
+ if (!isDirectMessage && message.thread_ts && !wasMentioned) {
4100
+ const replyToBotKinds = implicitMentionKindWhen("reply_to_bot", Boolean(ctx.botUserId && message.parent_user_id === ctx.botUserId));
3734
4101
  implicitMentionKinds = replyToBotKinds.length > 0 ? replyToBotKinds : implicitMentionKindWhen("bot_thread_participant", await hasSlackThreadParticipationWithPersistence({
3735
4102
  accountId: account.accountId,
3736
4103
  channelId: message.channel,
3737
4104
  threadTs: message.thread_ts,
3738
- ...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
4105
+ teamId: opts.eventScope?.teamId
3739
4106
  }));
3740
4107
  }
3741
4108
  const recordDroppedHistory = async (reason) => {
@@ -3843,12 +4210,14 @@ async function prepareSlackMessage(params) {
3843
4210
  const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
3844
4211
  const hasAbortRequest = isAbortRequestText(textForCommandDetection);
3845
4212
  const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
3846
- const messageIngress = await resolveSlackCommandIngress({
4213
+ const resolveMessageIngress = async (contextBinding, threadId) => await resolveSlackCommandIngress({
3847
4214
  ctx,
4215
+ teamId: opts.eventScope?.teamId ?? ctx.teamId,
3848
4216
  senderId,
3849
4217
  senderName: senderNameForAuth,
3850
4218
  channelType: conversation.resolvedChannelType ?? "channel",
3851
4219
  channelId: message.channel,
4220
+ threadId,
3852
4221
  ownerAllowFromLower: allowFromLower,
3853
4222
  channelUsers: isRoom ? channelConfig?.users : void 0,
3854
4223
  allowTextCommands,
@@ -3863,8 +4232,10 @@ async function prepareSlackMessage(params) {
3863
4232
  requireMention: shouldRequireMention,
3864
4233
  allowTextCommands,
3865
4234
  implicitMentions
3866
- }
4235
+ },
4236
+ contextBinding
3867
4237
  });
4238
+ let messageIngress = await resolveMessageIngress();
3868
4239
  const senderGate = messageIngress.senderAccess.gate;
3869
4240
  if (isRoomish && senderGate?.allowed === false) {
3870
4241
  logVerbose(`Blocked unauthorized slack sender ${senderId} (not in sender allowlist)`);
@@ -3885,6 +4256,18 @@ async function prepareSlackMessage(params) {
3885
4256
  channel: "slack",
3886
4257
  accountId: account.accountId
3887
4258
  });
4259
+ const preflightChannelTarget = formatSlackTarget({
4260
+ teamId: opts.eventScope?.teamId,
4261
+ kind: "channel",
4262
+ id: message.channel,
4263
+ explicitKind: true
4264
+ });
4265
+ const replyRouteTarget = formatSlackTarget({
4266
+ teamId: opts.eventScope?.teamId,
4267
+ kind: isDirectMessage ? "user" : "channel",
4268
+ id: isDirectMessage ? senderId : message.channel,
4269
+ explicitKind: true
4270
+ });
3888
4271
  const commandAuthorized = messageIngress.commandAccess.authorized;
3889
4272
  if (isRoomish && messageIngress.commandAccess.shouldBlockControlCommand) {
3890
4273
  logInboundDrop({
@@ -3931,7 +4314,7 @@ async function prepareSlackMessage(params) {
3931
4314
  media: preflightMedia,
3932
4315
  cfg,
3933
4316
  accountId: account.accountId,
3934
- originatingTo: `channel:${message.channel}`,
4317
+ originatingTo: preflightChannelTarget,
3935
4318
  sessionKey: preflightRouting.sessionKey,
3936
4319
  messageThreadId: preflightRouting.threadContext.messageThreadId
3937
4320
  }) : null;
@@ -4076,14 +4459,22 @@ async function prepareSlackMessage(params) {
4076
4459
  return false;
4077
4460
  }) : statusReactionsWillHandle ? Promise.resolve(true) : null;
4078
4461
  const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
4462
+ const workspaceId = opts.eventScope?.teamId || ctx.teamId;
4463
+ const workspaceName = ctx.installationIdentity?.kind === "workspace" && ctx.installationIdentity.teamId === workspaceId ? ctx.installationIdentity.teamName : void 0;
4464
+ const groupSessionSubject = isRoomish ? resolveSlackGroupSessionSubject({
4465
+ channelId: message.channel,
4466
+ channelName,
4467
+ workspaceId,
4468
+ workspaceName
4469
+ }) : void 0;
4079
4470
  const senderName = await resolveSenderName();
4080
4471
  const preview = truncateUtf16Safe(bodyForAgent.replace(/\s+/g, " "), 160);
4081
4472
  const inboundLabel = isDirectMessage ? `Slack DM from ${senderName}` : `Slack message in ${roomLabel} from ${senderName}`;
4082
4473
  const slackFrom = isDirectMessage ? `slack:${message.user}` : isRoom ? `slack:channel:${message.channel}` : `slack:group:${message.channel}`;
4083
- enqueueSystemEvent(inboundLabel, {
4084
- sessionKey,
4085
- contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`
4086
- });
4474
+ enqueueRoutedSystemEvent(inboundLabel, {
4475
+ ...route,
4476
+ sessionKey
4477
+ }, { contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}` });
4087
4478
  const envelopeFrom = resolveConversationLabel$1({
4088
4479
  ChatType: chatType,
4089
4480
  SenderName: senderName,
@@ -4147,7 +4538,6 @@ async function prepareSlackMessage(params) {
4147
4538
  envelope: envelopeOptions
4148
4539
  })
4149
4540
  });
4150
- const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`;
4151
4541
  const { channelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({
4152
4542
  isRoomish,
4153
4543
  channelInfo,
@@ -4155,8 +4545,10 @@ async function prepareSlackMessage(params) {
4155
4545
  });
4156
4546
  const { threadStarterBody, threadHistoryBody, shouldSeedInitialThreadContext, threadLabel, threadStarterMedia } = await resolveSlackThreadContextData({
4157
4547
  ctx,
4548
+ agentId: route.agentId,
4158
4549
  account,
4159
4550
  message,
4551
+ isGroupDm,
4160
4552
  isThreadReply,
4161
4553
  threadTs,
4162
4554
  threadStarter,
@@ -4180,8 +4572,20 @@ async function prepareSlackMessage(params) {
4180
4572
  const commandBody = textForCommandDetection.trim();
4181
4573
  const supplementalThreadHistoryBody = directThreadRoutedToDmSession && !threadHistoryBody ? threadStarterBody : threadHistoryBody;
4182
4574
  const effectiveMessageThreadId = assistantThreadContext?.threadTs ?? agentViewThreadTs ?? threadContext.messageThreadId;
4575
+ const boundMessageThreadId = directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId;
4576
+ messageIngress = await resolveMessageIngress({
4577
+ agentId: route.agentId,
4578
+ sessionKey,
4579
+ messageId: threadContext.messageTs,
4580
+ inboundEventKind
4581
+ }, boundMessageThreadId);
4582
+ if (messageIngress.ingress.admission !== "dispatch") {
4583
+ logVerbose(`Blocked slack sender ${senderId} after final route binding`);
4584
+ return null;
4585
+ }
4183
4586
  const agentContextEntities = isAgentViewMessage ? normalizeSlackAppContextEntities(message.app_context) : [];
4184
- const ctxPayload = buildChannelInboundEventContext({
4587
+ const ctxPayload = (ctx.buildContext ?? buildChannelInboundEventContext)({
4588
+ channelIngress: messageIngress,
4185
4589
  channel: "slack",
4186
4590
  accountId: route.accountId,
4187
4591
  messageId: threadContext.messageTs,
@@ -4198,20 +4602,21 @@ async function prepareSlackMessage(params) {
4198
4602
  id: message.channel,
4199
4603
  label: envelopeFrom,
4200
4604
  spaceId: opts.eventScope?.teamId || ctx.teamId || void 0,
4201
- threadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
4605
+ threadId: boundMessageThreadId,
4202
4606
  nativeChannelId: message.channel
4203
4607
  },
4204
4608
  route: {
4205
4609
  agentId: route.agentId,
4206
4610
  dmScope: route.dmScope,
4207
4611
  accountId: route.accountId,
4208
- routeSessionKey: sessionKey,
4612
+ routeSessionKey: route.sessionKey,
4613
+ dispatchSessionKey: sessionKey,
4209
4614
  parentSessionKey: threadKeys.parentSessionKey
4210
4615
  },
4211
4616
  reply: {
4212
- to: slackTo,
4617
+ to: replyRouteTarget,
4213
4618
  replyToId: threadContext.replyToId,
4214
- messageThreadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
4619
+ messageThreadId: boundMessageThreadId,
4215
4620
  nativeChannelId: message.channel
4216
4621
  },
4217
4622
  message: {
@@ -4244,7 +4649,7 @@ async function prepareSlackMessage(params) {
4244
4649
  groupSystemPrompt
4245
4650
  },
4246
4651
  extra: {
4247
- GroupSubject: isRoomish ? roomLabel : void 0,
4652
+ GroupSubject: groupSessionSubject,
4248
4653
  ChannelPromptContext: channelMetadata ? [channelMetadata] : void 0,
4249
4654
  ChannelStructuredContext: agentContextEntities.length > 0 ? [{
4250
4655
  label: "Slack active context",
@@ -4284,16 +4689,16 @@ async function prepareSlackMessage(params) {
4284
4689
  });
4285
4690
  const pinnedMainDmOwner = isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({
4286
4691
  dmScope: cfg.session?.dmScope,
4287
- allowFrom: ctx.allowFrom,
4692
+ allowFrom: allowFromLower,
4288
4693
  normalizeEntry: normalizeSlackAllowOwnerEntry
4289
4694
  }) : null;
4290
- const replyTarget = isDirectMessage ? `channel:${message.channel}` : ctxPayload.To ?? void 0;
4695
+ const replyTarget = `channel:${message.channel}`;
4291
4696
  if (!replyTarget) return null;
4292
4697
  if (preflightAudioTranscript) await sendSlackPreflightAudioTranscriptEcho({
4293
4698
  transcript: preflightAudioTranscript,
4294
4699
  cfg,
4295
4700
  accountId: account.accountId,
4296
- originatingTo: `channel:${message.channel}`,
4701
+ originatingTo: preflightChannelTarget,
4297
4702
  messageThreadId: threadContext.messageThreadId
4298
4703
  });
4299
4704
  if (shouldLogVerbose()) logVerbose(`slack inbound: account=${route.accountId} agent=${route.agentId} channel=${message.channel} message_ts=${message.ts ?? "unknown"} thread_ts=${effectiveMessageThreadId ?? "none"} from=${slackFrom} chat=${chatType} chars=${rawBody.length}`);
@@ -4306,7 +4711,7 @@ async function prepareSlackMessage(params) {
4306
4711
  account,
4307
4712
  message,
4308
4713
  ...opts.relayIdentity ? { relayIdentity: opts.relayIdentity } : {},
4309
- ...opts.eventScope ? { eventScope: opts.eventScope } : {},
4714
+ eventScope: opts.eventScope,
4310
4715
  route,
4311
4716
  channelConfig,
4312
4717
  replyTarget,
@@ -4314,13 +4719,13 @@ async function prepareSlackMessage(params) {
4314
4719
  turn: {
4315
4720
  storePath,
4316
4721
  record: {
4317
- updateLastRoute: isDirectMessage ? {
4722
+ updateLastRoute: isDirectMessage || opts.eventScope ? {
4318
4723
  sessionKey: updateLastRouteSessionKey,
4319
4724
  channel: "slack",
4320
- to: `user:${message.user}`,
4725
+ to: replyRouteTarget,
4321
4726
  accountId: route.accountId,
4322
4727
  threadId: effectiveMessageThreadId,
4323
- mainDmOwnerPin: updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && message.user ? {
4728
+ mainDmOwnerPin: isDirectMessage && updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && message.user ? {
4324
4729
  ownerRecipient: pinnedMainDmOwner,
4325
4730
  senderRecipient: normalizeLowercaseStringOrEmpty(message.user),
4326
4731
  onSkip: ({ ownerRecipient, senderRecipient }) => {