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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/{account-inspect-DEKpg9Ip.js → account-inspect-YJvFBbCR.js} +1 -1
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/{accounts-CHAeNZOX.js → accounts-BlzxuYle.js} +7 -3
  4. package/dist/{accounts.runtime-BHD4Ma2R.js → accounts.runtime-CbabBL8x.js} +1 -1
  5. package/dist/{action-runtime-DfuYoMjG.js → action-runtime-D6jg2CQd.js} +193 -125
  6. package/dist/action-runtime.runtime-JtoOK9l5.js +2 -0
  7. package/dist/{actions-D3NuJvva.js → actions-DFk0ANVB.js} +116 -235
  8. package/dist/actions.runtime-CYLsYRai.js +2 -0
  9. package/dist/api.js +17 -17
  10. package/dist/{approval-handler.runtime-BA05P0QT.js → approval-handler.runtime-BNvAoKdK.js} +53 -14
  11. package/dist/{channel-B9fVdW3s.js → channel-CA5w7qDF.js} +209 -110
  12. package/dist/channel-config-api.js +1 -1
  13. package/dist/channel-plugin-api.js +1 -1
  14. package/dist/{channel-type-DXyos1d2.js → channel-type-CZWv55Wu.js} +9 -12
  15. package/dist/{config-adapter-CxBQH8fq.js → channel.setup-CpyYsEXH.js} +83 -25
  16. package/dist/{config-schema-SgZT6nXV.js → config-schema-CN3XZRX5.js} +27 -17
  17. package/dist/conversation-binding-route-DFVLUdYF.js +115 -0
  18. package/dist/{directory-config-CnCIAktf.js → directory-config-BGYyuzWh.js} +3 -3
  19. package/dist/directory-contract-api.js +1 -1
  20. package/dist/{directory-live-DKunWh3A.js → directory-live-DjUJQLbq.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-DyfB_ZIE.js} +282 -183
  24. package/dist/index.js +4 -1
  25. package/dist/{message-tool-api-DkhFpMoy.js → message-tool-api-CqMr5Rmc.js} +14 -3
  26. package/dist/message-tool-api.js +1 -1
  27. package/dist/{monitor-CmKz0wtI.js → monitor-Chrecrb9.js} +3 -3
  28. package/dist/{outbound-adapter-BkuMp2Vs.js → outbound-adapter-DuRqAQXK.js} +35 -23
  29. package/dist/{pipeline.runtime-DgtmuYCP.js → pipeline.runtime-Bbfd5VFW.js} +1386 -929
  30. package/dist/{policy-PRysPn23.js → policy-BKXSxrIS.js} +18 -14
  31. package/dist/{probe-CgDsSops.js → probe-BVA65leh.js} +38 -18
  32. package/dist/{provider-DmRizMZt.js → provider-CUUiwsg2.js} +3454 -2601
  33. package/dist/{relay-source-Blxj8xb4.js → relay-source-mhpMMRG-.js} +11 -21
  34. package/dist/{replies-4s_Vq_z9.js → replies-SnWyXcsL.js} +84 -27
  35. package/dist/{resolve-channels-yEqmM-3I.js → resolve-channels-BSpqC8jU.js} +39 -12
  36. package/dist/{resolve-users-BOrt5K-5.js → resolve-users-Dvz9pDe0.js} +28 -5
  37. package/dist/{runtime-api-QMb-Q5Ko.js → runtime-api-DBi7xc_g.js} +3 -3
  38. package/dist/runtime-api.js +11 -11
  39. package/dist/{scopes-BJSrkAp6.js → scopes-BSk4GZvZ.js} +1 -1
  40. package/dist/security-contract-api.js +1 -1
  41. package/dist/{send-DsLeZpST.js → send-CtnOxKCc.js} +249 -163
  42. package/dist/{send.runtime-gUdK6V3h.js → send.runtime-kYctMHSd.js} +1 -1
  43. package/dist/setup-entry.js +1 -4
  44. package/dist/setup-plugin-api.js +1 -1
  45. package/dist/{setup-surface-BeNzrMrJ.js → setup-surface-BPFfp6So.js} +6 -6
  46. package/dist/{shared-DgIN3Dcr.js → shared-DJBaslN7.js} +45 -45
  47. package/dist/{block-text-DkHaoGem.js → slack-client-kind-BXyOOICI.js} +17 -2
  48. package/dist/{slash-dispatch.runtime-zPkyl7dB.js → slash-dispatch.runtime-Cf2FTrnf.js} +2 -2
  49. package/dist/{streaming-compat-3_X-21wW.js → streaming-compat-B-e0mqM0.js} +3 -8
  50. package/dist/{target-parsing-N6lHogYZ.js → target-parsing-BnMD2ZqZ.js} +40 -2
  51. package/dist/{targets-KmjP2E8s.js → targets-Cx5W_n3W.js} +1 -1
  52. package/node_modules/@slack/bolt/node_modules/@types/node/README.md +1 -1
  53. package/node_modules/@slack/bolt/node_modules/@types/node/crypto.d.ts +1 -1
  54. package/node_modules/@slack/bolt/node_modules/@types/node/package.json +2 -2
  55. package/node_modules/@slack/socket-mode/node_modules/@types/node/README.md +1 -1
  56. package/node_modules/@slack/socket-mode/node_modules/@types/node/crypto.d.ts +1 -1
  57. package/node_modules/@slack/socket-mode/node_modules/@types/node/package.json +2 -2
  58. package/node_modules/@slack/web-api/node_modules/@types/node/README.md +1 -1
  59. package/node_modules/@slack/web-api/node_modules/@types/node/crypto.d.ts +1 -1
  60. package/node_modules/@slack/web-api/node_modules/@types/node/package.json +2 -2
  61. package/node_modules/undici/lib/cache/memory-cache-store.js +54 -9
  62. package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
  63. package/node_modules/undici/lib/core/request.js +12 -1
  64. package/node_modules/undici/lib/dispatcher/client-h1.js +11 -2
  65. package/node_modules/undici/lib/handler/cache-handler.js +246 -64
  66. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +3 -3
  67. package/node_modules/undici/lib/handler/retry-handler.js +24 -0
  68. package/node_modules/undici/lib/interceptor/cache.js +159 -52
  69. package/node_modules/undici/lib/util/cache.js +324 -62
  70. package/node_modules/undici/lib/util/date.js +23 -6
  71. package/node_modules/undici/lib/web/cookies/util.js +79 -9
  72. package/node_modules/undici/package.json +1 -1
  73. package/openclaw.plugin.json +280 -33
  74. package/package.json +9 -11
  75. package/skills/slack/SKILL.md +10 -68
  76. package/dist/account-reply-mode-DrUXogvO.js +0 -14
  77. package/dist/action-runtime.runtime-DQErZYpz.js +0 -2
  78. package/dist/actions.runtime-BIRU_G4M.js +0 -2
  79. package/dist/channel.setup-CLP5oTvh.js +0 -56
  80. package/dist/preflight-audio.runtime-D5Bgags0.js +0 -3
  81. package/dist/slash-plugin-commands.runtime-Dosfcx3X.js +0 -2
  82. package/node_modules/p-map/index.d.ts +0 -155
  83. package/node_modules/p-map/index.js +0 -285
  84. package/node_modules/p-map/license +0 -9
  85. package/node_modules/p-map/package.json +0 -57
  86. package/node_modules/p-map/readme.md +0 -190
  87. package/dist/{security-audit-DR097212.js → security-audit-B_yKr7MW.js} +1 -1
@@ -1,41 +1,44 @@
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";
1
+ import { n as resolveSlackConversationBindingRoute, r as resolveSlackReplyToMode, t as normalizeSlackRouteBindingConfig } from "./conversation-binding-route-DFVLUdYF.js";
2
+ import { n as formatSlackTarget } from "./target-parsing-BnMD2ZqZ.js";
3
+ import { B as SLACK_EDIT_TEXT_MAX_BYTES, F as normalizeSlackOutboundText, I as buildSlackBlocksFallbackText, J as truncateSlackText, O as hasSlackNativeDataBlock, S as resolveSlackReplyRenderPlan, U as SLACK_TEXT_LIMIT, Y as truncateSlackTextByUtf8Bytes, a as qualifySlackConversationId, ct as escapeSlackMrkdwn, d as normalizeSlackAllowOwnerEntry, et as SLACK_SESSION_LINK_ACTION_ID, o as qualifySlackRoutePeerId, p as resolveSlackAllowListMatch, q as countSlackTextUtf8Bytes, s as resolveSlackEnterpriseMainDmSessionKey, u as normalizeAllowListLower } from "./group-policy-DyfB_ZIE.js";
4
+ import { r as formatSlackError } from "./probe-BVA65leh.js";
5
+ import { n as resolveSlackStreamingMode, t as resolveSlackNativeStreaming } from "./streaming-compat-B-e0mqM0.js";
6
+ import { b as formatSlackFileReferenceList, i as editSlackRenderedMessage, p as removeSlackReaction, r as editSlackMessage, t as deleteSlackMessage, u as reactSlackMessage, v as logVerbose$1, x as buildSlackEditTextPayload, y as formatSlackFileReference } from "./actions-DFk0ANVB.js";
7
+ import { a as hasSlackTableBlock, c as resolveSlackMessageText$1, o as isSlackUnfurlAttachment, s as resolveSlackBlocksText } from "./slack-client-kind-BXyOOICI.js";
8
+ import { c as hasSlackThreadFailureNotice, d as recordSlackThreadFailureNotice, f as recordSlackThreadParticipation, l as hasSlackThreadParticipation, m as buildSlackNativeDataDeliveryPlan, o as clearSlackThreadFailureNotice, r as sendMessageSlack, u as hasSlackThreadParticipationWithPersistence } from "./send-CtnOxKCc.js";
9
+ import { S as updateLastRoute, _ as parseSlackAssistantThreadMetadata, b as resolveChannelResetConfig, c as resolveConversationLabel$1, d as resolveSlackEffectiveAllowFrom, g as buildSlackAssistantThreadMetadata, h as resolveSlackChatType, l as authorizeSlackBotRoomMessage, m as normalizeSlackChannelType, o as authorizeSlackDirectMessage, r as resolveSlackRoomContextHints, s as trackSlackDraftMessage, u as resolveSlackCommandIngress, v as readSessionUpdatedAt, w as stripSlackMentionsForCommandDetection, x as resolveStorePath, y as resolveChannelContextVisibilityMode } from "./provider-CUUiwsg2.js";
10
+ import { n as resolveSlackChannelConfig } from "./policy-BKXSxrIS.js";
11
+ import { a as resolveDeliveredSlackReplyThreadTs, i as readSlackReplyBlocks, n as deliverReplies, o as resolveSlackThreadTs, s as emitSlackMessageSentHooks, t as createSlackReplyDeliveryPlan } from "./replies-SnWyXcsL.js";
13
12
  import { asOptionalRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
14
13
  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";
14
+ import { buildChannelProgressDraftLine, buildChannelProgressDraftLineForEntry, createChannelMessageReplyPipeline, createChannelProgressDraftCompositor, createChannelProgressWorkCounter, createFinalizableDraftStreamControlsForState, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, formatChannelProgressDraftText, formatPlanChecklistLines, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelProgressDraftConfig, resolveChannelProgressDraftMaxLineChars, resolveChannelStreamingBlockEnabled, resolveChannelStreamingPreviewToolProgress, resolveChannelStreamingSuppressDefaultToolProgressMessages } from "openclaw/plugin-sdk/channel-outbound";
16
15
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
17
16
  import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
18
17
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
19
18
  import { createHash } from "node:crypto";
19
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
20
+ import { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversation-runtime";
20
21
  import fs from "node:fs/promises";
22
+ import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
21
23
  import { formatErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
22
- import { asDateTimestampMs, asFiniteNumberInRange, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
23
- import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
24
+ import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, isReplyPayloadNonTerminalToolErrorWarning, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
24
25
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist, shouldIncludeSupplementalContext } from "openclaw/plugin-sdk/security-runtime";
25
- import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
26
+ import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
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 { asDateTimestampMs, asFiniteNumberInRange, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
32
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
33
+ import { buildChannelInboundEventContext, buildMentionRegexes, classifyChannelInboundEvent, dispatchChannelInboundTurn, formatInboundEnvelope, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, implicitMentionKindWhen, logInboundDrop, matchesMentionWithExplicit, readAgentRunTerminalOutcome, recordDroppedChannelInboundHistory, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundReplyDispatchCounts, resolveInboundSupplementalSenderAllowed, resolveUnmentionedGroupInboundPolicy, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
34
+ import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
35
+ import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
36
+ import { buildControlUiSessionPath } from "openclaw/plugin-sdk/session-discussion";
33
37
  import { DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
34
38
  import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
35
39
  import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
36
40
  import { shouldHandleTextCommands } from "openclaw/plugin-sdk/command-surface";
37
- import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
38
- import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
41
+ import { createChannelPreflightAudio, formatAudioTranscriptForAgent } from "openclaw/plugin-sdk/media-understanding-runtime";
39
42
  //#region extensions/slack/src/streaming.ts
40
43
  /**
41
44
  * Thrown when Slack definitively rejects a stream flush/finalize while text
@@ -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;
@@ -272,11 +267,16 @@ function resolveExplicitSlackProgressTitle(entry) {
272
267
  const trimmed = label.trim();
273
268
  return trimmed && trimmed.toLowerCase() !== "auto" ? trimmed : void 0;
274
269
  }
270
+ function resolveSlackProgressStyle(entry) {
271
+ return entry?.streaming?.progress?.style === "compact" ? "compact" : "card";
272
+ }
275
273
  function resolveSlackNativeProgressTaskCards(entry) {
274
+ if (resolveSlackProgressStyle(entry) === "compact") return false;
276
275
  const streaming = entry?.streaming;
277
- if (!streaming || typeof streaming !== "object" || Array.isArray(streaming)) return false;
276
+ if (!streaming || typeof streaming !== "object" || Array.isArray(streaming)) return true;
278
277
  const progressConfig = streaming.progress;
279
- return Boolean(progressConfig) && typeof progressConfig === "object" && !Array.isArray(progressConfig) && progressConfig.nativeTaskCards === true;
278
+ if (!progressConfig || typeof progressConfig !== "object" || Array.isArray(progressConfig)) return true;
279
+ return progressConfig.nativeTaskCards !== false;
280
280
  }
281
281
  function resolveSlackStreamingThreadHint(params) {
282
282
  return resolveSlackThreadTs({
@@ -386,21 +386,27 @@ function createSlackDraftStream(params) {
386
386
  const remove = params.remove ?? deleteSlackMessage;
387
387
  let streamMessageId;
388
388
  let streamChannelId;
389
+ let untrackConversationBoundary;
390
+ let lastVisibleUpdate;
389
391
  let lastSentKey = "";
390
- let pendingUpdate;
391
- let stopped = false;
392
+ const detachedMessages = [];
393
+ const finalizedMessageIds = /* @__PURE__ */ new Set();
394
+ const streamState = {
395
+ stopped: false,
396
+ final: false
397
+ };
392
398
  const normalizeUpdate = (update) => typeof update === "string" ? { text: update } : update;
393
- const sendOrEditStreamMessage = async (text) => {
394
- if (stopped) return;
395
- const trimmed = text.trimEnd();
399
+ const sendOrEditStreamMessage = async (pending) => {
400
+ if (streamState.stopped) return;
401
+ const update = normalizeUpdate(pending);
402
+ const trimmed = update.text.trimEnd();
396
403
  if (!trimmed) return;
397
404
  if (trimmed.length > maxChars) {
398
- stopped = true;
405
+ streamState.stopped = true;
399
406
  params.warn?.(`slack stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
400
407
  return;
401
408
  }
402
- const update = normalizeUpdate(pendingUpdate ?? text);
403
- const blocks = update.text === text ? update.blocks : void 0;
409
+ const blocks = update.blocks;
404
410
  const sentKey = `${trimmed}\n${blocks ? JSON.stringify(blocks) : ""}`;
405
411
  if (sentKey === lastSentKey) return;
406
412
  lastSentKey = sentKey;
@@ -413,56 +419,73 @@ function createSlackDraftStream(params) {
413
419
  ...params.eventScope ? { client: params.eventScope.client } : {},
414
420
  ...blocks ? { blocks } : {}
415
421
  });
422
+ lastVisibleUpdate = {
423
+ text: trimmed,
424
+ ...blocks ? { blocks } : {}
425
+ };
416
426
  return;
417
427
  }
428
+ const threadTs = params.resolveThreadTs?.();
429
+ const pendingBoundary = params.conversationChannelId ? trackSlackDraftMessage({
430
+ accountId: params.accountId,
431
+ teamId: params.eventScope?.teamId,
432
+ channelId: params.conversationChannelId,
433
+ threadTs,
434
+ onInterveningMessage: forceNewMessage
435
+ }) : void 0;
436
+ untrackConversationBoundary = pendingBoundary?.stop;
418
437
  const sent = await send(params.target, trimmed, {
419
438
  cfg: params.cfg,
420
439
  token: params.token,
421
440
  accountId: params.accountId,
422
- threadTs: params.resolveThreadTs?.(),
441
+ threadTs,
423
442
  identity: params.identity,
424
- ...params.eventScope ? {
425
- client: params.eventScope.client,
426
- enterpriseEventScope: params.eventScope
427
- } : {},
443
+ eventScope: params.eventScope,
428
444
  ...params.metadata ? { metadata: params.metadata } : {},
429
445
  ...blocks ? { blocks } : {}
430
446
  });
431
447
  streamChannelId = sent.channelId || streamChannelId;
432
448
  streamMessageId = sent.messageId || streamMessageId;
433
449
  if (!streamChannelId || !streamMessageId) {
434
- stopped = true;
450
+ stopTrackingConversationBoundary();
451
+ streamState.stopped = true;
435
452
  params.warn?.("slack stream preview stopped (missing identifiers from sendMessage)");
436
453
  return;
437
454
  }
438
- params.onMessageSent?.();
455
+ lastVisibleUpdate = {
456
+ text: trimmed,
457
+ ...blocks ? { blocks } : {}
458
+ };
459
+ if (pendingBoundary && params.conversationChannelId === streamChannelId) pendingBoundary.setMessageTs(streamMessageId);
460
+ else {
461
+ stopTrackingConversationBoundary();
462
+ untrackConversationBoundary = trackSlackDraftMessage({
463
+ accountId: params.accountId,
464
+ teamId: params.eventScope?.teamId,
465
+ channelId: streamChannelId,
466
+ threadTs,
467
+ messageTs: streamMessageId,
468
+ onInterveningMessage: forceNewMessage
469
+ }).stop;
470
+ }
439
471
  } catch (err) {
440
- stopped = true;
472
+ stopTrackingConversationBoundary();
473
+ streamState.stopped = true;
441
474
  params.warn?.(`slack stream preview failed: ${formatSlackError(err)}`);
442
475
  }
443
476
  };
444
- const loop = createDraftStreamLoop({
477
+ const { loop, update, discardPending, seal } = createFinalizableDraftStreamControlsForState({
445
478
  throttleMs,
446
- isStopped: () => stopped,
447
- sendOrEditStreamMessage
479
+ state: streamState,
480
+ sendOrEditStreamMessage,
481
+ emptyValue: "",
482
+ isEmpty: (value) => !normalizeUpdate(value).text.trim()
448
483
  });
449
- const stop = () => {
450
- stopped = true;
451
- loop.stop();
484
+ const stopTrackingConversationBoundary = () => {
485
+ untrackConversationBoundary?.();
486
+ untrackConversationBoundary = void 0;
452
487
  };
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;
488
+ const removeMessage = async (channelId, messageId) => {
466
489
  try {
467
490
  await remove(channelId, messageId, {
468
491
  token: params.token,
@@ -473,31 +496,131 @@ function createSlackDraftStream(params) {
473
496
  params.warn?.(`slack stream preview cleanup failed: ${formatSlackError(err)}`);
474
497
  }
475
498
  };
499
+ const clear = async () => {
500
+ stopTrackingConversationBoundary();
501
+ await discardPending();
502
+ const channelId = streamChannelId;
503
+ const messageId = streamMessageId;
504
+ streamChannelId = void 0;
505
+ streamMessageId = void 0;
506
+ lastVisibleUpdate = void 0;
507
+ lastSentKey = "";
508
+ if (!channelId || !messageId) return;
509
+ await removeMessage(channelId, messageId);
510
+ };
476
511
  const forceNewMessage = () => {
512
+ stopTrackingConversationBoundary();
513
+ streamState.stopped = false;
514
+ streamState.final = false;
515
+ if (streamChannelId && streamMessageId && !finalizedMessageIds.has(streamMessageId)) detachedMessages.push({
516
+ channelId: streamChannelId,
517
+ messageId: streamMessageId
518
+ });
477
519
  streamMessageId = void 0;
478
520
  streamChannelId = void 0;
521
+ lastVisibleUpdate = void 0;
479
522
  lastSentKey = "";
480
- pendingUpdate = void 0;
481
523
  loop.resetPending();
482
524
  };
525
+ const dropDetachedMessages = async () => {
526
+ let message;
527
+ while ((message = detachedMessages.shift()) !== void 0) {
528
+ const { channelId, messageId } = message;
529
+ await removeMessage(channelId, messageId);
530
+ }
531
+ };
532
+ const discardPendingAndStopTracking = async () => {
533
+ stopTrackingConversationBoundary();
534
+ await discardPending();
535
+ };
536
+ const finalizeMessage = async (messageId, editFinal) => {
537
+ const channelId = streamChannelId;
538
+ const previousUpdate = lastVisibleUpdate;
539
+ if (!channelId || streamMessageId !== messageId || !previousUpdate) return false;
540
+ await editFinal();
541
+ if (streamChannelId === channelId && streamMessageId === messageId) {
542
+ finalizedMessageIds.add(messageId);
543
+ stopTrackingConversationBoundary();
544
+ return true;
545
+ }
546
+ try {
547
+ await edit(channelId, messageId, previousUpdate.text, {
548
+ cfg: params.cfg,
549
+ token: params.token,
550
+ accountId: params.accountId,
551
+ ...params.eventScope ? { client: params.eventScope.client } : {},
552
+ ...previousUpdate.blocks ? { blocks: previousUpdate.blocks } : {}
553
+ });
554
+ } catch (err) {
555
+ params.warn?.(`slack stream preview restore failed: ${formatSlackError(err)}`);
556
+ }
557
+ return false;
558
+ };
483
559
  params.log?.(`slack stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
484
560
  return {
485
- update: (update) => {
486
- const normalized = normalizeUpdate(update);
487
- pendingUpdate = update;
488
- loop.update(normalized.text);
489
- },
561
+ update,
490
562
  flush: loop.flush,
491
563
  clear,
492
- discardPending,
493
- seal: discardPending,
494
- stop,
564
+ discardPending: discardPendingAndStopTracking,
565
+ seal,
495
566
  forceNewMessage,
567
+ dropDetachedMessages,
568
+ finalizeMessage,
496
569
  messageId: () => streamMessageId,
497
570
  channelId: () => streamChannelId
498
571
  };
499
572
  }
500
573
  //#endregion
574
+ //#region extensions/slack/src/stream-mode.ts
575
+ function resolveSlackStreamingConfig(params) {
576
+ return {
577
+ mode: resolveSlackStreamingMode(params),
578
+ nativeStreaming: resolveSlackNativeStreaming(params)
579
+ };
580
+ }
581
+ function applyAppendOnlyStreamUpdate(params) {
582
+ const incoming = params.incoming.trimEnd();
583
+ if (!incoming) return {
584
+ rendered: params.rendered,
585
+ source: params.source,
586
+ changed: false
587
+ };
588
+ if (!params.rendered) return {
589
+ rendered: incoming,
590
+ source: incoming,
591
+ changed: true
592
+ };
593
+ if (incoming === params.source) return {
594
+ rendered: params.rendered,
595
+ source: params.source,
596
+ changed: false
597
+ };
598
+ if (incoming.startsWith(params.rendered)) return {
599
+ rendered: incoming,
600
+ source: incoming,
601
+ changed: incoming !== params.rendered
602
+ };
603
+ if (incoming.startsWith(params.source)) {
604
+ const delta = incoming.slice(params.source.length);
605
+ return {
606
+ rendered: `${params.rendered}${delta}`,
607
+ source: incoming,
608
+ changed: delta.length > 0
609
+ };
610
+ }
611
+ if (params.source.startsWith(incoming)) return {
612
+ rendered: params.rendered,
613
+ source: params.source,
614
+ changed: false
615
+ };
616
+ const separator = params.separator ?? (params.rendered.endsWith("\n") ? "" : "\n");
617
+ return {
618
+ rendered: `${params.rendered}${separator}${incoming}`,
619
+ source: incoming,
620
+ changed: true
621
+ };
622
+ }
623
+ //#endregion
501
624
  //#region extensions/slack/src/progress-blocks.ts
502
625
  const SLACK_PROGRESS_FIELD_MAX = 1800;
503
626
  const DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS = 120;
@@ -505,6 +628,14 @@ const DEFAULT_SLACK_PROGRESS_TASK_DETAIL_MAX_CHARS = 48;
505
628
  const SLACK_PROGRESS_CHUNK_TEXT_MAX = 256;
506
629
  const SLACK_PROGRESS_TASK_TITLE_MAX = 120;
507
630
  const SLACK_PROGRESS_PLAN_FALLBACK_TITLE = "Thinking";
631
+ const SLACK_PROGRESS_LINE_DELTA_RE = /(?:^|\s)\+(\d+)\s+[−-](\d+)(?=\s|$)/u;
632
+ function buildSessionSources(url) {
633
+ return [{
634
+ type: "url_source",
635
+ url,
636
+ text: "Open in OpenClaw"
637
+ }];
638
+ }
508
639
  function field(text) {
509
640
  return {
510
641
  type: "mrkdwn",
@@ -535,18 +666,31 @@ function lineDetailParts(line) {
535
666
  function legacyLineTitle(line) {
536
667
  return `${line.icon ?? "•"} *${escapeSlackMrkdwn(line.label)}*`;
537
668
  }
669
+ function isAuthoredProgressLine(line) {
670
+ return line.id === "reasoning" || line.id?.startsWith("commentary:") === true;
671
+ }
538
672
  function legacyLineDetail(line, maxChars) {
539
673
  const detail = lineDetailParts(line).join(" · ");
540
- return detail ? escapeSlackMrkdwn(compactDetail(detail, maxChars)) : "—";
674
+ if (detail) return escapeSlackMrkdwn(compactDetail(detail, maxChars));
675
+ if (isAuthoredProgressLine(line)) return normalizeSlackOutboundText(compactDetail(line.text.replace(/^(?:🧠|💬)\s+/u, ""), maxChars));
676
+ return "—";
541
677
  }
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();
678
+ function lineTaskTitle(line) {
679
+ const label = (line.kind === "command-output" ? line.toolName : void 0) || line.label.replace(/\s+/g, " ").trim() || line.toolName || line.kind || "Update";
545
680
  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);
681
+ if (fallback && fallback !== label) return compactTitle(lineDetailParts(line).length > 0 || line.status ? label : fallback);
548
682
  return compactTitle(label);
549
683
  }
684
+ function lineTaskDetails(line, maxLineChars) {
685
+ const detail = line.detail?.replace(SLACK_PROGRESS_LINE_DELTA_RE, "").replace(/\s+·\s*$/u, "").trim();
686
+ return detail && detail !== line.status?.trim() ? compactDetail(detail, maxLineChars) : void 0;
687
+ }
688
+ function lineTaskOutput(line) {
689
+ const match = line.detail ? SLACK_PROGRESS_LINE_DELTA_RE.exec(line.detail) : null;
690
+ if (match) return `+${match[1]} −${match[2]}`;
691
+ const status = line.status?.replace(/\s+/g, " ").trim();
692
+ return status && lineTaskStatus(line) === "error" ? status : void 0;
693
+ }
550
694
  function lineTaskStatus(line) {
551
695
  const normalized = line.status?.replace(/\s+/g, " ").trim().toLowerCase();
552
696
  if (!normalized) return "in_progress";
@@ -594,15 +738,20 @@ function buildPlanTasks(params) {
594
738
  contentIdOccurrences.set(id, occurrence);
595
739
  id = `${id}_${occurrence}`;
596
740
  }
597
- return {
741
+ const details = lineTaskDetails(line, maxLineChars);
742
+ const output = lineTaskOutput(line);
743
+ const task = {
598
744
  id,
599
- title: lineTaskTitle(line, maxLineChars),
745
+ title: lineTaskTitle(line),
600
746
  status: lineTaskStatus(line)
601
747
  };
748
+ if (details) task.details = details;
749
+ if (output) task.output = output;
750
+ return task;
602
751
  });
603
752
  }
604
753
  function resolvePlanTitle(params) {
605
- return compactChunkText(params.title?.trim() || params.label?.trim() || params.tasks.at(-1)?.title || SLACK_PROGRESS_PLAN_FALLBACK_TITLE);
754
+ 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
755
  }
607
756
  function buildSlackProgressStreamChunks(params) {
608
757
  const tasks = buildPlanTasks({
@@ -612,38 +761,91 @@ function buildSlackProgressStreamChunks(params) {
612
761
  });
613
762
  if (tasks.length === 0) {
614
763
  const title = params.title?.trim() || params.label?.trim();
615
- return title ? [{
764
+ if (!title) return;
765
+ if (!params.sessionUrl && !params.diffStat) return [{
616
766
  type: "plan_update",
617
767
  title: compactChunkText(title)
618
- }] : void 0;
768
+ }];
769
+ return [{
770
+ type: "plan_update",
771
+ title: compactChunkText(title)
772
+ }, {
773
+ type: "task_update",
774
+ id: "openclaw_summary",
775
+ title: "Completed",
776
+ status: "complete",
777
+ ...formatTaskDiffOutput(params.diffStat) ? { output: formatTaskDiffOutput(params.diffStat) } : {},
778
+ ...params.sessionUrl ? { sources: buildSessionSources(params.sessionUrl) } : {}
779
+ }];
619
780
  }
781
+ const title = resolvePlanTitle({
782
+ label: params.label,
783
+ title: params.title,
784
+ tasks
785
+ });
786
+ const finalTaskIndex = tasks.length - 1;
787
+ const diffOutput = formatTaskDiffOutput(params.diffStat);
788
+ const taskChunks = tasks.map((task, index) => {
789
+ const chunk = {
790
+ type: "task_update",
791
+ id: task.id,
792
+ title: task.title,
793
+ status: task.status === "in_progress" ? params.finalInProgressStatus ?? (params.completeInProgress ? "complete" : task.status) : task.status
794
+ };
795
+ if (task.details) chunk.details = task.details;
796
+ if (task.output) chunk.output = task.output;
797
+ if (index === finalTaskIndex && diffOutput) chunk.output = [task.output, diffOutput].filter(Boolean).join(" · ");
798
+ if (index === finalTaskIndex && params.sessionUrl) chunk.sources = buildSessionSources(params.sessionUrl);
799
+ return chunk;
800
+ });
620
801
  return [{
621
802
  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();
803
+ title
804
+ }, ...taskChunks];
805
+ }
806
+ function formatDiffStat(diffStat) {
807
+ if (!diffStat || diffStat.files === 0 && diffStat.added === 0 && diffStat.removed === 0) return;
808
+ return [
809
+ `📝 ${diffStat.files} files`,
810
+ ...diffStat.added > 0 ? [`+${diffStat.added}`] : [],
811
+ ...diffStat.removed > 0 ? [`−${diffStat.removed}`] : []
812
+ ].join(" ");
813
+ }
814
+ function formatTaskDiffOutput(diffStat) {
815
+ return diffStat && (diffStat.added > 0 || diffStat.removed > 0) ? `+${diffStat.added} −${diffStat.removed}` : void 0;
816
+ }
817
+ function buildActivityText(lines, maxLineChars) {
818
+ const rendered = [];
819
+ let length = 0;
820
+ for (const line of lines.slice(-50).toReversed()) {
821
+ const row = `${legacyLineTitle(line)} — ${legacyLineDetail(line, maxLineChars)}`;
822
+ const nextLength = length + row.length + (rendered.length > 0 ? 1 : 0);
823
+ if (nextLength > SLACK_PROGRESS_FIELD_MAX) break;
824
+ rendered.push(row);
825
+ length = nextLength;
826
+ }
827
+ return rendered.toReversed().join("\n");
828
+ }
829
+ function buildSlackProgressCardBlocks(params) {
636
830
  const maxLineChars = resolveMaxLineChars(params.maxLineChars, DEFAULT_SLACK_PROGRESS_DETAIL_MAX_CHARS);
637
831
  const planLines = formatPlanChecklistLines(params.plan ?? [], {
638
832
  maxLines: 50,
639
833
  maxLineChars
640
834
  });
641
835
  const narration = params.narration?.replace(/\s+/g, " ").trim();
642
- const headBlocks = [
643
- ...label ? [{
836
+ const activityText = buildActivityText(params.lines, maxLineChars);
837
+ const diffStat = formatDiffStat(params.diffStat);
838
+ const workingFooter = [
839
+ ...params.toolCalls && params.toolCalls > 0 ? [`🛠️ ${params.toolCalls} tools`] : [],
840
+ ...diffStat ? [diffStat] : [],
841
+ ...params.elapsedSeconds && params.elapsedSeconds > 0 ? [`⏱ ${params.elapsedSeconds}s`] : []
842
+ ].join(" · ");
843
+ const footer = params.state === "working" ? workingFooter : diffStat;
844
+ return [
845
+ {
644
846
  type: "section",
645
- text: field(`*${escapeSlackMrkdwn(label)}*`)
646
- }] : [],
847
+ text: field(`${params.state === "working" ? "🔄" : params.state === "success" ? "✅" : "❌"} *${escapeSlackMrkdwn(params.title.trim() || "Working")}*`)
848
+ },
647
849
  ...narration ? [{
648
850
  type: "section",
649
851
  text: field(`_${escapeSlackMrkdwn(narration)}_`)
@@ -651,52 +853,131 @@ function buildSlackProgressDraftBlocks(params) {
651
853
  ...planLines.length > 0 ? [{
652
854
  type: "section",
653
855
  text: field(planLines.map((line) => escapeSlackMrkdwn(line)).join("\n"))
856
+ }] : [],
857
+ ...activityText ? [{
858
+ type: "section",
859
+ text: field(activityText)
860
+ }] : [],
861
+ ...footer ? [{
862
+ type: "context",
863
+ elements: [field(footer)]
864
+ }] : [],
865
+ ...params.state !== "working" && params.sessionUrl ? [{
866
+ type: "actions",
867
+ elements: [{
868
+ type: "button",
869
+ action_id: SLACK_SESSION_LINK_ACTION_ID,
870
+ text: {
871
+ type: "plain_text",
872
+ text: "Open in OpenClaw"
873
+ },
874
+ url: params.sessionUrl
875
+ }]
654
876
  }] : []
655
877
  ].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;
878
+ }
879
+ const EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT = { tasks: /* @__PURE__ */ new Map() };
880
+ const SLACK_TASK_FIELD_SEPARATOR = " · ";
881
+ function resolveTaskFieldDelta(previous, incoming) {
882
+ if (!incoming) return {
883
+ field: previous,
884
+ delta: void 0
885
+ };
886
+ if (previous?.rendered.includes(incoming)) return {
887
+ field: {
888
+ rendered: previous.rendered,
889
+ source: incoming
890
+ },
891
+ delta: void 0
892
+ };
893
+ const next = applyAppendOnlyStreamUpdate({
894
+ incoming,
895
+ rendered: previous?.rendered ?? "",
896
+ source: previous?.source ?? "",
897
+ separator: SLACK_TASK_FIELD_SEPARATOR
898
+ });
899
+ const delta = next.changed ? next.rendered.slice(previous?.rendered.length ?? 0) : void 0;
900
+ return {
901
+ field: {
902
+ rendered: next.rendered,
903
+ source: next.source
904
+ },
905
+ delta
906
+ };
662
907
  }
663
908
  /**
664
- * Slack native streams key task rows by persistent id with no removal chunk.
665
- * When the task source switches representation (tool lines <-> typed plan) or
666
- * a snapshot drops ids, previously emitted non-terminal rows must receive a
667
- * final update or they linger in_progress forever.
909
+ * Turns a full task snapshot into the delta Slack must receive. Native streams
910
+ * key rows by persistent id with no removal chunk: unchanged rows are omitted,
911
+ * changed rows carry only their unsent field text, and rows that dropped out
912
+ * (plan shrinks, tool-line <-> plan source switches) get a final complete
913
+ * update or they linger in_progress forever.
668
914
  */
669
915
  function reconcileSlackNativeTaskChunks(params) {
670
916
  const nextTasks = /* @__PURE__ */ new Map();
671
- for (const chunk of params.chunks ?? []) if (chunk.type === "task_update") nextTasks.set(chunk.id, {
672
- title: chunk.title,
673
- status: chunk.status
674
- });
675
- const terminalized = [...params.previousTasks].filter(([id, task]) => !nextTasks.has(id) && task.status !== "complete" && task.status !== "error").map(([id, task]) => {
676
- const entry = {
677
- title: task.title,
678
- status: "complete"
917
+ let planTitle = params.previous.planTitle;
918
+ const emitted = [];
919
+ for (const chunk of params.chunks ?? []) {
920
+ if (chunk.type === "plan_update") {
921
+ if (chunk.title !== planTitle) {
922
+ planTitle = chunk.title;
923
+ emitted.push(chunk);
924
+ }
925
+ continue;
926
+ }
927
+ if (chunk.type !== "task_update") {
928
+ emitted.push(chunk);
929
+ continue;
930
+ }
931
+ const previousRow = params.previous.tasks.get(chunk.id);
932
+ const status = chunk.status;
933
+ const details = resolveTaskFieldDelta(previousRow?.details, chunk.details);
934
+ const output = resolveTaskFieldDelta(previousRow?.output, chunk.output);
935
+ const sourcesChanged = Boolean(chunk.sources) && !previousRow?.sourcesSent;
936
+ const row = {
937
+ title: chunk.title,
938
+ status
679
939
  };
680
- nextTasks.set(id, entry);
681
- return {
940
+ if (details.field) row.details = details.field;
941
+ if (output.field) row.output = output.field;
942
+ if (sourcesChanged || previousRow?.sourcesSent) row.sourcesSent = true;
943
+ nextTasks.set(chunk.id, row);
944
+ if (!(!previousRow || previousRow.title !== chunk.title || previousRow.status !== status || Boolean(details.delta) || Boolean(output.delta) || sourcesChanged)) continue;
945
+ const update = {
946
+ type: "task_update",
947
+ id: chunk.id,
948
+ title: chunk.title,
949
+ status
950
+ };
951
+ if (details.delta) update.details = details.delta;
952
+ if (output.delta) update.output = output.delta;
953
+ if (sourcesChanged) update.sources = chunk.sources;
954
+ emitted.push(update);
955
+ }
956
+ for (const [id, row] of params.previous.tasks) {
957
+ if (nextTasks.has(id)) continue;
958
+ if (row.status === "complete" || row.status === "error") {
959
+ nextTasks.set(id, row);
960
+ continue;
961
+ }
962
+ nextTasks.set(id, {
963
+ ...row,
964
+ status: "complete"
965
+ });
966
+ emitted.push({
682
967
  type: "task_update",
683
968
  id,
684
- title: task.title,
969
+ title: row.title,
685
970
  status: "complete"
686
- };
687
- });
688
- for (const [id, task] of params.previousTasks) if (!nextTasks.has(id)) nextTasks.set(id, task);
971
+ });
972
+ }
689
973
  return {
690
- chunks: params.chunks?.length ? [...params.chunks, ...terminalized] : terminalized.length ? terminalized : params.chunks,
691
- tasks: nextTasks
974
+ chunks: emitted.length > 0 ? emitted : void 0,
975
+ snapshot: {
976
+ ...planTitle ? { planTitle } : {},
977
+ tasks: nextTasks
978
+ }
692
979
  };
693
980
  }
694
- function buildSlackProgressStreamStartChunks(params) {
695
- return buildSlackProgressStreamChunks(params);
696
- }
697
- function buildSlackProgressStreamUpdateChunks(params) {
698
- return buildSlackProgressStreamChunks(params);
699
- }
700
981
  function buildSlackProgressStreamCompletionChunks(params) {
701
982
  return buildSlackProgressStreamChunks({
702
983
  ...params,
@@ -704,48 +985,48 @@ function buildSlackProgressStreamCompletionChunks(params) {
704
985
  });
705
986
  }
706
987
  //#endregion
707
- //#region extensions/slack/src/stream-mode.ts
708
- function resolveSlackStreamingConfig(params) {
709
- const mode = resolveSlackStreamingMode(params);
710
- return {
711
- mode,
712
- nativeStreaming: resolveSlackNativeStreaming(params),
713
- draftMode: mapStreamingModeToSlackLegacyDraftStreamMode(mode)
714
- };
988
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-render.ts
989
+ function resolveStructuredProgressLines(lines) {
990
+ return lines.map((line) => {
991
+ if (typeof line !== "string") return line;
992
+ const reasoning = line.startsWith("🧠 ");
993
+ const text = line.replace(/^(?:🧠|💬)\s+/u, "").replace(/^_(.*)_$/su, "$1").trim();
994
+ return {
995
+ ...reasoning ? { id: "reasoning" } : {},
996
+ kind: "item",
997
+ text,
998
+ label: reasoning ? "Reasoning" : "Update",
999
+ prefix: false
1000
+ };
1001
+ });
715
1002
  }
716
- function applyAppendOnlyStreamUpdate(params) {
717
- const incoming = params.incoming.trimEnd();
718
- if (!incoming) return {
719
- rendered: params.rendered,
720
- source: params.source,
721
- changed: false
722
- };
723
- if (!params.rendered) return {
724
- rendered: incoming,
725
- source: incoming,
726
- changed: true
727
- };
728
- if (incoming === params.source) return {
729
- rendered: params.rendered,
730
- source: params.source,
731
- changed: false
732
- };
733
- if (incoming.startsWith(params.source) || incoming.startsWith(params.rendered)) return {
734
- rendered: incoming,
735
- source: incoming,
736
- changed: incoming !== params.rendered
737
- };
738
- if (params.source.startsWith(incoming)) return {
739
- rendered: params.rendered,
740
- source: params.source,
741
- changed: false
742
- };
743
- const separator = params.rendered.endsWith("\n") ? "" : "\n";
744
- return {
745
- rendered: `${params.rendered}${separator}${incoming}`,
746
- source: incoming,
747
- changed: true
748
- };
1003
+ function resolveNativeProgressPlan(snapshot) {
1004
+ return snapshot.plan?.length ? snapshot.plan : void 0;
1005
+ }
1006
+ function resolveNativeProgressLines(snapshot) {
1007
+ const lines = resolveStructuredProgressLines(snapshot.lines).filter((line) => line.id !== "reasoning" && line.id?.startsWith("commentary:") !== true);
1008
+ if (snapshot.plan?.length || !snapshot.planExplanation) return lines;
1009
+ const explanationLine = buildChannelProgressDraftLine({
1010
+ event: "plan",
1011
+ phase: "update",
1012
+ explanation: snapshot.planExplanation
1013
+ });
1014
+ return explanationLine ? [...lines, explanationLine] : lines;
1015
+ }
1016
+ function resolveNativeProgressNarration(snapshot) {
1017
+ 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);
1018
+ return paragraphs.length > 0 ? paragraphs.join("\n\n") : void 0;
1019
+ }
1020
+ function combineProgressHeadlineAndExplanation(headline, explanation) {
1021
+ return headline && explanation && headline !== explanation ? `${headline} — ${explanation}` : headline ?? explanation;
1022
+ }
1023
+ function buildNativeProgressChunks(params) {
1024
+ return buildSlackProgressStreamChunks({
1025
+ title: params.title,
1026
+ lines: resolveNativeProgressLines(params.snapshot),
1027
+ plan: resolveNativeProgressPlan(params.snapshot),
1028
+ maxLineChars: params.maxLineChars
1029
+ });
749
1030
  }
750
1031
  //#endregion
751
1032
  //#region extensions/slack/src/monitor/message-handler/preview-finalize.ts
@@ -795,8 +1076,9 @@ async function readSlackMessageAfterEditError(params) {
795
1076
  channel: params.channelId,
796
1077
  ts: params.threadTs,
797
1078
  latest: params.messageId,
1079
+ oldest: params.messageId,
798
1080
  inclusive: true,
799
- limit: 100
1081
+ limit: 1
800
1082
  })).messages ?? []).find((message) => message?.ts === params.messageId) ?? null;
801
1083
  const message = (await params.client.conversations.history({
802
1084
  token: params.token,
@@ -826,7 +1108,7 @@ async function didSlackPreviewEditApplyAfterError(params) {
826
1108
  }
827
1109
  async function finalizeSlackPreviewEdit(params) {
828
1110
  try {
829
- await editSlackMessage(params.channelId, params.messageId, params.text, {
1111
+ await editSlackRenderedMessage(params.channelId, params.messageId, params.text, {
830
1112
  token: params.token,
831
1113
  accountId: params.accountId,
832
1114
  client: params.client,
@@ -853,74 +1135,196 @@ async function finalizeSlackPreviewEdit(params) {
853
1135
  }
854
1136
  }
855
1137
  //#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
1138
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-card.ts
1139
+ function createSlackDraftProgressCardRuntime(params) {
1140
+ const { account, cfg, ctx, prepared, slackClient } = params.setup;
1141
+ let latestFallbackText = "";
1142
+ let finalStatus;
1143
+ const resolveSessionUrl = () => {
1144
+ if (cfg.gateway?.controlUi?.enabled === false) return;
1145
+ const publicOrigin = resolveGatewayPublicOrigin(cfg);
1146
+ if (!publicOrigin) return;
1147
+ const url = new URL(publicOrigin);
1148
+ const path = buildControlUiSessionPath({
1149
+ namespace: "chat",
1150
+ sessionKey: prepared.route.sessionKey,
1151
+ fallbackAgentId: prepared.route.agentId,
1152
+ basePath: cfg.gateway?.controlUi?.basePath
865
1153
  });
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
1154
+ if (!path) return;
1155
+ url.pathname = path;
1156
+ return url.toString();
1157
+ };
1158
+ const resolveText = (snapshot) => latestFallbackText || formatChannelProgressDraftText({
1159
+ entry: account.config,
1160
+ lines: [...snapshot.lines],
1161
+ seed: params.progressSeed,
1162
+ formatLine: formatSlackProgressDraftLine,
1163
+ narration: snapshot.statusHeadline,
1164
+ plan: snapshot.plan
898
1165
  });
899
- return explanationLine ? [...lines, explanationLine] : lines;
1166
+ const resolvePresentation = (snapshot, state) => {
1167
+ const title = params.explicitTitle ?? snapshot.statusHeadline ?? "Working";
1168
+ return buildSlackProgressCardBlocks({
1169
+ state,
1170
+ title,
1171
+ narration: params.explicitTitle ? combineProgressHeadlineAndExplanation(snapshot.statusHeadline, snapshot.planExplanation) : snapshot.planExplanation && snapshot.planExplanation !== title ? snapshot.planExplanation : void 0,
1172
+ plan: snapshot.plan,
1173
+ lines: resolveStructuredProgressLines(snapshot.lines),
1174
+ maxLineChars: params.maxLineChars,
1175
+ diffStat: snapshot.diffStat,
1176
+ ...state === "working" ? {
1177
+ toolCalls: params.progressWorkCounter.toolCalls,
1178
+ elapsedSeconds: params.progressWorkCounter.elapsedSeconds
1179
+ } : { sessionUrl: resolveSessionUrl() }
1180
+ });
1181
+ };
1182
+ const finalize = async (status, snapshot = params.getSnapshot(), fallbackText = resolveText(snapshot)) => {
1183
+ if (!params.draftStream || !params.enabled) return false;
1184
+ await params.draftStream.dropDetachedMessages();
1185
+ const terminalStatus = finalStatus === "error" || status === "error" ? "error" : "success";
1186
+ if (finalStatus === terminalStatus) return true;
1187
+ await params.draftStream.flush();
1188
+ const channelId = params.draftStream.channelId();
1189
+ const messageId = params.draftStream.messageId();
1190
+ if (!channelId || !messageId) return false;
1191
+ await params.draftStream.seal();
1192
+ try {
1193
+ const finalized = await params.draftStream.finalizeMessage(messageId, async () => {
1194
+ await finalizeSlackPreviewEdit({
1195
+ client: slackClient,
1196
+ token: ctx.botToken,
1197
+ accountId: account.accountId,
1198
+ channelId,
1199
+ messageId,
1200
+ text: fallbackText,
1201
+ blocks: resolvePresentation(snapshot, terminalStatus),
1202
+ threadTs: params.getThreadTs()
1203
+ });
1204
+ });
1205
+ if (finalized) finalStatus = terminalStatus;
1206
+ return finalized;
1207
+ } catch (err) {
1208
+ logVerbose(`slack: progress card final edit failed (${formatSlackError(err)})`);
1209
+ return false;
1210
+ }
1211
+ };
1212
+ return {
1213
+ resolveSessionUrl,
1214
+ resolveText,
1215
+ resolvePresentation,
1216
+ finalize,
1217
+ get hasTerminalized() {
1218
+ return finalStatus !== void 0;
1219
+ },
1220
+ setFallbackText(text) {
1221
+ latestFallbackText = text;
1222
+ },
1223
+ reset() {
1224
+ latestFallbackText = "";
1225
+ finalStatus = void 0;
1226
+ }
1227
+ };
900
1228
  }
901
- function combineProgressHeadlineAndExplanation(headline, explanation) {
902
- return headline && explanation && headline !== explanation ? `${headline} — ${explanation}` : headline ?? explanation;
1229
+ function formatSlackProgressDraftLine(line) {
1230
+ if (/^(?:🧠|💬)\s/u.test(line)) return line;
1231
+ const italicCommentary = /^_(.*)_$/su.exec(line);
1232
+ if (!italicCommentary) return escapeSlackMrkdwn(line);
1233
+ return `_${italicCommentary[1].split(/(`[^`\n]+`)/u).map((segment, index) => {
1234
+ if (index % 2 === 0) return escapeSlackMrkdwn(segment);
1235
+ return `\`${segment.slice(1, -1).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}\``;
1236
+ }).join("")}_`;
903
1237
  }
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
1238
+ //#endregion
1239
+ //#region extensions/slack/src/monitor/message-handler/dispatch-progress-native.ts
1240
+ function createSlackNativeProgressTransport(params) {
1241
+ const { ctx, message, replyPlan, slackClient, slackIdentity, slackStreamFallbackTeamId } = params.setup;
1242
+ const { delivery } = params;
1243
+ const markDelivered = (threadTs) => {
1244
+ if (!delivery.streamSession?.delivered) return false;
1245
+ delivery.observedReplyDelivery = true;
1246
+ if (threadTs) {
1247
+ delivery.usedReplyThreadTs ??= threadTs;
1248
+ delivery.rememberDeliveredThreadTs("block", threadTs);
1249
+ }
1250
+ return true;
1251
+ };
1252
+ const waitForStart = async () => {
1253
+ if (delivery.streamSession || !delivery.nativeProgressStreamStartPromise) return true;
1254
+ try {
1255
+ await delivery.nativeProgressStreamStartPromise;
1256
+ } catch {
1257
+ delivery.streamFailed = true;
1258
+ return false;
1259
+ }
1260
+ return !delivery.streamFailed;
1261
+ };
1262
+ const start = async (update) => {
1263
+ const streamThreadTs = replyPlan.nextThreadTs();
1264
+ if (!streamThreadTs) {
1265
+ logVerbose("slack-stream: no reply thread target for native progress stream start, falling back");
1266
+ delivery.streamFailed = true;
1267
+ return false;
1268
+ }
1269
+ delivery.nativeProgressStreamThreadTs = streamThreadTs;
1270
+ const startPromise = (async () => {
1271
+ const session = await startSlackStream({
1272
+ client: slackClient,
1273
+ channel: message.channel,
1274
+ threadTs: streamThreadTs,
1275
+ ...update.text ? { text: update.text } : {},
1276
+ ...update.chunks?.length ? { chunks: update.chunks } : {},
1277
+ taskDisplayMode: "plan",
1278
+ ...slackIdentity ? { identity: slackIdentity } : {},
1279
+ teamId: await resolveSlackStreamRecipientTeamId({
1280
+ client: slackClient,
1281
+ token: ctx.botToken,
1282
+ userId: message.user,
1283
+ fallbackTeamId: slackStreamFallbackTeamId
1284
+ }),
1285
+ userId: message.user
1286
+ });
1287
+ delivery.streamSession = session;
1288
+ return session;
1289
+ })();
1290
+ delivery.nativeProgressStreamStartPromise = startPromise;
1291
+ try {
1292
+ if (!await startPromise) return false;
1293
+ const delivered = markDelivered(streamThreadTs);
1294
+ return update.chunks?.length ? delivered : true;
1295
+ } finally {
1296
+ if (delivery.nativeProgressStreamStartPromise === startPromise) delivery.nativeProgressStreamStartPromise = null;
1297
+ }
1298
+ };
1299
+ const append = async (update) => {
1300
+ const session = delivery.streamSession;
1301
+ if (!session) return false;
1302
+ await appendSlackStream({
1303
+ session,
1304
+ ...update.text ? { text: update.text } : {},
1305
+ ...update.chunks?.length ? { chunks: update.chunks } : {}
1306
+ });
1307
+ const delivered = markDelivered(delivery.nativeProgressStreamThreadTs);
1308
+ return update.chunks?.length ? delivered : true;
1309
+ };
1310
+ return {
1311
+ append,
1312
+ start,
1313
+ waitForStart
910
1314
  };
911
- return params.streamStarted ? buildSlackProgressStreamUpdateChunks(input) : buildSlackProgressStreamStartChunks(input);
912
1315
  }
913
1316
  //#endregion
914
1317
  //#region extensions/slack/src/monitor/message-handler/dispatch-progress.ts
915
1318
  function createSlackProgressRuntime(runtimeParams) {
916
1319
  const { setup, delivery, resetPreviewDeliveryState } = runtimeParams;
917
- const { account, cfg, ctx, hasSlackCustomIdentity, message, prepared, replyPlan, runtime, slackClient, slackIdentity, slackMessageMetadata, slackStreaming, slackStreamFallbackTeamId, shouldUseDraftStream, useStreaming, previewStreamingEnabled } = setup;
1320
+ const { account, cfg, ctx, hasSlackCustomIdentity, message, prepared, replyPlan, runtime, slackClient, slackIdentity, slackMessageMetadata, slackStreaming, shouldUseDraftStream, useStreaming, previewStreamingEnabled } = setup;
918
1321
  const draftStream = shouldUseDraftStream ? createSlackDraftStream({
919
1322
  target: prepared.replyTarget,
920
1323
  cfg,
921
1324
  token: ctx.botToken,
922
1325
  accountId: account.accountId,
923
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {},
1326
+ conversationChannelId: message.channel,
1327
+ eventScope: prepared.eventScope,
924
1328
  ...!hasSlackCustomIdentity && slackIdentity ? { identity: slackIdentity } : {},
925
1329
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
926
1330
  maxChars: Math.min(ctx.textLimit, SLACK_TEXT_LIMIT),
@@ -933,40 +1337,61 @@ function createSlackProgressRuntime(runtimeParams) {
933
1337
  warn: logVerbose
934
1338
  }) : void 0;
935
1339
  let hasStreamedMessage = false;
936
- const streamMode = slackStreaming.draftMode;
1340
+ const isProgressMode = slackStreaming.mode === "progress";
937
1341
  const useNativeProgressStreaming = useStreaming && slackStreaming.mode === "progress";
938
1342
  const progressDraftActive = Boolean(draftStream) || useNativeProgressStreaming;
939
1343
  const previewToolProgressEnabled = progressDraftActive && resolveChannelStreamingPreviewToolProgress(account.config, true, slackStreaming.mode);
940
1344
  let shouldYieldDraftProgress = () => false;
941
1345
  const suppressDefaultToolProgressMessages = resolveChannelStreamingSuppressDefaultToolProgressMessages(account.config, {
942
1346
  draftStreamActive: Boolean(draftStream) || useNativeProgressStreaming,
1347
+ mode: slackStreaming.mode,
943
1348
  previewToolProgressEnabled,
944
1349
  previewStreamingEnabled
945
1350
  });
946
1351
  let previewToolProgressSuppressed = false;
947
- let legacyPreviewToolProgressLines = [];
948
- let nativeTaskState = /* @__PURE__ */ new Map();
1352
+ let nativeStreamSnapshot = EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT;
949
1353
  let appendRenderedText = "";
950
1354
  let appendSourceText = "";
951
1355
  let nativeProgressCompletionSent = false;
952
1356
  let nativeProgressTerminalStatus = "complete";
953
- let nativeProgressChunkKey;
954
- const progressReceipt = createChannelProgressReceiptTracker();
955
- let progressReceiptCollapsed = false;
956
- let pendingNativeProgressReceipt;
1357
+ let nativeNarrationRenderedText = "";
1358
+ let nativeNarrationSourceText = "";
1359
+ let nativeStreamOrder = Promise.resolve();
1360
+ const withNativeStreamOrder = (task) => {
1361
+ const run = nativeStreamOrder.then(task, task);
1362
+ nativeStreamOrder = run.catch(() => void 0);
1363
+ return run;
1364
+ };
1365
+ const progressWorkCounter = createChannelProgressWorkCounter();
957
1366
  const progressSeed = `${account.accountId}:${message.channel}`;
958
- const useRichProgressDraft = streamMode === "status_final" && resolveChannelProgressDraftRender(account.config) === "rich";
1367
+ const slackProgressStyle = resolveSlackProgressStyle(account.config);
1368
+ const useDraftProgressCard = Boolean(draftStream) && isProgressMode && slackProgressStyle === "card";
959
1369
  const explicitProgressTitle = resolveExplicitSlackProgressTitle(account.config);
960
1370
  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;
1371
+ const progressCard = createSlackDraftProgressCardRuntime({
1372
+ setup: {
1373
+ account,
1374
+ cfg,
1375
+ ctx,
1376
+ prepared,
1377
+ slackClient
1378
+ },
1379
+ draftStream,
1380
+ enabled: useDraftProgressCard,
1381
+ progressWorkCounter,
1382
+ progressSeed,
1383
+ explicitTitle: explicitProgressTitle,
1384
+ maxLineChars: progressDraftMaxLineChars,
1385
+ getSnapshot: () => progressDraft.getSnapshot(),
1386
+ getThreadTs: () => delivery.usedReplyThreadTs
1387
+ });
1388
+ const nativeTransport = createSlackNativeProgressTransport({
1389
+ setup,
1390
+ delivery
1391
+ });
1392
+ const dropDetachedProgressCards = async () => {
1393
+ if (!useDraftProgressCard) return;
1394
+ await draftStream?.dropDetachedMessages();
970
1395
  };
971
1396
  const appendNativeProgressCompletion = async (isError) => {
972
1397
  const session = delivery.streamSession;
@@ -986,150 +1411,151 @@ function createSlackProgressRuntime(runtimeParams) {
986
1411
  runtime.error?.(danger(`slack-stream: native progress completion failed: ${formatSlackError(err)}`));
987
1412
  }
988
1413
  };
1414
+ const resolveNativeProgressTitle = (snapshot) => combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation);
989
1415
  const buildNativeProgressChunks$1 = (snapshot) => buildNativeProgressChunks({
990
1416
  snapshot,
991
- streamStarted: Boolean(delivery.streamSession),
992
- title: combineProgressHeadlineAndExplanation(explicitProgressTitle ?? snapshot.statusHeadline, snapshot.planExplanation),
1417
+ title: resolveNativeProgressTitle(snapshot),
993
1418
  maxLineChars: progressDraftMaxLineChars
994
1419
  });
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
- }
1420
+ const normalizeProgressText = (text) => text?.replace(/\s+/gu, " ").trim() ?? "";
1421
+ const isRenderedAsProgressTitle = (text) => {
1422
+ const candidate = normalizeProgressText(text);
1423
+ if (!candidate) return false;
1424
+ const title = normalizeProgressText(resolveNativeProgressTitle(progressDraft.getSnapshot()));
1425
+ return title.length > 0 && title.includes(candidate);
1002
1426
  };
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();
1040
- };
1041
- const appendNativeProgressStream = async (chunks, chunkKey) => {
1042
- if (!delivery.streamSession) return;
1043
- await appendSlackStream({
1044
- session: delivery.streamSession,
1045
- chunks
1427
+ const resolveNarrationUpdate = (incoming) => {
1428
+ const next = applyAppendOnlyStreamUpdate({
1429
+ incoming: incoming ?? "",
1430
+ rendered: nativeNarrationRenderedText,
1431
+ source: nativeNarrationSourceText
1046
1432
  });
1047
- markNativeProgressDelivered(delivery.streamSession);
1048
- nativeProgressChunkKey = chunkKey;
1433
+ return {
1434
+ next,
1435
+ delta: next.changed ? next.rendered.slice(nativeNarrationRenderedText.length) : ""
1436
+ };
1049
1437
  };
1050
- const updateNativeProgressStream = async () => {
1438
+ const updateNativeProgressStream = () => withNativeStreamOrder(updateNativeProgressStreamNow);
1439
+ const updateNativeProgressStreamNow = async () => {
1051
1440
  const snapshot = progressDraft.getSnapshot();
1052
1441
  const progressLines = resolveNativeProgressLines(snapshot);
1053
- 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;
1442
+ const narrationUpdate = resolveNarrationUpdate(resolveNativeProgressNarration(snapshot));
1443
+ const hasRetirableNativeTasks = [...nativeStreamSnapshot.tasks.values()].some((task) => task.status !== "complete" && task.status !== "error");
1444
+ if (!useNativeProgressStreaming || delivery.streamFailed || progressLines.length === 0 && !snapshot.plan?.length && !snapshot.statusHeadline && !explicitProgressTitle && !hasRetirableNativeTasks && !narrationUpdate.delta) return false;
1445
+ if (!await nativeTransport.waitForStart()) return false;
1056
1446
  const reconciled = reconcileSlackNativeTaskChunks({
1057
- previousTasks: nativeTaskState,
1447
+ previous: nativeStreamSnapshot,
1058
1448
  chunks: buildNativeProgressChunks$1(snapshot)
1059
1449
  });
1060
1450
  const chunks = reconciled.chunks;
1061
- if (!chunks?.length) return;
1062
- const chunkKey = JSON.stringify(chunks);
1063
- if (chunkKey === nativeProgressChunkKey) return;
1451
+ if (!chunks?.length && !narrationUpdate.delta) return false;
1064
1452
  try {
1065
- if (!delivery.streamSession) await startNativeProgressStream(chunks, chunkKey);
1066
- else await appendNativeProgressStream(chunks, chunkKey);
1067
- nativeTaskState = reconciled.tasks;
1453
+ const hadSession = Boolean(delivery.streamSession);
1454
+ const streamUpdate = {
1455
+ ...narrationUpdate.delta ? { text: narrationUpdate.delta } : {},
1456
+ ...chunks?.length ? { chunks } : {}
1457
+ };
1458
+ if (!(hadSession ? await nativeTransport.append(streamUpdate) : await nativeTransport.start(streamUpdate))) return false;
1459
+ if (!hadSession) replyPlan.markSent();
1460
+ if (narrationUpdate.next.changed) {
1461
+ nativeNarrationRenderedText = narrationUpdate.next.rendered;
1462
+ nativeNarrationSourceText = narrationUpdate.next.source;
1463
+ }
1464
+ if (chunks?.length) nativeStreamSnapshot = reconciled.snapshot;
1465
+ return true;
1068
1466
  } catch (err) {
1069
1467
  runtime.error?.(danger(`slack-stream: native progress stream failed: ${formatSlackError(err)}, falling back`));
1070
1468
  delivery.streamFailed = true;
1469
+ return false;
1071
1470
  }
1072
1471
  };
1073
- const resetProgressTurnState = () => {
1074
- progressReceipt.reset();
1075
- progressReceiptCollapsed = false;
1076
- pendingNativeProgressReceipt = void 0;
1077
- };
1078
- const collapseProgressReceipt = async (params) => {
1079
- progressReceiptCollapsed = await collapseSlackProgressReceipt({
1080
- setup,
1081
- edit: params
1472
+ const appendNativeNarration = (payload, kind) => withNativeStreamOrder(() => appendNativeNarrationNow(payload, kind));
1473
+ const appendNativeNarrationNow = async (payload, kind) => {
1474
+ if (isRenderedAsProgressTitle(payload.text)) return false;
1475
+ const narrationUpdate = resolveNarrationUpdate(payload.text?.trimEnd());
1476
+ if (!narrationUpdate.delta) return false;
1477
+ await delivery.deliverWithStreaming({
1478
+ payload,
1479
+ kind,
1480
+ streamText: narrationUpdate.delta,
1481
+ appendSeparator: false,
1482
+ taskDisplayMode: "plan"
1082
1483
  });
1484
+ if (!delivery.streamFailed) {
1485
+ nativeNarrationRenderedText = narrationUpdate.next.rendered;
1486
+ nativeNarrationSourceText = narrationUpdate.next.source;
1487
+ }
1488
+ return true;
1489
+ };
1490
+ const resetProgressTurnState = () => {
1491
+ progressWorkCounter.reset();
1492
+ nativeNarrationRenderedText = "";
1493
+ nativeNarrationSourceText = "";
1083
1494
  };
1084
1495
  const progressDraft = createChannelProgressDraftCompositor({
1085
1496
  entry: account.config,
1086
1497
  mode: slackStreaming.mode,
1087
- active: progressDraftActive && streamMode === "status_final",
1498
+ active: progressDraftActive,
1088
1499
  seed: progressSeed,
1089
- formatLine: escapeSlackMrkdwn,
1500
+ formatLine: formatSlackProgressDraftLine,
1090
1501
  reasoningLinePrefix: "🧠 ",
1091
- commentaryLinePrefix: "💬 ",
1502
+ commentaryLinePrefix: "",
1092
1503
  reasoningGate: previewToolProgressEnabled,
1093
- commentaryItalics: false,
1094
- updateOnLineChange: useNativeProgressStreaming || useRichProgressDraft,
1504
+ commentaryItalics: true,
1505
+ buildProgressEventLine: (input, options) => input.event === "tool" || input.event === "item" || input.event === "command-output" ? buildChannelProgressDraftLineForEntry(account.config, input, options) : buildChannelProgressDraftLine(input, options),
1506
+ updateOnLineChange: useNativeProgressStreaming || useDraftProgressCard,
1095
1507
  update: async (previewText, options) => {
1096
- if (useNativeProgressStreaming) {
1097
- await updateNativeProgressStream();
1098
- return;
1099
- }
1100
- if (!draftStream) return;
1508
+ if (useNativeProgressStreaming) return await updateNativeProgressStream();
1509
+ if (!draftStream) return false;
1101
1510
  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 ? {
1511
+ progressCard.setFallbackText(previewText);
1512
+ draftStream.update(useDraftProgressCard ? {
1112
1513
  text: previewText,
1113
- blocks: richProgressBlocks
1514
+ blocks: progressCard.resolvePresentation(snapshot, "working")
1114
1515
  } : previewText);
1115
1516
  hasStreamedMessage = true;
1116
1517
  if (options?.flush) await draftStream.flush();
1518
+ return Boolean(draftStream.messageId() && draftStream.channelId());
1117
1519
  }
1118
1520
  });
1119
1521
  const commentaryProgressEnabled = progressDraft.commentaryProgressEnabled;
1522
+ const deliverNativeFinal = (payload, kind) => withNativeStreamOrder(() => deliverNativeFinalNow(payload, kind));
1523
+ const deliverNativeFinalNow = async (payload, kind) => {
1524
+ progressDraft.markFinalReplyStarted();
1525
+ const streamReady = await nativeTransport.waitForStart();
1526
+ const finalThreadTs = delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs;
1527
+ if (payload.isError !== true && streamReady && Boolean(delivery.streamSession) && delivery.isStreamingEligible(payload, { maxTextBytes: 4e3 })) {
1528
+ await appendNativeProgressCompletion(false);
1529
+ await delivery.deliverWithStreaming({
1530
+ payload,
1531
+ kind
1532
+ });
1533
+ } else {
1534
+ await delivery.deliverNormally({
1535
+ payload,
1536
+ kind,
1537
+ forcedThreadTs: finalThreadTs
1538
+ });
1539
+ await appendNativeProgressCompletion(payload.isError === true);
1540
+ }
1541
+ progressDraft.markFinalReplyDelivered();
1542
+ };
1120
1543
  const buildNativeProgressCompletionChunks = (finalInProgressStatus) => {
1121
1544
  const snapshot = progressDraft.getSnapshot();
1122
1545
  const lines = resolveNativeProgressLines(snapshot);
1123
- const hasRetirableNativeTasks = [...nativeTaskState.values()].some((task) => task.status !== "complete" && task.status !== "error");
1124
- if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks) return;
1546
+ const sessionUrl = progressCard.resolveSessionUrl();
1547
+ const hasRetirableNativeTasks = [...nativeStreamSnapshot.tasks.values()].some((task) => task.status !== "complete" && task.status !== "error");
1548
+ if (lines.length === 0 && !snapshot.plan?.length && !hasRetirableNativeTasks && !snapshot.diffStat && !sessionUrl) return;
1125
1549
  return reconcileSlackNativeTaskChunks({
1126
- previousTasks: nativeTaskState,
1550
+ previous: nativeStreamSnapshot,
1127
1551
  chunks: buildSlackProgressStreamCompletionChunks({
1128
- title: resolveNativeProgressTitle(snapshot),
1552
+ title: resolveNativeProgressTitle(snapshot) ?? (lines.length === 0 && !snapshot.plan?.length ? "Working" : void 0),
1129
1553
  lines,
1130
1554
  plan: resolveNativeProgressPlan(snapshot),
1131
1555
  maxLineChars: progressDraftMaxLineChars,
1132
- finalInProgressStatus
1556
+ finalInProgressStatus,
1557
+ diffStat: snapshot.diffStat,
1558
+ sessionUrl
1133
1559
  })
1134
1560
  }).chunks;
1135
1561
  };
@@ -1144,12 +1570,6 @@ function createSlackProgressRuntime(runtimeParams) {
1144
1570
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
1145
1571
  });
1146
1572
  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
1573
  } catch (err) {
1154
1574
  const error = formatSlackError(err);
1155
1575
  delivery.emitAcknowledgedStreamedDeliveries();
@@ -1162,16 +1582,16 @@ function createSlackProgressRuntime(runtimeParams) {
1162
1582
  delivery.streamFailed = false;
1163
1583
  };
1164
1584
  const pushPlanProgress = async (steps, explanation) => {
1165
- if (streamMode === "status_final") {
1166
- await progressDraft.pushPlanProgress(steps, { explanation });
1167
- return;
1585
+ if (isProgressMode) {
1586
+ if (slackProgressStyle === "compact") return false;
1587
+ return await progressDraft.pushPlanProgress(steps, { explanation });
1168
1588
  }
1169
- if (previewToolProgressSuppressed || !draftStream) return;
1589
+ if (previewToolProgressSuppressed || !draftStream) return false;
1170
1590
  const text = formatChannelProgressDraftText({
1171
1591
  entry: account.config,
1172
- lines: legacyPreviewToolProgressLines,
1592
+ lines: [...progressDraft.getSnapshot().lines],
1173
1593
  seed: progressSeed,
1174
- formatLine: escapeSlackMrkdwn,
1594
+ formatLine: formatSlackProgressDraftLine,
1175
1595
  narration: explanation,
1176
1596
  plan: steps
1177
1597
  });
@@ -1179,37 +1599,25 @@ function createSlackProgressRuntime(runtimeParams) {
1179
1599
  draftStream.update(text);
1180
1600
  hasStreamedMessage = true;
1181
1601
  }
1602
+ return false;
1182
1603
  };
1183
1604
  const pushPreviewProgress = async (line, options) => {
1184
- if (!draftStream && !useNativeProgressStreaming) return;
1185
- if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return;
1605
+ if (!draftStream && !useNativeProgressStreaming) return false;
1606
+ if (options?.toolName !== void 0 && !isChannelProgressDraftWorkToolName(options.toolName)) return false;
1186
1607
  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;
1608
+ if (isProgressMode) {
1609
+ if (!line || !normalized) return await progressDraft.noteActivity();
1610
+ return await progressDraft.pushToolProgress(line, options);
1194
1611
  }
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;
1612
+ if (!line || !normalized || !draftStream || !previewToolProgressEnabled) return false;
1613
+ return await progressDraft.pushToolProgress(line, options);
1206
1614
  };
1207
1615
  const updateDraftFromPartial = (text) => {
1208
1616
  const trimmed = text?.trimEnd();
1209
- if (!trimmed) return;
1210
- if (streamMode === "append") {
1617
+ if (!trimmed) return false;
1618
+ if (slackStreaming.mode === "block") {
1211
1619
  previewToolProgressSuppressed = true;
1212
- legacyPreviewToolProgressLines = [];
1620
+ progressDraft.suppress();
1213
1621
  const next = applyAppendOnlyStreamUpdate({
1214
1622
  incoming: trimmed,
1215
1623
  rendered: appendRenderedText,
@@ -1217,32 +1625,33 @@ function createSlackProgressRuntime(runtimeParams) {
1217
1625
  });
1218
1626
  appendRenderedText = next.rendered;
1219
1627
  appendSourceText = next.source;
1220
- if (!next.changed) return;
1628
+ if (!next.changed) return false;
1221
1629
  draftStream?.update(next.rendered);
1222
1630
  hasStreamedMessage = true;
1223
- return;
1631
+ return false;
1224
1632
  }
1225
- if (streamMode === "status_final") return;
1633
+ if (isProgressMode) return false;
1226
1634
  previewToolProgressSuppressed = true;
1227
- legacyPreviewToolProgressLines = [];
1635
+ progressDraft.suppress();
1228
1636
  draftStream?.update(trimmed);
1229
1637
  hasStreamedMessage = true;
1638
+ return false;
1230
1639
  };
1231
1640
  const pushReasoningProgress = async (payload) => {
1232
- if (!payload?.text) return;
1233
- if (streamMode !== "status_final") {
1641
+ if (!payload?.text) return false;
1642
+ if (!isProgressMode) {
1234
1643
  const normalized = progressDraft.mergeReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true }).replace(/^_(.*)_$/su, "$1").trim();
1235
- if (!normalized) return;
1236
- await pushPreviewProgress({
1644
+ if (!normalized) return false;
1645
+ const visible = await pushPreviewProgress({
1237
1646
  id: "reasoning",
1238
1647
  kind: "item",
1239
1648
  text: normalized,
1240
1649
  label: "Reasoning"
1241
1650
  });
1242
- return;
1651
+ progressDraft.mergeReasoningProgress(normalized, { snapshot: true });
1652
+ return visible;
1243
1653
  }
1244
- progressReceipt.noteReasoning();
1245
- await progressDraft.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
1654
+ return await progressDraft.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
1246
1655
  };
1247
1656
  const resetDraftDeliveryState = () => {
1248
1657
  hasStreamedMessage = false;
@@ -1250,44 +1659,62 @@ function createSlackProgressRuntime(runtimeParams) {
1250
1659
  appendSourceText = "";
1251
1660
  };
1252
1661
  const resetDraftProgressState = () => {
1253
- progressDraft.resetReasoningProgress();
1254
1662
  previewToolProgressSuppressed = false;
1255
- legacyPreviewToolProgressLines = [];
1663
+ progressDraft.reset();
1256
1664
  };
1257
1665
  const beginNewProgressTurn = async (options) => {
1666
+ const priorSnapshot = progressDraft.getSnapshot();
1667
+ const priorFallbackText = progressCard.resolveText(priorSnapshot);
1258
1668
  const completionChunks = useNativeProgressStreaming && !nativeProgressCompletionSent ? buildNativeProgressCompletionChunks(nativeProgressTerminalStatus) : void 0;
1259
1669
  if (!progressDraft.beginNewTurn(options)) return false;
1260
1670
  if (useNativeProgressStreaming) await finishNativeProgressTurn(completionChunks);
1261
- else draftStream?.forceNewMessage();
1671
+ else {
1672
+ await progressCard.finalize("success", priorSnapshot, priorFallbackText);
1673
+ draftStream?.forceNewMessage();
1674
+ await dropDetachedProgressCards();
1675
+ }
1262
1676
  resetProgressTurnState();
1263
- nativeTaskState = /* @__PURE__ */ new Map();
1677
+ nativeStreamSnapshot = EMPTY_SLACK_NATIVE_STREAM_SNAPSHOT;
1264
1678
  nativeProgressCompletionSent = false;
1265
1679
  nativeProgressTerminalStatus = "complete";
1266
- nativeProgressChunkKey = void 0;
1680
+ progressCard.reset();
1267
1681
  resetPreviewDeliveryState();
1268
1682
  delivery.resetDeliveryTracker();
1269
- progressReceiptCollapsed = false;
1270
1683
  return true;
1271
1684
  };
1685
+ const onDraftBoundary = !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1686
+ if (isProgressMode) {
1687
+ await beginNewProgressTurn();
1688
+ return;
1689
+ }
1690
+ if (hasStreamedMessage) draftStream?.forceNewMessage();
1691
+ resetDraftDeliveryState();
1692
+ resetDraftProgressState();
1693
+ };
1694
+ const onQueuedFollowupAdmitted = !shouldUseDraftStream && !useNativeProgressStreaming ? void 0 : async () => {
1695
+ await draftStream?.flush();
1696
+ resetPreviewDeliveryState();
1697
+ if (isProgressMode) await beginNewProgressTurn({ force: true });
1698
+ else draftStream?.forceNewMessage();
1699
+ delivery.resetDeliveryTracker();
1700
+ resetDraftDeliveryState();
1701
+ resetDraftProgressState();
1702
+ };
1703
+ const onQueuedFollowupSettled = !useDraftProgressCard ? void 0 : async () => {
1704
+ if (!progressCard.hasTerminalized) await draftStream?.clear();
1705
+ await dropDetachedProgressCards();
1706
+ };
1272
1707
  return {
1273
1708
  draftStream,
1274
- streamMode,
1709
+ isProgressMode,
1710
+ useDraftProgressCard,
1275
1711
  useNativeProgressStreaming,
1276
1712
  progressDraftActive,
1277
1713
  previewToolProgressEnabled,
1278
1714
  suppressDefaultToolProgressMessages,
1279
1715
  progressDraft,
1280
1716
  commentaryProgressEnabled,
1281
- progressReceipt,
1282
- get progressReceiptCollapsed() {
1283
- return progressReceiptCollapsed;
1284
- },
1285
- get pendingNativeProgressReceipt() {
1286
- return pendingNativeProgressReceipt;
1287
- },
1288
- set pendingNativeProgressReceipt(value) {
1289
- pendingNativeProgressReceipt = value;
1290
- },
1717
+ progressWorkCounter,
1291
1718
  get nativeProgressCompletionSent() {
1292
1719
  return nativeProgressCompletionSent;
1293
1720
  },
@@ -1297,33 +1724,17 @@ function createSlackProgressRuntime(runtimeParams) {
1297
1724
  get nativeProgressTerminalStatus() {
1298
1725
  return nativeProgressTerminalStatus;
1299
1726
  },
1300
- appendNativeProgressCompletion,
1301
- beginNewProgressTurn,
1727
+ appendNativeNarration,
1302
1728
  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
- },
1729
+ deliverNativeFinal,
1730
+ dropDetachedProgressCards,
1731
+ finalizeDraftProgressCard: progressCard.finalize,
1732
+ onDraftBoundary,
1733
+ onQueuedFollowupAdmitted,
1734
+ onQueuedFollowupSettled,
1322
1735
  pushPlanProgress,
1323
- pushPreviewProgress,
1324
1736
  pushReasoningProgress,
1325
1737
  updateDraftFromPartial,
1326
- waitForNativeProgressStreamStart,
1327
1738
  setShouldYieldDraftProgress: (value) => {
1328
1739
  shouldYieldDraftProgress = value;
1329
1740
  },
@@ -1398,7 +1809,7 @@ async function createSlackDispatchSetup(prepared) {
1398
1809
  sessionKey: inboundLastRouteSessionKey,
1399
1810
  deliveryContext: {
1400
1811
  channel: "slack",
1401
- to: `user:${message.user}`,
1812
+ to: prepared.ctxPayload.OriginatingTo ?? prepared.ctxPayload.To ?? `user:${message.user}`,
1402
1813
  accountId: route.accountId,
1403
1814
  threadId: prepared.ctxPayload.MessageThreadId ?? prepared.ctxPayload.TransportThreadId
1404
1815
  },
@@ -1434,6 +1845,7 @@ async function createSlackDispatchSetup(prepared) {
1434
1845
  const messageTs = message.ts ?? message.event_ts;
1435
1846
  const incomingThreadTs = message.thread_ts;
1436
1847
  let didSetStatus = false;
1848
+ let didAddTypingReaction = false;
1437
1849
  const statusReactionsEnabled = prepared.ctxPayload.InboundEventKind !== "room_event" && Boolean(prepared.ackReactionPromise) && Boolean(reactionMessageTs) && cfg.messages?.statusReactions?.enabled === true;
1438
1850
  const statusReactions = createStatusReactionController({
1439
1851
  enabled: statusReactionsEnabled,
@@ -1480,6 +1892,7 @@ async function createSlackDispatchSetup(prepared) {
1480
1892
  });
1481
1893
  const typingTarget = statusThreadTs ? `${message.channel}/${statusThreadTs}` : message.channel;
1482
1894
  const typingReaction = ctx.typingReaction;
1895
+ const threadStatusGate = { hasVisibleOutput: () => false };
1483
1896
  const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
1484
1897
  cfg,
1485
1898
  agentId: route.agentId,
@@ -1491,35 +1904,44 @@ async function createSlackDispatchSetup(prepared) {
1491
1904
  },
1492
1905
  typing: {
1493
1906
  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
- });
1907
+ if (!threadStatusGate.hasVisibleOutput()) {
1908
+ didSetStatus = true;
1909
+ await ctx.setSlackThreadStatus({
1910
+ channelId: message.channel,
1911
+ threadTs: statusThreadTs,
1912
+ status: "is typing...",
1913
+ eventScope: prepared.eventScope
1914
+ });
1915
+ }
1916
+ if (typingReaction && message.ts) {
1917
+ didAddTypingReaction = true;
1918
+ await reactSlackMessage(message.channel, message.ts, typingReaction, {
1919
+ token: ctx.botToken,
1920
+ client: slackClient
1921
+ }).catch((err) => {
1922
+ logVerbose(`slack send: typing reaction failed: ${formatSlackError(err)}`);
1923
+ });
1924
+ }
1507
1925
  },
1508
1926
  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
- });
1927
+ if (didSetStatus) {
1928
+ didSetStatus = false;
1929
+ await ctx.setSlackThreadStatus({
1930
+ channelId: message.channel,
1931
+ threadTs: statusThreadTs,
1932
+ status: "",
1933
+ eventScope: prepared.eventScope
1934
+ });
1935
+ }
1936
+ if (didAddTypingReaction && typingReaction && message.ts) {
1937
+ didAddTypingReaction = false;
1938
+ await removeSlackReaction(message.channel, message.ts, typingReaction, {
1939
+ token: ctx.botToken,
1940
+ client: slackClient
1941
+ }).catch((err) => {
1942
+ logVerbose(`slack send: typing reaction removal failed: ${formatSlackError(err)}`);
1943
+ });
1944
+ }
1523
1945
  },
1524
1946
  onStartError: (err) => {
1525
1947
  logTypingFailure({
@@ -1541,10 +1963,7 @@ async function createSlackDispatchSetup(prepared) {
1541
1963
  }
1542
1964
  }
1543
1965
  });
1544
- const slackStreaming = resolveSlackStreamingConfig({
1545
- streaming: account.config.streaming,
1546
- nativeStreaming: resolveChannelStreamingNativeTransport(account.config)
1547
- });
1966
+ const slackStreaming = resolveSlackStreamingConfig({ streaming: account.config.streaming });
1548
1967
  const streamThreadHint = forcedReplyThreadTs ?? resolveSlackStreamingThreadHint({
1549
1968
  replyToMode: replyDeliveryMode,
1550
1969
  incomingThreadTs,
@@ -1553,7 +1972,7 @@ async function createSlackDispatchSetup(prepared) {
1553
1972
  });
1554
1973
  const hookRunner = getGlobalHookRunner();
1555
1974
  const allowPreHookProviderStreaming = !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false));
1556
- const previewStreamingEnabled = allowPreHookProviderStreaming && !sourceRepliesAreToolOnly && shouldEnableSlackPreviewStreaming({ mode: slackStreaming.mode });
1975
+ const previewStreamingEnabled = allowPreHookProviderStreaming && !sourceRepliesAreToolOnly && slackStreaming.mode !== "off";
1557
1976
  const hasSlackCustomIdentity = Boolean(slackIdentity?.username || slackIdentity?.iconUrl || slackIdentity?.iconEmoji);
1558
1977
  const useStreaming = shouldUseStreaming({
1559
1978
  streamingEnabled: !sourceRepliesAreToolOnly && (allowPreHookProviderStreaming || slackStreaming.mode !== "progress") && isSlackStreamingEnabled({
@@ -1563,10 +1982,7 @@ async function createSlackDispatchSetup(prepared) {
1563
1982
  }),
1564
1983
  threadTs: streamThreadHint
1565
1984
  });
1566
- const shouldUseDraftStream = shouldInitializeSlackDraftStream({
1567
- previewStreamingEnabled,
1568
- useStreaming
1569
- });
1985
+ const shouldUseDraftStream = previewStreamingEnabled && !useStreaming;
1570
1986
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1571
1987
  const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveSlackDisableBlockStreaming({
1572
1988
  useStreaming,
@@ -1604,6 +2020,7 @@ async function createSlackDispatchSetup(prepared) {
1604
2020
  statusReactionsEnabled,
1605
2021
  statusReactions,
1606
2022
  hasRepliedRef,
2023
+ threadStatusGate,
1607
2024
  replyPlan,
1608
2025
  onModelSelected,
1609
2026
  replyPipeline,
@@ -1632,11 +2049,6 @@ function createSlackStreamingDeliveryRuntime(setup) {
1632
2049
  observedFinalReplyDelivery: false
1633
2050
  };
1634
2051
  const streamedDeliveries = [];
1635
- const streamedFailuresOwnedByDispatcher = {
1636
- tool: 0,
1637
- block: 0,
1638
- final: 0
1639
- };
1640
2052
  const refreshStreamedAcknowledgements = (session) => {
1641
2053
  if (session.pendingText.length === 0) for (const delivery of streamedDeliveries) delivery.acknowledged = true;
1642
2054
  };
@@ -1752,7 +2164,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1752
2164
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1753
2165
  ...messageSentDeliveryHookContext,
1754
2166
  deferMessageSentHooks: true,
1755
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {}
2167
+ eventScope: prepared.eventScope
1756
2168
  });
1757
2169
  markSlackStreamFallbackDelivered(session);
1758
2170
  if (!session.stopped) try {
@@ -1800,7 +2212,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1800
2212
  ...slackIdentity ? { identity: slackIdentity } : {},
1801
2213
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {},
1802
2214
  ...messageSentDeliveryHookContext,
1803
- ...prepared.eventScope ? { eventScope: prepared.eventScope } : {}
2215
+ eventScope: prepared.eventScope
1804
2216
  });
1805
2217
  state.observedReplyDelivery = true;
1806
2218
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
@@ -1819,10 +2231,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1819
2231
  return deliveryReplyThreadTs;
1820
2232
  };
1821
2233
  const deliverBufferedStreamFallback = async (params) => {
1822
- if (!await deliverPendingStreamFallback(params.session, params.err)) {
1823
- streamedFailuresOwnedByDispatcher[params.kind] += 1;
1824
- return false;
1825
- }
2234
+ if (!await deliverPendingStreamFallback(params.session, params.err)) return false;
1826
2235
  replyPlan.markSent();
1827
2236
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
1828
2237
  deliveryTracker.markDelivered({
@@ -1834,12 +2243,16 @@ function createSlackStreamingDeliveryRuntime(setup) {
1834
2243
  rememberDeliveredThreadTs(params.kind, params.session.threadTs);
1835
2244
  return true;
1836
2245
  };
2246
+ const isStreamingEligible = (payload, options) => {
2247
+ const reply = resolveSendableOutboundReplyParts(payload);
2248
+ const renderPlan = resolveSlackReplyRenderPlan(payload);
2249
+ const plannedBlocks = renderPlan.mode === "single" ? renderPlan.blocks : renderPlan.blockPart?.blocks;
2250
+ return !state.streamFailed && !reply.hasMedia && renderPlan.mode !== "split" && !plannedBlocks?.length && !readSlackReplyBlocks(payload)?.length && reply.hasText && (!options?.maxTextBytes || countSlackTextUtf8Bytes(reply.trimmedText) <= options.maxTextBytes);
2251
+ };
1837
2252
  const deliverWithStreaming = async (params) => {
1838
2253
  if (params.payload.isReasoning === true) return;
1839
2254
  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) {
2255
+ if (!isStreamingEligible(params.payload)) {
1843
2256
  await deliverNormally({
1844
2257
  payload: params.payload,
1845
2258
  kind: params.kind,
@@ -1847,7 +2260,8 @@ function createSlackStreamingDeliveryRuntime(setup) {
1847
2260
  });
1848
2261
  return;
1849
2262
  }
1850
- const text = reply.trimmedText;
2263
+ const text = params.streamText ?? reply.trimmedText;
2264
+ const hookContent = reply.trimmedText;
1851
2265
  let plannedThreadTs;
1852
2266
  try {
1853
2267
  if (!state.streamSession && state.nativeProgressStreamStartPromise) await state.nativeProgressStreamStartPromise;
@@ -1885,6 +2299,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1885
2299
  channel: message.channel,
1886
2300
  threadTs: streamThreadTs,
1887
2301
  text,
2302
+ ...params.taskDisplayMode ? { taskDisplayMode: params.taskDisplayMode } : {},
1888
2303
  ...slackIdentity ? { identity: slackIdentity } : {},
1889
2304
  teamId: await resolveSlackStreamRecipientTeamId({
1890
2305
  client: slackClient,
@@ -1899,7 +2314,7 @@ function createSlackStreamingDeliveryRuntime(setup) {
1899
2314
  state.observedReplyDelivery = true;
1900
2315
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
1901
2316
  }
1902
- if (text) rememberStreamedDelivery(params.kind, text, state.streamSession);
2317
+ if (hookContent) rememberStreamedDelivery(params.kind, hookContent, state.streamSession);
1903
2318
  rememberDeliveredThreadTs(params.kind, streamThreadTs);
1904
2319
  replyPlan.markSent();
1905
2320
  deliveryTracker.markDelivered({
@@ -1919,13 +2334,13 @@ function createSlackStreamingDeliveryRuntime(setup) {
1919
2334
  logVerbose("slack-stream: suppressed duplicate append payload");
1920
2335
  return;
1921
2336
  }
1922
- if (text) recordStreamedDelivery(params.kind, text);
2337
+ if (hookContent) recordStreamedDelivery(params.kind, hookContent);
1923
2338
  await appendSlackStream({
1924
2339
  session: state.streamSession,
1925
- text: "\n" + text
2340
+ text: `${params.appendSeparator === false ? "" : "\n"}${text}`
1926
2341
  });
1927
2342
  refreshStreamedAcknowledgements(state.streamSession);
1928
- if (state.streamSession.delivered) {
2343
+ if (state.streamSession.delivered && state.streamSession.pendingText.length === 0) {
1929
2344
  state.observedReplyDelivery = true;
1930
2345
  if (params.kind === "final") state.observedFinalReplyDelivery = true;
1931
2346
  }
@@ -1975,23 +2390,6 @@ function createSlackStreamingDeliveryRuntime(setup) {
1975
2390
  });
1976
2391
  }
1977
2392
  };
1978
- const reconcileCounts = (counts) => {
1979
- const next = { ...counts };
1980
- let changed = false;
1981
- for (const kind of [
1982
- "tool",
1983
- "block",
1984
- "final"
1985
- ]) {
1986
- const failedStreamedCount = streamedDeliveries.filter((delivery) => delivery.kind === kind && delivery.outcome === "failure").length;
1987
- const additionalFailedStreamed = Math.max(0, failedStreamedCount - streamedFailuresOwnedByDispatcher[kind]);
1988
- if (additionalFailedStreamed > 0) {
1989
- next[kind] = Math.max(0, (next[kind] ?? 0) - additionalFailedStreamed);
1990
- changed = true;
1991
- }
1992
- }
1993
- return changed ? next : counts;
1994
- };
1995
2393
  return Object.assign(state, {
1996
2394
  acknowledgeStoppedStreamedDeliveries,
1997
2395
  deliverNormally,
@@ -2000,9 +2398,9 @@ function createSlackStreamingDeliveryRuntime(setup) {
2000
2398
  emitAcknowledgedStreamedDeliveries,
2001
2399
  emitFailedPendingStreamedDeliveries,
2002
2400
  hasDelivered: (params) => deliveryTracker.hasDelivered(params),
2401
+ isStreamingEligible,
2003
2402
  markPreviewPayloadDelivered,
2004
2403
  rememberDeliveredThreadTs,
2005
- reconcileCounts,
2006
2404
  resetDeliveryTracker: () => {
2007
2405
  deliveryTracker = createSlackEventDeliveryTracker();
2008
2406
  }
@@ -2024,47 +2422,66 @@ async function dispatchPreparedSlackMessage(prepared) {
2024
2422
  }
2025
2423
  });
2026
2424
  const draftStream = progress.draftStream;
2425
+ setup.threadStatusGate.hasVisibleOutput = () => delivery.observedReplyDelivery || draftPreviewCommitted.value || Boolean(draftStream?.messageId());
2426
+ const failureNoticeThreadTs = message.thread_ts;
2427
+ const failureNoticeTeamId = prepared.eventScope?.teamId;
2428
+ let sawTerminalFailurePayload = false;
2429
+ let pendingFailureNotice;
2430
+ const filterPassiveThreadFailure = (payload) => {
2431
+ if (payload.isError !== true || prepared.ctxPayload.ChatType !== "channel" || isReplyPayloadNonTerminalToolErrorWarning(payload)) return payload;
2432
+ sawTerminalFailurePayload = true;
2433
+ if (delivery.observedReplyDelivery || draftPreviewCommitted.value) return payload;
2434
+ 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;
2435
+ const noticeThreadTs = failureNoticeThreadTs ?? (explicitlyAddressed ? statusThreadTs : void 0);
2436
+ const notice = {
2437
+ accountId: account.accountId,
2438
+ channelId: message.channel,
2439
+ ...noticeThreadTs ? { threadTs: noticeThreadTs } : {},
2440
+ failureText: payload.text ?? "",
2441
+ ...failureNoticeTeamId ? { teamId: failureNoticeTeamId } : {}
2442
+ };
2443
+ if (failureNoticeThreadTs && !explicitlyAddressed && prepared.ctxPayload.MentionSource !== "implicit_thread" && !hasSlackThreadParticipation(notice.accountId, notice.channelId, failureNoticeThreadTs, failureNoticeTeamId)) {
2444
+ logVerbose("slack: suppressed passive failure before thread participation");
2445
+ return null;
2446
+ }
2447
+ if (!explicitlyAddressed && hasSlackThreadFailureNotice(notice)) {
2448
+ logVerbose("slack: suppressed repeated passive channel or thread failure");
2449
+ return null;
2450
+ }
2451
+ pendingFailureNotice = notice;
2452
+ return payload;
2453
+ };
2027
2454
  const deliverSlackPayload = async (payload, info) => {
2028
2455
  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();
2456
+ if (info.kind === "final" && slackStreaming.mode === "progress" && progress.isProgressMode) {
2032
2457
  if (progress.useNativeProgressStreaming) {
2033
- await progress.waitForNativeProgressStreamStart();
2034
- const finalThreadTs = delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs;
2458
+ await progress.deliverNativeFinal(payload, info.kind);
2459
+ return;
2460
+ }
2461
+ progress.progressDraft.markFinalReplyStarted();
2462
+ if (progress.useDraftProgressCard) {
2035
2463
  await delivery.deliverNormally({
2036
2464
  payload,
2037
2465
  kind: info.kind,
2038
- forcedThreadTs: finalThreadTs
2466
+ forcedThreadTs: delivery.usedReplyThreadTs
2039
2467
  });
2040
- await progress.appendNativeProgressCompletion(payload.isError === true);
2468
+ if (!await progress.finalizeDraftProgressCard(payload.isError === true ? "error" : "success")) await draftStream?.clear();
2041
2469
  progress.progressDraft.markFinalReplyDelivered();
2042
- if (!payload.isError && hadProgressDraft && delivery.streamSession) progress.pendingNativeProgressReceipt = progress.progressReceipt.buildSummaryLine();
2043
2470
  return;
2044
2471
  }
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
2472
  }
2067
2473
  if (progress.useNativeProgressStreaming) {
2474
+ if (info.kind !== "final" && payload.isError !== true) {
2475
+ if (!delivery.isStreamingEligible(payload)) {
2476
+ await delivery.deliverNormally({
2477
+ payload,
2478
+ kind: info.kind,
2479
+ forcedThreadTs: delivery.streamSession?.threadTs ?? delivery.nativeProgressStreamThreadTs
2480
+ });
2481
+ return;
2482
+ }
2483
+ return await progress.appendNativeNarration(payload, info.kind) ? void 0 : { visibleReplySent: false };
2484
+ }
2068
2485
  await delivery.deliverNormally({
2069
2486
  payload,
2070
2487
  kind: info.kind,
@@ -2087,61 +2504,9 @@ async function dispatchPreparedSlackMessage(prepared) {
2087
2504
  const requiresSeparateFallbackDelivery = replyRenderPlan.mode === "split";
2088
2505
  const trimmedFinalText = replyRenderPlan.mode === "single" ? replyRenderPlan.text.trim() : replyRenderPlan.fallbackText.trim();
2089
2506
  const previewFinalText = replyRenderPlan.mode === "single" && replyRenderPlan.textIsSlackMrkdwn ? trimmedFinalText : normalizeSlackOutboundText((replySourceText ?? "").trim());
2507
+ const previewFinalTextFitsEdit = countSlackTextUtf8Bytes(previewFinalText) <= SLACK_EDIT_TEXT_MAX_BYTES;
2090
2508
  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) {
2092
- const channelId = draftStream.channelId();
2093
- const messageId = draftStream.messageId();
2094
- if (channelId && messageId) {
2095
- const finalThreadTs = delivery.usedReplyThreadTs ?? statusThreadTs;
2096
- await draftStream.flush();
2097
- await draftStream.seal();
2098
- 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
- });
2109
- } catch (err) {
2110
- logVerbose(`slack: preview final edit failed; falling back to standard send (${formatSlackError(err)})`);
2111
- await draftStream.discardPending();
2112
- let delivered = false;
2113
- try {
2114
- await delivery.deliverNormally({
2115
- payload: payload.text?.trim() ? payload : {
2116
- ...payload,
2117
- text: ttsSupplement.spokenText
2118
- },
2119
- kind: info.kind,
2120
- forcedThreadTs: finalThreadTs
2121
- });
2122
- delivered = true;
2123
- } finally {
2124
- if (delivered) await draftStream.clear();
2125
- }
2126
- return;
2127
- }
2128
- draftPreviewCommitted.value = true;
2129
- delivery.observedFinalReplyDelivery = true;
2130
- delivery.observedReplyDelivery = true;
2131
- replyPlan.markSent();
2132
- await delivery.deliverNormally({
2133
- payload: buildTtsSupplementMediaPayload(payload),
2134
- kind: info.kind,
2135
- forcedThreadTs: finalThreadTs
2136
- });
2137
- delivery.markPreviewPayloadDelivered({
2138
- kind: info.kind,
2139
- payload,
2140
- threadTs: finalThreadTs
2141
- });
2142
- return;
2143
- }
2144
- }
2509
+ let ttsPreviewFinalization;
2145
2510
  await deliverWithFinalizableLivePreviewAdapter({
2146
2511
  kind: info.kind,
2147
2512
  payload,
@@ -2161,7 +2526,7 @@ async function dispatchPreparedSlackMessage(prepared) {
2161
2526
  }
2162
2527
  } : void 0,
2163
2528
  buildFinalEdit: () => {
2164
- if (hasSlackCustomIdentity || !previewStreamingEnabled || reply.hasMedia && !ttsSupplement || payload.isError || requiresSeparateFallbackDelivery || trimmedFinalText.length === 0 && !slackBlocks?.length) return;
2529
+ if (hasSlackCustomIdentity || !previewStreamingEnabled || reply.hasMedia && !ttsSupplement || payload.isError || requiresSeparateFallbackDelivery || !previewFinalTextFitsEdit || trimmedFinalText.length === 0 && !slackBlocks?.length) return;
2165
2530
  return {
2166
2531
  text: previewFinalText,
2167
2532
  blocks: slackBlocks,
@@ -2174,16 +2539,19 @@ async function dispatchPreparedSlackMessage(prepared) {
2174
2539
  payload,
2175
2540
  threadTs: edit.threadTs
2176
2541
  })) 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
- });
2542
+ if (ttsSupplement) ttsPreviewFinalization = { threadTs: edit.threadTs };
2543
+ if (!await draftStream?.finalizeMessage(preview.messageId, async () => {
2544
+ await finalizeSlackPreviewEdit({
2545
+ client: slackClient,
2546
+ token: ctx.botToken,
2547
+ accountId: account.accountId,
2548
+ channelId: preview.channelId,
2549
+ messageId: preview.messageId,
2550
+ text: edit.text,
2551
+ ...edit.blocks?.length ? { blocks: edit.blocks } : {},
2552
+ threadTs: edit.threadTs
2553
+ });
2554
+ })) throw new Error("Slack preview moved below a newer conversation message");
2187
2555
  if (!ttsSupplement) emitSlackMessageSentHooks({
2188
2556
  ...messageSentHookContext,
2189
2557
  to: messageSentHookTarget,
@@ -2227,18 +2595,20 @@ async function dispatchPreparedSlackMessage(prepared) {
2227
2595
  }),
2228
2596
  deliverNormally: async () => {
2229
2597
  await delivery.deliverNormally({
2230
- payload: shouldRestoreTtsSupplementTextForPreviewFallback ? {
2598
+ payload: shouldRestoreTtsSupplementTextForPreviewFallback || ttsPreviewFinalization && !payload.text?.trim() ? {
2231
2599
  ...payload,
2232
2600
  text: ttsSupplement?.spokenText
2233
2601
  } : payload,
2234
- kind: info.kind
2602
+ kind: info.kind,
2603
+ ...ttsPreviewFinalization?.threadTs ? { forcedThreadTs: ttsPreviewFinalization.threadTs } : {}
2235
2604
  });
2236
2605
  }
2237
2606
  });
2607
+ if (info.kind === "final") progress.progressDraft.markFinalReplyDelivered();
2238
2608
  };
2239
2609
  let dispatchError;
2240
- let queuedFinal = false;
2241
- let counts = {};
2610
+ let agentRunFailed = false;
2611
+ let settledDispatchResult;
2242
2612
  try {
2243
2613
  const turnResult = await dispatchChannelInboundTurn({
2244
2614
  cfg,
@@ -2251,6 +2621,10 @@ async function dispatchPreparedSlackMessage(prepared) {
2251
2621
  ctxPayload: prepared.ctxPayload,
2252
2622
  dispatcherOptions: {
2253
2623
  ...replyPipeline,
2624
+ transformReplyPayload: (payload) => {
2625
+ const transformed = replyPipeline.transformReplyPayload ? replyPipeline.transformReplyPayload(payload) : payload;
2626
+ return transformed ? filterPassiveThreadFailure(transformed) : null;
2627
+ },
2254
2628
  humanDelay: resolveHumanDelayConfig(cfg, route.agentId)
2255
2629
  },
2256
2630
  delivery: {
@@ -2272,120 +2646,82 @@ async function dispatchPreparedSlackMessage(prepared) {
2272
2646
  commentaryProgressEnabled: progress.commentaryProgressEnabled ? true : void 0,
2273
2647
  progressPreambleEnabled: progress.progressDraftActive && slackStreaming.mode === "progress" ? true : void 0,
2274
2648
  commentaryPayloadsEnabled: progress.commentaryProgressEnabled ? true : void 0,
2649
+ shouldDeliverCommentaryPayloads: progress.commentaryProgressEnabled ? progress.shouldYieldDraftProgress : void 0,
2275
2650
  onVerboseProgressVisibility: progress.commentaryProgressEnabled ? (isActive) => {
2276
2651
  progress.setShouldYieldDraftProgress(isActive);
2277
2652
  } : void 0,
2278
2653
  allowProgressCallbacksWhenSourceDeliverySuppressed: sourceReplyDeliveryMode === "message_tool_only" && statusReactionsEnabled ? true : void 0,
2279
2654
  allowToolLifecycleWhenProgressHidden: statusReactionsEnabled ? true : void 0,
2280
2655
  onPartialReply: useStreaming ? void 0 : !previewStreamingEnabled ? void 0 : async (payload) => {
2281
- progress.updateDraftFromPartial(payload.text);
2656
+ return progress.updateDraftFromPartial(payload.text);
2282
2657
  },
2283
- onAssistantMessageStart: progress.onDraftBoundary,
2658
+ onAssistantMessageStart: progress.onDraftBoundary ? async () => {
2659
+ await progress.onDraftBoundary?.();
2660
+ return false;
2661
+ } : void 0,
2284
2662
  onReasoningEnd: async () => {
2285
- progress.progressReceipt.closeReasoning();
2286
2663
  await progress.onDraftBoundary?.();
2664
+ return false;
2287
2665
  },
2288
2666
  onQueuedFollowupAdmitted: progress.onQueuedFollowupAdmitted,
2667
+ onQueuedFollowupSettled: progress.onQueuedFollowupSettled,
2289
2668
  onReasoningStream: statusReactionsEnabled || progress.previewToolProgressEnabled ? async (payload) => {
2290
- await progress.pushReasoningProgress(payload);
2291
- if (!statusReactionsEnabled) return;
2669
+ const visible = await progress.pushReasoningProgress(payload);
2670
+ if (!statusReactionsEnabled) return visible;
2292
2671
  await statusReactions.setThinking();
2672
+ return visible;
2293
2673
  } : void 0,
2294
2674
  onToolStart: async (payload) => {
2295
2675
  if (statusReactionsEnabled) await statusReactions.setTool(payload.name);
2296
- 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 });
2676
+ if (payload.phase === "start") progress.progressWorkCounter.noteToolCall(payload.name);
2677
+ return await progress.progressDraft.pushToolEvent(payload);
2305
2678
  },
2306
2679
  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 });
2310
- if (progress.commentaryProgressEnabled) {
2311
- if (await progress.progressDraft.pushCommentaryProgress(payload.progressText, { itemId: payload.itemId })) progress.progressReceipt.noteCommentary(payload.itemId, payload.progressText);
2312
- }
2313
- return;
2680
+ if (progress.isProgressMode && payload.kind === "preamble") {
2681
+ if (progress.shouldYieldDraftProgress()) return false;
2682
+ const headlineVisible = await progress.progressDraft.pushPreambleHeadline(payload.progressText, { itemId: payload.itemId });
2683
+ if (progress.commentaryProgressEnabled) return await progress.progressDraft.pushCommentaryProgress(payload.progressText, { itemId: payload.itemId }) || headlineVisible;
2684
+ return headlineVisible;
2314
2685
  }
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
- }));
2686
+ return await progress.progressDraft.pushItemEvent(payload);
2328
2687
  },
2329
2688
  onPlanUpdate: async (payload) => {
2330
- if (payload.phase !== "update") return;
2331
- await progress.pushPlanProgress(payload.steps, payload.explanation);
2689
+ if (payload.phase !== "update") return false;
2690
+ return await progress.pushPlanProgress(payload.steps, payload.explanation);
2332
2691
  },
2333
2692
  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
- }));
2693
+ return await progress.progressDraft.pushApprovalEvent(payload);
2343
2694
  },
2344
2695
  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
- }));
2696
+ return await progress.progressDraft.pushCommandOutputEvent(payload);
2356
2697
  },
2357
2698
  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
- }));
2699
+ return await progress.progressDraft.pushPatchEvent(payload);
2371
2700
  }
2372
2701
  }
2373
2702
  });
2374
2703
  if (turnResult.dispatched) {
2375
2704
  const result = turnResult.dispatchResult;
2376
- queuedFinal = result.queuedFinal;
2377
- counts = result.counts;
2705
+ settledDispatchResult = result;
2706
+ const agentRunOutcome = readAgentRunTerminalOutcome(result);
2707
+ agentRunFailed = agentRunOutcome === "failed";
2708
+ if (agentRunOutcome === "completed" && !sawTerminalFailurePayload && prepared.ctxPayload.ChatType === "channel") clearSlackThreadFailureNotice({
2709
+ accountId: account.accountId,
2710
+ channelId: message.channel,
2711
+ ...failureNoticeThreadTs ? { threadTs: failureNoticeThreadTs } : {},
2712
+ ...failureNoticeTeamId ? { teamId: failureNoticeTeamId } : {}
2713
+ });
2378
2714
  }
2379
2715
  } catch (err) {
2380
2716
  dispatchError = err;
2381
2717
  } finally {
2382
2718
  progress.progressDraft.cancel();
2383
- await draftStream?.discardPending();
2719
+ if (!progress.useDraftProgressCard) await draftStream?.discardPending();
2384
2720
  }
2385
2721
  let streamFallbackDelivered = false;
2386
2722
  const finalStream = delivery.streamSession;
2387
2723
  if (finalStream && !finalStream.stopped) try {
2388
- const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2724
+ const completionChunks = progress.useNativeProgressStreaming && !progress.nativeProgressCompletionSent ? progress.buildNativeProgressCompletionChunks(dispatchError || agentRunFailed ? "error" : progress.nativeProgressTerminalStatus) : void 0;
2389
2725
  if (completionChunks?.length) progress.nativeProgressCompletionSent = true;
2390
2726
  const stopResult = await stopSlackStream({
2391
2727
  session: finalStream,
@@ -2393,12 +2729,6 @@ async function dispatchPreparedSlackMessage(prepared) {
2393
2729
  ...slackMessageMetadata ? { metadata: slackMessageMetadata } : {}
2394
2730
  });
2395
2731
  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
2732
  } catch (err) {
2403
2733
  if (err instanceof SlackStreamNotDeliveredError) {
2404
2734
  streamFallbackDelivered = await delivery.deliverPendingStreamFallback(finalStream, err);
@@ -2411,50 +2741,45 @@ async function dispatchPreparedSlackMessage(prepared) {
2411
2741
  if (!finalStream.delivered) dispatchError ??= err;
2412
2742
  }
2413
2743
  }
2414
- counts = delivery.reconcileCounts(counts);
2415
- queuedFinal = queuedFinal && (counts.final ?? 0) > 0;
2416
- const anyReplyDelivered = hasVisibleInboundReplyDispatch({
2417
- queuedFinal,
2418
- counts
2419
- }, {
2744
+ const anyReplyDelivered = hasVisibleInboundReplyDispatch(settledDispatchResult, {
2420
2745
  observedReplyDelivery: delivery.observedReplyDelivery,
2421
2746
  fallbackDelivered: streamFallbackDelivered
2422
2747
  });
2423
- if (statusReactionsEnabled) if (dispatchError) await statusReactions.setError();
2424
- else if (anyReplyDelivered) {
2748
+ if (pendingFailureNotice && anyReplyDelivered) recordSlackThreadFailureNotice(pendingFailureNotice);
2749
+ if (dispatchError || agentRunFailed) await progress.finalizeDraftProgressCard("error");
2750
+ await progress.dropDetachedProgressCards();
2751
+ if (statusReactionsEnabled) if (dispatchError || agentRunFailed) {
2752
+ await statusReactions.setError();
2753
+ statusReactions.restoreInitial();
2754
+ } else if (anyReplyDelivered) {
2425
2755
  await statusReactions.setDone();
2426
2756
  statusReactions.restoreInitial();
2427
2757
  } else await statusReactions.restoreInitial();
2428
2758
  const participationThreadTs = delivery.usedReplyThreadTs ?? statusThreadTs;
2429
2759
  if (anyReplyDelivered && participationThreadTs) recordSlackThreadParticipation(account.accountId, message.channel, participationThreadTs, {
2430
2760
  agentId: route.agentId,
2431
- ...prepared.eventScope ? { teamId: prepared.eventScope.teamId } : {}
2761
+ teamId: prepared.eventScope?.teamId
2432
2762
  });
2433
2763
  if (dispatchError) throw toErrorObject(dispatchError, "Slack dispatch failed");
2434
- if (!anyReplyDelivered && !draftPreviewCommitted.value) {
2764
+ if (!anyReplyDelivered && !draftPreviewCommitted.value && !(agentRunFailed && progress.useDraftProgressCard)) {
2435
2765
  await draftStream?.clear();
2766
+ await draftStream?.dropDetachedMessages();
2436
2767
  return;
2437
2768
  }
2438
2769
  if (shouldLogVerbose()) {
2439
- const finalCount = counts.final;
2770
+ const finalCount = resolveInboundReplyDispatchCounts(settledDispatchResult).final;
2440
2771
  logVerbose(`slack: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${prepared.replyTarget}`);
2441
2772
  }
2442
2773
  }
2443
2774
  //#endregion
2444
2775
  //#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
2776
  function normalizeEntity(value) {
2452
- const entity = asRecord(value);
2453
- const type = trimmedString(entity?.type);
2777
+ const entity = asOptionalRecord(value);
2778
+ const type = normalizeOptionalString(entity?.type);
2454
2779
  if (!entity || !type) return;
2455
- const teamId = trimmedString(entity.team_id);
2780
+ const teamId = normalizeOptionalString(entity.team_id);
2456
2781
  if (type === "slack#/types/channel_id" || type === "slack#/types/canvas_id" || type === "slack#/types/list_id") {
2457
- const entityValue = trimmedString(entity.value);
2782
+ const entityValue = normalizeOptionalString(entity.value);
2458
2783
  return entityValue ? {
2459
2784
  type,
2460
2785
  value: entityValue,
@@ -2462,9 +2787,9 @@ function normalizeEntity(value) {
2462
2787
  } : void 0;
2463
2788
  }
2464
2789
  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);
2790
+ const message = asOptionalRecord(entity.value);
2791
+ const channelId = normalizeOptionalString(message?.channel_id);
2792
+ const messageTs = normalizeOptionalString(message?.message_ts);
2468
2793
  return channelId && messageTs ? {
2469
2794
  type,
2470
2795
  value: {
@@ -2475,10 +2800,10 @@ function normalizeEntity(value) {
2475
2800
  } : void 0;
2476
2801
  }
2477
2802
  function isSlackAppContext(value) {
2478
- return Boolean(asRecord(value));
2803
+ return Boolean(asOptionalRecord(value));
2479
2804
  }
2480
2805
  function normalizeSlackAppContextEntities(value) {
2481
- const context = asRecord(value);
2806
+ const context = asOptionalRecord(value);
2482
2807
  if (!Array.isArray(context?.entities)) return [];
2483
2808
  return context.entities.flatMap((entity) => {
2484
2809
  const normalized = normalizeEntity(entity);
@@ -2486,89 +2811,194 @@ function normalizeSlackAppContextEntities(value) {
2486
2811
  });
2487
2812
  }
2488
2813
  //#endregion
2814
+ //#region extensions/slack/src/monitor/thread.ts
2815
+ const THREAD_STARTER_CACHE = /* @__PURE__ */ new Map();
2816
+ const THREAD_STARTER_CACHE_TTL_MS = 360 * 6e4;
2817
+ const THREAD_STARTER_CACHE_MAX = 2e3;
2818
+ function evictThreadStarterCache() {
2819
+ const now = asDateTimestampMs(Date.now());
2820
+ if (now === void 0) {
2821
+ THREAD_STARTER_CACHE.clear();
2822
+ return;
2823
+ }
2824
+ for (const [cacheKey, entry] of THREAD_STARTER_CACHE.entries()) if (asDateTimestampMs(entry.expiresAt) === void 0 || entry.expiresAt <= now) THREAD_STARTER_CACHE.delete(cacheKey);
2825
+ pruneMapToMaxSize(THREAD_STARTER_CACHE, THREAD_STARTER_CACHE_MAX);
2826
+ }
2827
+ function formatSlackFilePlaceholder(files) {
2828
+ return `[attached: ${formatSlackFileReferenceList(files)}]`;
2829
+ }
2830
+ function pushUniqueText(parts, value, options = {}) {
2831
+ const text = options.preserveWhitespace ? typeof value === "string" && value.trim().length > 0 ? value : void 0 : normalizeOptionalString(value);
2832
+ if (text && !parts.includes(text)) parts.push(text);
2833
+ }
2834
+ function resolveSlackBlocksFallbackText(blocks) {
2835
+ return resolveSlackBlocksText(blocks)?.text;
2836
+ }
2837
+ function resolveSlackAttachmentFallbackText(attachments) {
2838
+ if (!Array.isArray(attachments) || attachments.length === 0) return;
2839
+ const parts = [];
2840
+ for (const attachment of attachments) {
2841
+ const excludeTableBlocks = isSlackUnfurlAttachment(attachment);
2842
+ const fallbackBlocks = (blocks) => excludeTableBlocks ? blocks?.filter((block) => !hasSlackTableBlock([block])) : blocks;
2843
+ pushUniqueText(parts, attachment.pretext);
2844
+ pushUniqueText(parts, attachment.title);
2845
+ pushUniqueText(parts, attachment.text);
2846
+ if (!(hasSlackTableBlock(attachment.blocks) && normalizeOptionalString(attachment.fallback) === "[no preview available]")) pushUniqueText(parts, attachment.fallback);
2847
+ for (const field of attachment.fields ?? []) {
2848
+ pushUniqueText(parts, field.title);
2849
+ pushUniqueText(parts, field.value);
2850
+ }
2851
+ pushUniqueText(parts, resolveSlackBlocksFallbackText(fallbackBlocks(attachment.blocks)), { preserveWhitespace: true });
2852
+ pushUniqueText(parts, resolveSlackBlocksFallbackText(fallbackBlocks(attachment.message_blocks)), { preserveWhitespace: true });
2853
+ }
2854
+ return parts.length > 0 ? parts.join("\n") : void 0;
2855
+ }
2856
+ function resolveSlackMessageText(message) {
2857
+ const messageText = normalizeOptionalString(message.text) ?? resolveSlackAttachmentFallbackText(message.attachments);
2858
+ return resolveSlackMessageText$1({
2859
+ ...message,
2860
+ text: messageText
2861
+ }, { preserveMessageTextWhitespace: true });
2862
+ }
2863
+ async function resolveSlackThreadStarter(params) {
2864
+ evictThreadStarterCache();
2865
+ const cacheKey = JSON.stringify([
2866
+ params.workspaceScope.accountId,
2867
+ params.workspaceScope.teamId,
2868
+ params.channelId,
2869
+ params.threadTs
2870
+ ]);
2871
+ const cached = THREAD_STARTER_CACHE.get(cacheKey);
2872
+ if (cached) {
2873
+ const now = asDateTimestampMs(Date.now());
2874
+ if (now !== void 0 && cached.expiresAt > now) return cached.value;
2875
+ THREAD_STARTER_CACHE.delete(cacheKey);
2876
+ }
2877
+ try {
2878
+ const message = (await params.client.conversations.replies({
2879
+ channel: params.channelId,
2880
+ ts: params.threadTs,
2881
+ limit: 1,
2882
+ inclusive: true
2883
+ }))?.messages?.[0];
2884
+ const text = message ? resolveSlackMessageText(message) : void 0;
2885
+ const files = message?.files?.length ? message.files : void 0;
2886
+ if (!message || !text && !files) return null;
2887
+ const starter = {
2888
+ text: text || formatSlackFilePlaceholder(files),
2889
+ userId: message.user,
2890
+ botId: message.bot_id,
2891
+ ts: message.ts,
2892
+ files
2893
+ };
2894
+ const expiresAt = resolveExpiresAtMsFromDurationMs(THREAD_STARTER_CACHE_TTL_MS);
2895
+ if (expiresAt !== void 0) {
2896
+ if (THREAD_STARTER_CACHE.has(cacheKey)) THREAD_STARTER_CACHE.delete(cacheKey);
2897
+ THREAD_STARTER_CACHE.set(cacheKey, {
2898
+ value: starter,
2899
+ expiresAt
2900
+ });
2901
+ evictThreadStarterCache();
2902
+ }
2903
+ return starter;
2904
+ } catch (err) {
2905
+ logVerbose$1(`slack thread starter fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`);
2906
+ return null;
2907
+ }
2908
+ }
2909
+ const SLACK_THREAD_HISTORY_MAX_PAGES = 3;
2910
+ /**
2911
+ * Fetches the most recent messages in a Slack thread (excluding the current message).
2912
+ * Used to populate thread context when a new thread session starts.
2913
+ *
2914
+ * Uses cursor pagination and keeps only the latest N retained messages when the full
2915
+ * thread fits in the bounded fetch window.
2916
+ */
2917
+ async function resolveSlackThreadHistory(params) {
2918
+ const maxMessages = params.limit ?? 20;
2919
+ if (!Number.isFinite(maxMessages) || maxMessages <= 0) return [];
2920
+ const fetchLimit = 200;
2921
+ const retained = [];
2922
+ let cursor;
2923
+ let pagesFetched = 0;
2924
+ try {
2925
+ do {
2926
+ pagesFetched += 1;
2927
+ const response = await params.client.conversations.replies({
2928
+ channel: params.channelId,
2929
+ ts: params.threadTs,
2930
+ limit: fetchLimit,
2931
+ inclusive: true,
2932
+ ...cursor ? { cursor } : {}
2933
+ });
2934
+ for (const msg of response.messages ?? []) {
2935
+ const text = resolveSlackMessageText(msg);
2936
+ if (!text && !msg.files?.length) continue;
2937
+ if (params.currentMessageTs && msg.ts === params.currentMessageTs) continue;
2938
+ retained.push([msg, text]);
2939
+ }
2940
+ if (retained.length > maxMessages) retained.splice(0, retained.length - maxMessages);
2941
+ const next = response.response_metadata?.next_cursor;
2942
+ cursor = typeof next === "string" && next.trim().length > 0 ? next.trim() : void 0;
2943
+ } while (cursor && pagesFetched < SLACK_THREAD_HISTORY_MAX_PAGES);
2944
+ if (cursor) {
2945
+ logVerbose$1(`slack thread history capped channel=${params.channelId} ts=${params.threadTs} pages=${SLACK_THREAD_HISTORY_MAX_PAGES}`);
2946
+ return [];
2947
+ }
2948
+ return retained.map(([message, text]) => ({
2949
+ text: text ?? formatSlackFilePlaceholder(message.files),
2950
+ userId: message.user,
2951
+ botId: message.bot_id,
2952
+ ts: message.ts,
2953
+ files: message.files
2954
+ }));
2955
+ } catch (err) {
2956
+ logVerbose$1(`slack thread history fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`);
2957
+ return [];
2958
+ }
2959
+ }
2960
+ //#endregion
2489
2961
  //#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
2962
  function isSlackAudioFile(file) {
2493
2963
  if (file.subtype === "slack_audio") return true;
2494
2964
  if ((file.mimetype?.split(";")[0]?.trim().toLowerCase())?.startsWith("audio/")) return true;
2495
2965
  return Boolean(mimeTypeFromFilePath(file.name)?.startsWith("audio/"));
2496
2966
  }
2967
+ const slackPreflightAudio = createChannelPreflightAudio({
2968
+ channel: "slack",
2969
+ isAudio: isSlackAudioFile
2970
+ });
2497
2971
  function findCaptionlessSlackAudioFile(message) {
2498
2972
  if (message.text?.trim()) return;
2499
2973
  return message.files?.slice(0, 8).find(isSlackAudioFile);
2500
2974
  }
2501
2975
  function formatSlackAudioTranscriptForAgent(params) {
2502
- return [`[Audio transcript (machine-generated, untrusted)]: ${JSON.stringify(params.transcript)}`, params.rawBody].filter(Boolean).join("\n");
2503
- }
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
- };
2976
+ return [formatAudioTranscriptForAgent(params.transcript), params.rawBody].filter(Boolean).join("\n");
2520
2977
  }
2521
2978
  async function resolveSlackPreflightAudioTranscript(params) {
2522
2979
  const mediaIndex = params.media.findIndex((entry) => entry.contentType?.toLowerCase().startsWith("audio/"));
2523
2980
  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);
2981
+ const transcript = await slackPreflightAudio.resolve({ request: {
2982
+ ctx: {
2983
+ media: [...params.media],
2984
+ Provider: "slack",
2985
+ Surface: "slack",
2986
+ OriginatingChannel: "slack",
2987
+ OriginatingTo: params.originatingTo,
2988
+ AccountId: params.accountId,
2989
+ MessageThreadId: params.messageThreadId,
2990
+ ChatType: "channel",
2991
+ SessionKey: params.sessionKey
2992
+ },
2993
+ cfg: params.cfg
2994
+ } });
2995
+ return transcript ? {
2996
+ transcript,
2997
+ mediaIndex
2998
+ } : null;
2551
2999
  }
2552
3000
  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
- }
3001
+ await slackPreflightAudio.send(params);
2572
3002
  }
2573
3003
  async function discardSlackPreflightMedia(media) {
2574
3004
  await Promise.allSettled((media ?? []).map((entry) => fs.rm(entry.path, { force: true })));
@@ -2578,7 +3008,7 @@ async function discardSlackPreflightMedia(media) {
2578
3008
  const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4;
2579
3009
  const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20;
2580
3010
  const SLACK_USER_MENTION_RE$1 = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi;
2581
- const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-D3NuJvva.js").then((n) => n.g));
3011
+ const loadSlackMediaModule$1 = createLazyRuntimeModule(() => import("./actions-DFk0ANVB.js").then((n) => n._));
2582
3012
  function collectUniqueSlackMentionIds$1(texts) {
2583
3013
  const seen = /* @__PURE__ */ new Set();
2584
3014
  const mentionIds = [];
@@ -2639,7 +3069,14 @@ async function resolveSlackMessageContent(params) {
2639
3069
  const mergedMedia = [...media ?? [], ...attachmentContent?.media ?? []];
2640
3070
  const effectiveDirectMedia = mergedMedia.length > 0 ? mergedMedia : null;
2641
3071
  const mediaPlaceholder = effectiveDirectMedia ? effectiveDirectMedia.map((item) => item.placeholder).join(" ") : void 0;
2642
- const fallbackFiles = ownFiles ?? [];
3072
+ const fallbackFileIds = /* @__PURE__ */ new Set();
3073
+ const fallbackFiles = [...ownFiles ?? [], ...attachmentContent?.files ?? []].filter((file) => {
3074
+ const fileId = normalizeOptionalString(file.id);
3075
+ if (!fileId) return true;
3076
+ if (fallbackFileIds.has(fileId)) return false;
3077
+ fallbackFileIds.add(fileId);
3078
+ return true;
3079
+ });
2643
3080
  const fileOnlyFallback = !mediaPlaceholder && fallbackFiles.length > 0 ? fallbackFiles.slice(0, 8).map((file) => formatSlackFileReference(file)).join(", ") : void 0;
2644
3081
  const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : void 0;
2645
3082
  let botAttachmentText;
@@ -2652,7 +3089,7 @@ async function resolveSlackMessageContent(params) {
2652
3089
  botAttachmentText = botAttachmentTextParts.length > 0 ? botAttachmentTextParts.join("\n") : void 0;
2653
3090
  }
2654
3091
  const textParts = [
2655
- resolveSlackMessageText(params.message),
3092
+ resolveSlackMessageText$1(params.message),
2656
3093
  attachmentContent?.text,
2657
3094
  botAttachmentText
2658
3095
  ];
@@ -2678,13 +3115,18 @@ async function resolveSlackMessageContent(params) {
2678
3115
  renderedMentions.set(result.userId, result.rendered);
2679
3116
  }
2680
3117
  }
2681
- const rawBody = [
3118
+ let rawBody = [
2682
3119
  renderSlackUserMentions(textParts[0], renderedMentions),
2683
3120
  renderSlackUserMentions(textParts[1], renderedMentions),
2684
3121
  renderSlackUserMentions(textParts[2], renderedMentions),
2685
3122
  mediaPlaceholder,
2686
3123
  fileOnlyPlaceholder
2687
3124
  ].filter(Boolean).join("\n") || "";
3125
+ const unavailableImageCount = attachmentContent?.unavailableImageCount ?? 0;
3126
+ if (unavailableImageCount > 0) rawBody = formatInboundMediaUnavailableText({
3127
+ body: rawBody,
3128
+ notice: `[slack ${unavailableImageCount > 1 ? `${unavailableImageCount} forwarded images` : "forwarded image"} unavailable]`
3129
+ });
2688
3130
  if (!rawBody) return null;
2689
3131
  return {
2690
3132
  rawBody,
@@ -2765,68 +3207,8 @@ async function resolveSlackDmHistoryContext(params) {
2765
3207
  }
2766
3208
  //#endregion
2767
3209
  //#region extensions/slack/src/monitor/message-handler/prepare-routing.ts
2768
- const slackRouteBindingConfigCache = /* @__PURE__ */ new WeakMap();
2769
- function slackTargetDefaultKindForPeer(kind) {
2770
- return kind === "direct" ? "user" : "channel";
2771
- }
2772
- function slackTargetKindMatchesPeer(peerKind, targetKind) {
2773
- if (targetKind === "user") return peerKind === "direct";
2774
- return peerKind === "channel" || peerKind === "group";
2775
- }
2776
- function normalizeSlackRouteBindingPeer(peer) {
2777
- const rawId = peer.id.trim();
2778
- if (!rawId || rawId === "*") return peer;
2779
- const target = (() => {
2780
- try {
2781
- return parseSlackTarget(rawId, { defaultKind: slackTargetDefaultKindForPeer(peer.kind) });
2782
- } catch {
2783
- return;
2784
- }
2785
- })();
2786
- if (!target || !slackTargetKindMatchesPeer(peer.kind, target.kind) || target.id === peer.id) return peer;
2787
- return {
2788
- ...peer,
2789
- id: target.id
2790
- };
2791
- }
2792
- function normalizeSlackRouteBindingConfig(cfg) {
2793
- const bindings = cfg.bindings;
2794
- const cached = slackRouteBindingConfigCache.get(cfg);
2795
- if (cached && cached.bindingsRef === bindings) return cached.normalizedCfg;
2796
- if (!Array.isArray(bindings)) return cfg;
2797
- let changed = false;
2798
- const normalizedBindings = bindings.map((binding) => {
2799
- if (binding.type === "acp" || binding.match.channel.trim().toLowerCase() !== "slack") return binding;
2800
- const peer = binding.match.peer;
2801
- if (!peer) return binding;
2802
- const normalizedPeer = normalizeSlackRouteBindingPeer(peer);
2803
- if (normalizedPeer === peer) return binding;
2804
- changed = true;
2805
- return {
2806
- ...binding,
2807
- match: {
2808
- ...binding.match,
2809
- peer: normalizedPeer
2810
- }
2811
- };
2812
- });
2813
- const normalizedCfg = changed ? {
2814
- ...cfg,
2815
- bindings: normalizedBindings
2816
- } : cfg;
2817
- slackRouteBindingConfigCache.set(cfg, {
2818
- bindingsRef: bindings,
2819
- normalizedCfg
2820
- });
2821
- return normalizedCfg;
2822
- }
2823
3210
  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)}`;
3211
+ return qualifySlackConversationId(params.isDirectMessage ? `user:${params.message.user ?? "unknown"}` : params.message.channel, params.eventScope);
2830
3212
  }
2831
3213
  function resolveSlackInitialAgentRoute(params) {
2832
3214
  const route = resolveAgentRoute({
@@ -2836,7 +3218,7 @@ function resolveSlackInitialAgentRoute(params) {
2836
3218
  teamId: params.eventScope?.teamId || params.ctx.teamId || void 0,
2837
3219
  peer: {
2838
3220
  kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
2839
- id: qualifySlackPeerId({
3221
+ id: qualifySlackRoutePeerId({
2840
3222
  id: params.isDirectMessage ? params.message.user ?? "unknown" : params.message.channel,
2841
3223
  kind: params.isDirectMessage ? "user" : "channel",
2842
3224
  eventScope: params.eventScope
@@ -2844,8 +3226,11 @@ function resolveSlackInitialAgentRoute(params) {
2844
3226
  }
2845
3227
  });
2846
3228
  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}`;
3229
+ const sessionKey = resolveSlackEnterpriseMainDmSessionKey({
3230
+ baseSessionKey: route.sessionKey,
3231
+ accountId: params.account.accountId,
3232
+ eventScope: params.eventScope
3233
+ });
2849
3234
  return {
2850
3235
  ...route,
2851
3236
  sessionKey,
@@ -2880,44 +3265,18 @@ function resolveSlackRoutingContext(params) {
2880
3265
  eventScope
2881
3266
  });
2882
3267
  const runtimeBindingThreadId = routedThreadId ?? (isDirectMessage && isThreadReply ? threadTs : void 0);
2883
- const boundThreadRoute = !eventScope && runtimeBindingThreadId ? resolveRuntimeConversationBindingRoute({
2884
- route,
2885
- conversation: {
2886
- channel: "slack",
2887
- accountId: account.accountId,
2888
- conversationId: runtimeBindingThreadId,
2889
- parentConversationId: baseConversationId
2890
- }
2891
- }) : null;
2892
- const runtimeRoute = eventScope ? {
2893
- route,
2894
- bindingRecord: null,
2895
- boundSessionKey: void 0
2896
- } : boundThreadRoute?.boundSessionKey || boundThreadRoute?.bindingRecord ? boundThreadRoute : resolveRuntimeConversationBindingRoute({
3268
+ const bindingRoute = resolveSlackConversationBindingRoute({
3269
+ cfg: ctx.cfg,
2897
3270
  route,
2898
- conversation: {
2899
- channel: "slack",
2900
- accountId: account.accountId,
2901
- conversationId: baseConversationId
2902
- }
3271
+ accountId: account.accountId,
3272
+ baseConversationId,
3273
+ runtimeBindingThreadId,
3274
+ bindingsEnabled: !eventScope
2903
3275
  });
2904
- let configuredBinding = null;
2905
- let configuredBindingSessionKey = "";
2906
- if (runtimeRoute.boundSessionKey || runtimeRoute.bindingRecord) route = runtimeRoute.route;
2907
- else if (!eventScope) {
2908
- const configuredRoute = resolveConfiguredBindingRoute({
2909
- cfg: ctx.cfg,
2910
- route,
2911
- conversation: {
2912
- channel: "slack",
2913
- accountId: account.accountId,
2914
- conversationId: baseConversationId
2915
- }
2916
- });
2917
- configuredBinding = configuredRoute.bindingResolution;
2918
- configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
2919
- route = configuredRoute.route;
2920
- }
3276
+ const runtimeRoute = bindingRoute.runtimeRoute;
3277
+ const configuredBinding = bindingRoute.configuredRoute?.bindingResolution ?? null;
3278
+ const configuredBindingSessionKey = bindingRoute.configuredRoute?.boundSessionKey ?? "";
3279
+ route = bindingRoute.route;
2921
3280
  const threadKeys = runtimeRoute.boundSessionKey || configuredBindingSessionKey ? {
2922
3281
  sessionKey: route.sessionKey,
2923
3282
  parentSessionKey: void 0
@@ -2953,8 +3312,12 @@ function isSlackThreadAuthorCurrentBot(params) {
2953
3312
  return false;
2954
3313
  }
2955
3314
  function resolveSlackThreadHistoryFilterPolicy(params) {
2956
- if (!params.includeBotStarterAsRootContext || !params.starterTs) return {};
2957
- return { retainCurrentBotRootTs: params.starterTs };
3315
+ if (params.retainCurrentBotHistory) return { currentBot: "all" };
3316
+ if (!params.includeBotStarterAsRootContext || !params.starterTs) return { currentBot: "omit" };
3317
+ return {
3318
+ currentBot: "root-only",
3319
+ rootTs: params.starterTs
3320
+ };
2958
3321
  }
2959
3322
  function applySlackThreadHistoryFilterPolicy(params) {
2960
3323
  const kept = [];
@@ -2967,7 +3330,7 @@ function applySlackThreadHistoryFilterPolicy(params) {
2967
3330
  kept.push(entry);
2968
3331
  continue;
2969
3332
  }
2970
- if (params.policy.retainCurrentBotRootTs && entry.ts === params.policy.retainCurrentBotRootTs) kept.push(entry);
3333
+ if (params.policy.currentBot === "all" || params.policy.currentBot === "root-only" && entry.ts === params.policy.rootTs) kept.push(entry);
2971
3334
  else omittedCurrentBot += 1;
2972
3335
  }
2973
3336
  return {
@@ -2996,11 +3359,11 @@ function formatSlackThreadLabelSnippet(text) {
2996
3359
  }
2997
3360
  //#endregion
2998
3361
  //#region extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
2999
- const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-D3NuJvva.js").then((n) => n.g));
3362
+ const loadSlackMediaModule = createLazyRuntimeModule(() => import("./actions-DFk0ANVB.js").then((n) => n._));
3000
3363
  const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4;
3001
3364
  function resolveSlackThreadSessionFreshness(params) {
3002
3365
  return params.ctx.channelRuntime?.session?.resolveEntryResetFreshness?.({
3003
- defaultAgentId: resolveDefaultAgentId(params.ctx.cfg),
3366
+ agentId: params.agentId,
3004
3367
  storePath: params.storePath,
3005
3368
  sessionKey: params.sessionKey,
3006
3369
  sessionCfg: params.ctx.cfg.session,
@@ -3066,6 +3429,7 @@ async function resolveSlackThreadContextData(params) {
3066
3429
  let threadStarterMedia = null;
3067
3430
  const threadSessionFreshness = params.isThreadReply && params.threadTs ? resolveSlackThreadSessionFreshness({
3068
3431
  ctx: params.ctx,
3432
+ agentId: params.agentId,
3069
3433
  storePath: params.storePath,
3070
3434
  sessionKey: params.sessionKey
3071
3435
  }) : void 0;
@@ -3073,7 +3437,9 @@ async function resolveSlackThreadContextData(params) {
3073
3437
  storePath: params.storePath,
3074
3438
  sessionKey: params.sessionKey
3075
3439
  }) : void 0;
3076
- const shouldSeedInitialThreadContext = Boolean(params.isThreadReply && params.threadTs && (threadSessionFreshness ? threadSessionFreshness.state !== "fresh" : threadSessionPreviousTimestamp === void 0));
3440
+ const isMissingThreadSession = threadSessionFreshness ? threadSessionFreshness.state === "missing" : threadSessionPreviousTimestamp === void 0;
3441
+ const isOutboundOnlyThreadSession = threadSessionFreshness !== void 0 && threadSessionFreshness.state !== "missing" && threadSessionFreshness.entry.lastInteractionAt === void 0 && threadSessionFreshness.entry.updatedAt !== 0;
3442
+ const shouldSeedInitialThreadContext = Boolean(params.isThreadReply && params.threadTs && (threadSessionFreshness ? threadSessionFreshness.state !== "fresh" || isOutboundOnlyThreadSession : threadSessionPreviousTimestamp === void 0));
3077
3443
  const shouldLoadInitialThreadHistory = shouldSeedInitialThreadContext || params.forceInitialHistory === true;
3078
3444
  if (!params.isThreadReply || !params.threadTs) return {
3079
3445
  threadStarterBody,
@@ -3151,7 +3517,8 @@ async function resolveSlackThreadContextData(params) {
3151
3517
  history: threadHistoryWithBotRoot,
3152
3518
  policy: resolveSlackThreadHistoryFilterPolicy({
3153
3519
  includeBotStarterAsRootContext,
3154
- starterTs: currentBotRootTs
3520
+ starterTs: currentBotRootTs,
3521
+ retainCurrentBotHistory: params.isGroupDm && (isMissingThreadSession || isOutboundOnlyThreadSession)
3155
3522
  }),
3156
3523
  identity: botIdentity
3157
3524
  });
@@ -3298,6 +3665,13 @@ const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4;
3298
3665
  const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
3299
3666
  const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1e3;
3300
3667
  const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3e3;
3668
+ const SLACK_CHANNEL_ACCESS_DOCS_URL = "https://docs.openclaw.ai/channels/slack#access-control-and-routing";
3669
+ function resolveSlackGroupSessionSubject(params) {
3670
+ const channelName = normalizeOptionalString(params.channelName);
3671
+ const workspaceName = normalizeOptionalString(params.workspaceName);
3672
+ if (channelName && workspaceName) return `${workspaceName} #${channelName}`;
3673
+ return `Slack Channel (Workspace ID: ${params.workspaceId}, Channel ID: ${params.channelId})`;
3674
+ }
3301
3675
  function recordString(record, key) {
3302
3676
  return normalizeOptionalString(record?.[key]);
3303
3677
  }
@@ -3504,6 +3878,8 @@ async function resolveSlackConversationContext(params) {
3504
3878
  const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
3505
3879
  const isRoomish = isRoom || isGroupDm;
3506
3880
  const channelConfig = isRoom ? resolveSlackChannelConfig({
3881
+ teamId: params.eventScope?.teamId ?? ctx.teamId,
3882
+ allowUnscoped: ctx.installationIdentity?.kind !== "enterprise",
3507
3883
  channelId: message.channel,
3508
3884
  channelName,
3509
3885
  channels: ctx.channelsConfig,
@@ -3545,14 +3921,38 @@ async function authorizeSlackInboundMessage(params) {
3545
3921
  return null;
3546
3922
  }
3547
3923
  if (!ctx.isChannelAllowed({
3924
+ teamId: params.eventScope?.teamId ?? ctx.teamId,
3548
3925
  channelId: message.channel,
3549
3926
  channelName,
3550
3927
  channelType: resolvedChannelType
3551
3928
  })) {
3929
+ if (conversation.isRoom && ctx.groupPolicy === "allowlist" && params.explicitBotMention && !isBotMessage && message.user) {
3930
+ let subject = "This OpenClaw bot";
3931
+ if (ctx.botUserId) try {
3932
+ const botName = normalizeOptionalString((await ctx.resolveUserName(ctx.botUserId, params.eventScope))?.name);
3933
+ if (botName) subject = escapeSlackMrkdwn(botName);
3934
+ } catch (error) {
3935
+ logVerbose(`slack allowlist denial bot-name lookup failed: ${formatSlackError(error)}`);
3936
+ }
3937
+ try {
3938
+ await (params.eventScope?.client ?? ctx.app.client).chat.postEphemeral({
3939
+ token: ctx.botToken,
3940
+ channel: message.channel,
3941
+ user: message.user,
3942
+ 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.>`
3943
+ });
3944
+ params.onVisibleDrop?.();
3945
+ } catch (error) {
3946
+ ctx.runtime.error?.(`slack allowlist denial notice failed for channel ${message.channel}: ${formatSlackError(error)}`);
3947
+ }
3948
+ }
3552
3949
  logVerbose("slack: drop message (channel not allowed)");
3553
3950
  return null;
3554
3951
  }
3555
- const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, { includePairingStore: isDirectMessage });
3952
+ const allowFromLower = await resolveSlackEffectiveAllowFrom(ctx, {
3953
+ includePairingStore: isDirectMessage,
3954
+ eventScope: params.eventScope
3955
+ });
3556
3956
  if (isDirectMessage) {
3557
3957
  const directUserId = message.user;
3558
3958
  if (!directUserId) {
@@ -3563,6 +3963,7 @@ async function authorizeSlackInboundMessage(params) {
3563
3963
  ctx,
3564
3964
  accountId: account.accountId,
3565
3965
  senderId: directUserId,
3966
+ eventScope: params.eventScope,
3566
3967
  allowFromLower,
3567
3968
  resolveSenderName: (userId) => ctx.resolveUserName(userId, params.eventScope),
3568
3969
  sendPairingReply: async (text) => {
@@ -3570,7 +3971,8 @@ async function authorizeSlackInboundMessage(params) {
3570
3971
  cfg: ctx.cfg,
3571
3972
  token: ctx.botToken,
3572
3973
  client: params.eventScope?.client ?? ctx.app.client,
3573
- accountId: account.accountId
3974
+ accountId: account.accountId,
3975
+ eventScope: params.eventScope
3574
3976
  });
3575
3977
  },
3576
3978
  onDisabled: () => {
@@ -3590,10 +3992,10 @@ async function authorizeSlackInboundMessage(params) {
3590
3992
  async function prepareSlackMessage(params) {
3591
3993
  const { ctx, account, message, opts } = params;
3592
3994
  const slackClient = opts.eventScope?.client ?? ctx.app.client;
3593
- const threadStarterWorkspaceScope = opts.eventScope ? {
3995
+ const threadStarterWorkspaceScope = {
3594
3996
  accountId: account.accountId,
3595
- teamId: opts.eventScope.teamId
3596
- } : void 0;
3997
+ teamId: opts.eventScope?.teamId ?? ctx.teamId
3998
+ };
3597
3999
  const cfg = ctx.cfg;
3598
4000
  const conversation = await resolveSlackConversationContext({
3599
4001
  ctx,
@@ -3602,16 +4004,20 @@ async function prepareSlackMessage(params) {
3602
4004
  eventScope: opts.eventScope
3603
4005
  });
3604
4006
  const { channelInfo, channelName, isDirectMessage, isGroupDm, isRoom, isRoomish, channelConfig, allowBotsMode, isBotMessage } = conversation;
4007
+ const messageText = message.text ?? "";
4008
+ const mentionMetadata = collectSlackMentionMetadata(messageText);
4009
+ const normalizedBotUserId = normalizeSlackId(ctx.botUserId);
3605
4010
  const authorization = await authorizeSlackInboundMessage({
3606
4011
  ctx,
3607
4012
  account,
3608
4013
  message,
3609
4014
  conversation,
3610
- eventScope: opts.eventScope
4015
+ explicitBotMention: opts.source === "app_mention" || Boolean(normalizedBotUserId && mentionMetadata.mentionedUserIds.includes(normalizedBotUserId)),
4016
+ eventScope: opts.eventScope,
4017
+ onVisibleDrop: opts.onVisibleDrop
3611
4018
  });
3612
4019
  if (!authorization) return null;
3613
4020
  const { senderId, allowFromLower } = authorization;
3614
- const messageText = message.text ?? "";
3615
4021
  let resolvedSenderName = normalizeOptionalString(message.username);
3616
4022
  const resolveSenderName = async () => {
3617
4023
  if (resolvedSenderName) return resolvedSenderName;
@@ -3625,7 +4031,6 @@ async function prepareSlackMessage(params) {
3625
4031
  resolvedSenderName = message.user ?? message.bot_id ?? "unknown";
3626
4032
  return resolvedSenderName;
3627
4033
  };
3628
- const mentionMetadata = collectSlackMentionMetadata(messageText);
3629
4034
  const { mentionedUserIds, mentionedSubteamIds, hasAnyMention } = mentionMetadata;
3630
4035
  const messageAssistantThreadContext = resolveSlackMessageAssistantThreadContext(message);
3631
4036
  const assistantContextLookupChannelId = messageAssistantThreadContext?.assistantChannelId ?? message.channel;
@@ -3688,7 +4093,11 @@ async function prepareSlackMessage(params) {
3688
4093
  }));
3689
4094
  const buildPolicyMentionRegexes = (agentId) => resolveCachedMentionRegexes(ctx, agentId, {
3690
4095
  provider: "slack",
3691
- conversationId: message.channel,
4096
+ conversationId: formatSlackTarget({
4097
+ teamId: opts.eventScope?.teamId,
4098
+ kind: "channel",
4099
+ id: message.channel
4100
+ }),
3692
4101
  providerPolicy: account.config.mentionPatterns
3693
4102
  });
3694
4103
  let mentionRegexes = buildPolicyMentionRegexes(routing.route.agentId);
@@ -3729,13 +4138,13 @@ async function prepareSlackMessage(params) {
3729
4138
  name: void 0
3730
4139
  });
3731
4140
  let implicitMentionKinds = [];
3732
- if (!isDirectMessage && ctx.botUserId && message.thread_ts && !wasMentioned) {
3733
- const replyToBotKinds = implicitMentionKindWhen("reply_to_bot", message.parent_user_id === ctx.botUserId);
4141
+ if (!isDirectMessage && message.thread_ts && !wasMentioned) {
4142
+ const replyToBotKinds = implicitMentionKindWhen("reply_to_bot", Boolean(ctx.botUserId && message.parent_user_id === ctx.botUserId));
3734
4143
  implicitMentionKinds = replyToBotKinds.length > 0 ? replyToBotKinds : implicitMentionKindWhen("bot_thread_participant", await hasSlackThreadParticipationWithPersistence({
3735
4144
  accountId: account.accountId,
3736
4145
  channelId: message.channel,
3737
4146
  threadTs: message.thread_ts,
3738
- ...opts.eventScope ? { teamId: opts.eventScope.teamId } : {}
4147
+ teamId: opts.eventScope?.teamId
3739
4148
  }));
3740
4149
  }
3741
4150
  const recordDroppedHistory = async (reason) => {
@@ -3843,12 +4252,14 @@ async function prepareSlackMessage(params) {
3843
4252
  const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg);
3844
4253
  const hasAbortRequest = isAbortRequestText(textForCommandDetection);
3845
4254
  const channelUsersAllowlistConfigured = isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
3846
- const messageIngress = await resolveSlackCommandIngress({
4255
+ const resolveMessageIngress = async (contextBinding, threadId) => await resolveSlackCommandIngress({
3847
4256
  ctx,
4257
+ teamId: opts.eventScope?.teamId ?? ctx.teamId,
3848
4258
  senderId,
3849
4259
  senderName: senderNameForAuth,
3850
4260
  channelType: conversation.resolvedChannelType ?? "channel",
3851
4261
  channelId: message.channel,
4262
+ threadId,
3852
4263
  ownerAllowFromLower: allowFromLower,
3853
4264
  channelUsers: isRoom ? channelConfig?.users : void 0,
3854
4265
  allowTextCommands,
@@ -3863,8 +4274,10 @@ async function prepareSlackMessage(params) {
3863
4274
  requireMention: shouldRequireMention,
3864
4275
  allowTextCommands,
3865
4276
  implicitMentions
3866
- }
4277
+ },
4278
+ contextBinding
3867
4279
  });
4280
+ let messageIngress = await resolveMessageIngress();
3868
4281
  const senderGate = messageIngress.senderAccess.gate;
3869
4282
  if (isRoomish && senderGate?.allowed === false) {
3870
4283
  logVerbose(`Blocked unauthorized slack sender ${senderId} (not in sender allowlist)`);
@@ -3885,6 +4298,18 @@ async function prepareSlackMessage(params) {
3885
4298
  channel: "slack",
3886
4299
  accountId: account.accountId
3887
4300
  });
4301
+ const preflightChannelTarget = formatSlackTarget({
4302
+ teamId: opts.eventScope?.teamId,
4303
+ kind: "channel",
4304
+ id: message.channel,
4305
+ explicitKind: true
4306
+ });
4307
+ const replyRouteTarget = formatSlackTarget({
4308
+ teamId: opts.eventScope?.teamId,
4309
+ kind: isDirectMessage ? "user" : "channel",
4310
+ id: isDirectMessage ? senderId : message.channel,
4311
+ explicitKind: true
4312
+ });
3888
4313
  const commandAuthorized = messageIngress.commandAccess.authorized;
3889
4314
  if (isRoomish && messageIngress.commandAccess.shouldBlockControlCommand) {
3890
4315
  logInboundDrop({
@@ -3931,7 +4356,7 @@ async function prepareSlackMessage(params) {
3931
4356
  media: preflightMedia,
3932
4357
  cfg,
3933
4358
  accountId: account.accountId,
3934
- originatingTo: `channel:${message.channel}`,
4359
+ originatingTo: preflightChannelTarget,
3935
4360
  sessionKey: preflightRouting.sessionKey,
3936
4361
  messageThreadId: preflightRouting.threadContext.messageThreadId
3937
4362
  }) : null;
@@ -4076,14 +4501,23 @@ async function prepareSlackMessage(params) {
4076
4501
  return false;
4077
4502
  }) : statusReactionsWillHandle ? Promise.resolve(true) : null;
4078
4503
  const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
4504
+ const workspaceId = opts.eventScope?.teamId || ctx.teamId;
4505
+ const workspaceName = ctx.installationIdentity?.kind === "workspace" && ctx.installationIdentity.teamId === workspaceId ? ctx.installationIdentity.teamName : void 0;
4506
+ const groupSessionSubject = isRoomish ? resolveSlackGroupSessionSubject({
4507
+ channelId: message.channel,
4508
+ channelName,
4509
+ workspaceId,
4510
+ workspaceName
4511
+ }) : void 0;
4079
4512
  const senderName = await resolveSenderName();
4513
+ const conversationAvatar = isDirectMessage && message.user ? ctx.resolveUserAvatar(message.user, opts.eventScope) : void 0;
4080
4514
  const preview = truncateUtf16Safe(bodyForAgent.replace(/\s+/g, " "), 160);
4081
4515
  const inboundLabel = isDirectMessage ? `Slack DM from ${senderName}` : `Slack message in ${roomLabel} from ${senderName}`;
4082
4516
  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
- });
4517
+ enqueueRoutedSystemEvent(inboundLabel, {
4518
+ ...route,
4519
+ sessionKey
4520
+ }, { contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}` });
4087
4521
  const envelopeFrom = resolveConversationLabel$1({
4088
4522
  ChatType: chatType,
4089
4523
  SenderName: senderName,
@@ -4147,7 +4581,6 @@ async function prepareSlackMessage(params) {
4147
4581
  envelope: envelopeOptions
4148
4582
  })
4149
4583
  });
4150
- const slackTo = isDirectMessage ? `user:${message.user}` : `channel:${message.channel}`;
4151
4584
  const { channelMetadata, groupSystemPrompt } = resolveSlackRoomContextHints({
4152
4585
  isRoomish,
4153
4586
  channelInfo,
@@ -4155,8 +4588,10 @@ async function prepareSlackMessage(params) {
4155
4588
  });
4156
4589
  const { threadStarterBody, threadHistoryBody, shouldSeedInitialThreadContext, threadLabel, threadStarterMedia } = await resolveSlackThreadContextData({
4157
4590
  ctx,
4591
+ agentId: route.agentId,
4158
4592
  account,
4159
4593
  message,
4594
+ isGroupDm,
4160
4595
  isThreadReply,
4161
4596
  threadTs,
4162
4597
  threadStarter,
@@ -4180,8 +4615,20 @@ async function prepareSlackMessage(params) {
4180
4615
  const commandBody = textForCommandDetection.trim();
4181
4616
  const supplementalThreadHistoryBody = directThreadRoutedToDmSession && !threadHistoryBody ? threadStarterBody : threadHistoryBody;
4182
4617
  const effectiveMessageThreadId = assistantThreadContext?.threadTs ?? agentViewThreadTs ?? threadContext.messageThreadId;
4618
+ const boundMessageThreadId = directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId;
4619
+ messageIngress = await resolveMessageIngress({
4620
+ agentId: route.agentId,
4621
+ sessionKey,
4622
+ messageId: threadContext.messageTs,
4623
+ inboundEventKind
4624
+ }, boundMessageThreadId);
4625
+ if (messageIngress.ingress.admission !== "dispatch") {
4626
+ logVerbose(`Blocked slack sender ${senderId} after final route binding`);
4627
+ return null;
4628
+ }
4183
4629
  const agentContextEntities = isAgentViewMessage ? normalizeSlackAppContextEntities(message.app_context) : [];
4184
- const ctxPayload = buildChannelInboundEventContext({
4630
+ const ctxPayload = (ctx.buildContext ?? buildChannelInboundEventContext)({
4631
+ channelIngress: messageIngress,
4185
4632
  channel: "slack",
4186
4633
  accountId: route.accountId,
4187
4634
  messageId: threadContext.messageTs,
@@ -4196,22 +4643,32 @@ async function prepareSlackMessage(params) {
4196
4643
  conversation: {
4197
4644
  kind: chatType,
4198
4645
  id: message.channel,
4646
+ routePeer: {
4647
+ kind: chatType,
4648
+ id: qualifySlackRoutePeerId({
4649
+ id: isDirectMessage ? message.user ?? "unknown" : message.channel,
4650
+ kind: isDirectMessage ? "user" : "channel",
4651
+ eventScope: opts.eventScope
4652
+ })
4653
+ },
4199
4654
  label: envelopeFrom,
4200
4655
  spaceId: opts.eventScope?.teamId || ctx.teamId || void 0,
4201
- threadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
4202
- nativeChannelId: message.channel
4656
+ threadId: boundMessageThreadId,
4657
+ nativeChannelId: message.channel,
4658
+ avatar: conversationAvatar
4203
4659
  },
4204
4660
  route: {
4205
4661
  agentId: route.agentId,
4206
4662
  dmScope: route.dmScope,
4207
4663
  accountId: route.accountId,
4208
- routeSessionKey: sessionKey,
4664
+ routeSessionKey: route.sessionKey,
4665
+ dispatchSessionKey: sessionKey,
4209
4666
  parentSessionKey: threadKeys.parentSessionKey
4210
4667
  },
4211
4668
  reply: {
4212
- to: slackTo,
4669
+ to: replyRouteTarget,
4213
4670
  replyToId: threadContext.replyToId,
4214
- messageThreadId: directThreadRoutedToDmSession ? void 0 : effectiveMessageThreadId,
4671
+ messageThreadId: boundMessageThreadId,
4215
4672
  nativeChannelId: message.channel
4216
4673
  },
4217
4674
  message: {
@@ -4244,7 +4701,7 @@ async function prepareSlackMessage(params) {
4244
4701
  groupSystemPrompt
4245
4702
  },
4246
4703
  extra: {
4247
- GroupSubject: isRoomish ? roomLabel : void 0,
4704
+ GroupSubject: groupSessionSubject,
4248
4705
  ChannelPromptContext: channelMetadata ? [channelMetadata] : void 0,
4249
4706
  ChannelStructuredContext: agentContextEntities.length > 0 ? [{
4250
4707
  label: "Slack active context",
@@ -4284,16 +4741,16 @@ async function prepareSlackMessage(params) {
4284
4741
  });
4285
4742
  const pinnedMainDmOwner = isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({
4286
4743
  dmScope: cfg.session?.dmScope,
4287
- allowFrom: ctx.allowFrom,
4744
+ allowFrom: allowFromLower,
4288
4745
  normalizeEntry: normalizeSlackAllowOwnerEntry
4289
4746
  }) : null;
4290
- const replyTarget = isDirectMessage ? `channel:${message.channel}` : ctxPayload.To ?? void 0;
4747
+ const replyTarget = `channel:${message.channel}`;
4291
4748
  if (!replyTarget) return null;
4292
4749
  if (preflightAudioTranscript) await sendSlackPreflightAudioTranscriptEcho({
4293
4750
  transcript: preflightAudioTranscript,
4294
4751
  cfg,
4295
4752
  accountId: account.accountId,
4296
- originatingTo: `channel:${message.channel}`,
4753
+ originatingTo: preflightChannelTarget,
4297
4754
  messageThreadId: threadContext.messageThreadId
4298
4755
  });
4299
4756
  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 +4763,7 @@ async function prepareSlackMessage(params) {
4306
4763
  account,
4307
4764
  message,
4308
4765
  ...opts.relayIdentity ? { relayIdentity: opts.relayIdentity } : {},
4309
- ...opts.eventScope ? { eventScope: opts.eventScope } : {},
4766
+ eventScope: opts.eventScope,
4310
4767
  route,
4311
4768
  channelConfig,
4312
4769
  replyTarget,
@@ -4314,13 +4771,13 @@ async function prepareSlackMessage(params) {
4314
4771
  turn: {
4315
4772
  storePath,
4316
4773
  record: {
4317
- updateLastRoute: isDirectMessage ? {
4774
+ updateLastRoute: isDirectMessage || opts.eventScope ? {
4318
4775
  sessionKey: updateLastRouteSessionKey,
4319
4776
  channel: "slack",
4320
- to: `user:${message.user}`,
4777
+ to: replyRouteTarget,
4321
4778
  accountId: route.accountId,
4322
4779
  threadId: effectiveMessageThreadId,
4323
- mainDmOwnerPin: updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && message.user ? {
4780
+ mainDmOwnerPin: isDirectMessage && updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && message.user ? {
4324
4781
  ownerRecipient: pinnedMainDmOwner,
4325
4782
  senderRecipient: normalizeLowercaseStringOrEmpty(message.user),
4326
4783
  onSkip: ({ ownerRecipient, senderRecipient }) => {