diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ce64e07c6..f7b901c90fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai - Gateway/status: add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway, so frozen or port-conflict reports include the data needed for root-cause triage. Refs #49012. Thanks @vincentkoc. - Codex harness: forward OpenClaw workspace bootstrap files such as `SOUL.md` through native Codex config instructions while leaving `AGENTS.md` to Codex project-doc discovery. Fixes #76273. Thanks @zknicker. - Parallels/Windows update smoke: escape the stale post-swap import regex in the generated PowerShell script so expected `ERR_MODULE_NOT_FOUND` update handoffs continue to post-update health checks. (#75315) +- Slack: allow draft preview streaming in top-level DMs when `replyToMode` is `off` while keeping Slack native streaming and assistant thread status gated on reply threads. Fixes #56480. (#56544) Thanks @HangGlidersRule. ## 2026.5.2 diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index ab077059a16..f34249ed4af 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -27444d825e0dda91c769267beb81b3826ca42fd9c2e16d4d6bedccbc5cbccba4 config-baseline.json +bb58618ecdd5cb16d985eafa4a3a4634f366e6674f3db16c246336e6e02a3706 config-baseline.json b95cc4a3b15e688afa58cf700dac425af4e50fd092209e75903e73d5d2988717 config-baseline.core.json -07005d122bda68081d530ae5cd455ffbc0817f7fb94bd462c4f25edc94a0546c config-baseline.channel.json +f2a1aad257c570b497865680c331568a6775369528749826dfa35c1f644483fc config-baseline.channel.json fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 06500026511..07ce37ae44a 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -648,8 +648,8 @@ Notes: `channels.slack.streaming.nativeTransport` controls Slack native text streaming when `channels.slack.streaming.mode` is `partial` (default: `true`). - A reply thread must be available for native text streaming and Slack assistant thread status to appear. Thread selection still follows `replyToMode`. -- Channel and group-chat roots can still use the normal draft preview when native streaming is unavailable. -- Top-level Slack DMs stay off-thread by default, so they do not show the thread-style preview; use thread replies or `typingReaction` if you want visible progress there. +- Channel, group-chat, and top-level DM roots can still use the normal draft preview when native streaming is unavailable or no reply thread exists. +- Top-level Slack DMs stay off-thread by default, so they do not show Slack's thread-style native stream/status preview; OpenClaw posts and edits a draft preview in the DM instead. - Media and non-text payloads fall back to normal delivery. - Media/error finals cancel pending preview edits; eligible text/block finals flush only when they can edit the preview in place. - If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads. diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index fe6fbdbbe4f..230242e9776 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -139,7 +139,7 @@ Modes: Slack-only: - `channels.slack.streaming.nativeTransport` toggles Slack native streaming API calls when `channels.slack.streaming.mode="partial"` (default: `true`). -- Slack native streaming and Slack assistant thread status require a reply thread target; top-level DMs do not show that thread-style preview. +- Slack native streaming and Slack assistant thread status require a reply thread target. Top-level DMs do not show that thread-style preview, but they can still use Slack draft preview posts and edits. Legacy key migration: @@ -168,6 +168,7 @@ Slack: - `partial` can use Slack native streaming (`chat.startStream`/`append`/`stop`) when available. - `block` uses append-style draft previews. - `progress` uses status preview text, then final answer. +- Top-level DMs without a reply thread use draft preview posts and edits instead of Slack native streaming. - Native and draft preview streaming suppress block replies for that turn, so a Slack reply is streamed by one delivery path only. - Final media/error payloads and progress finals do not create throwaway draft messages; only text/block finals that can edit the preview flush pending draft text. diff --git a/docs/gateway/config-channels.md b/docs/gateway/config-channels.md index ec3851efc0a..29a5a010237 100644 --- a/docs/gateway/config-channels.md +++ b/docs/gateway/config-channels.md @@ -484,7 +484,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat **Thread session isolation:** `thread.historyScope` is per-thread (default) or shared across channel. `thread.inheritParent` copies parent channel transcript to new threads. -- Slack native streaming plus the Slack assistant-style "is typing..." thread status require a reply thread target. Top-level DMs stay off-thread by default, so they use `typingReaction` or normal delivery instead of the thread-style preview. +- Slack native streaming plus the Slack assistant-style "is typing..." thread status require a reply thread target. Top-level DMs stay off-thread by default, so they can still stream through Slack draft post-and-edit previews instead of showing the thread-style native stream/status preview. - `typingReaction` adds a temporary reaction to the inbound Slack message while a reply is running, then removes it on completion. Use a Slack emoji shortcode such as `"hourglass_flowing_sand"`. - `channels.slack.execApprovals`: Slack-native exec approval delivery and approver authorization. Same schema as Discord: `enabled` (`true`/`false`/`"auto"`), `approvers` (Slack user IDs), `agentFilter`, `sessionFilter`, and `target` (`"dm"`, `"channel"`, or `"both"`). diff --git a/extensions/slack/src/config-ui-hints.ts b/extensions/slack/src/config-ui-hints.ts index bd7aab07ac1..4476e9c0b0c 100644 --- a/extensions/slack/src/config-ui-hints.ts +++ b/extensions/slack/src/config-ui-hints.ts @@ -111,7 +111,7 @@ export const slackChannelConfigUiHints = { }, "streaming.nativeTransport": { label: "Slack Native Streaming", - help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Requires a reply thread target; top-level DMs stay on the non-thread fallback path.", + help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming.", }, "streaming.preview.toolProgress": { label: "Slack Draft Tool Progress", diff --git a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts index 8b66a680f27..86666f5d3cc 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts @@ -179,35 +179,44 @@ describe("slack native streaming thread hint", () => { }); describe("slack preview streaming eligibility", () => { - it("stays on for room messages when streaming mode is enabled", () => { + it("stays off when streaming mode is disabled", () => { expect( shouldEnableSlackPreviewStreaming({ - mode: "partial", - isDirectMessage: false, - }), - ).toBe(true); - }); - - it("stays off for top-level DMs without a reply thread", () => { - expect( - shouldEnableSlackPreviewStreaming({ - mode: "partial", - isDirectMessage: true, + mode: "off", }), ).toBe(false); }); - it("allows DM preview when the reply is threaded", () => { + it("stays on for room messages when streaming mode is enabled", () => { expect( shouldEnableSlackPreviewStreaming({ mode: "partial", - isDirectMessage: true, - threadTs: "1000.1", }), ).toBe(true); }); - it("keeps top-level DMs off even when replyToMode would create a reply thread", () => { + it("allows top-level DM draft previews without a reply thread", () => { + expect( + shouldEnableSlackPreviewStreaming({ + mode: "partial", + }), + ).toBe(true); + }); + + it("allows non-partial draft preview modes", () => { + expect( + shouldEnableSlackPreviewStreaming({ + mode: "block", + }), + ).toBe(true); + expect( + shouldEnableSlackPreviewStreaming({ + mode: "progress", + }), + ).toBe(true); + }); + + it("keeps native streaming thread hints separate from draft preview eligibility", () => { const streamThreadHint = resolveSlackStreamingThreadHint({ replyToMode: "all", incomingThreadTs: undefined, @@ -218,10 +227,8 @@ describe("slack preview streaming eligibility", () => { expect( shouldEnableSlackPreviewStreaming({ mode: "partial", - isDirectMessage: true, - threadTs: undefined, }), - ).toBe(false); + ).toBe(true); expect(streamThreadHint).toBe("1000.4"); }); }); diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 5fae667c29b..f6e47f95556 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -117,16 +117,8 @@ export function isSlackStreamingEnabled(params: { export function shouldEnableSlackPreviewStreaming(params: { mode: "off" | "partial" | "block" | "progress"; - isDirectMessage: boolean; - threadTs?: string; }): boolean { - if (params.mode === "off") { - return false; - } - if (!params.isDirectMessage) { - return true; - } - return Boolean(params.threadTs); + return params.mode !== "off"; } export function shouldInitializeSlackDraftStream(params: { @@ -446,8 +438,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag !sourceRepliesAreToolOnly && shouldEnableSlackPreviewStreaming({ mode: slackStreaming.mode, - isDirectMessage: prepared.isDirectMessage, - threadTs: streamThreadHint, }); const streamingEnabled = !sourceRepliesAreToolOnly && diff --git a/scripts/check-no-raw-channel-fetch.mjs b/scripts/check-no-raw-channel-fetch.mjs index 0378f4404d2..0e3d1cacb85 100644 --- a/scripts/check-no-raw-channel-fetch.mjs +++ b/scripts/check-no-raw-channel-fetch.mjs @@ -18,8 +18,8 @@ const allowedRawFetchCallsites = new Set([ bundledPluginCallsite("bluebubbles", "src/types.ts", 204), bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 268), bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 192), - bundledPluginCallsite("chutes", "models.ts", 535), - bundledPluginCallsite("chutes", "models.ts", 542), + bundledPluginCallsite("chutes", "models.ts", 536), + bundledPluginCallsite("chutes", "models.ts", 543), bundledPluginCallsite("discord", "src/monitor/gateway-plugin.ts", 417), bundledPluginCallsite("discord", "src/monitor/gateway-plugin.ts", 483), bundledPluginCallsite("discord", "src/voice-message.ts", 298), @@ -30,7 +30,7 @@ const allowedRawFetchCallsites = new Set([ bundledPluginCallsite("github-copilot", "login.ts", 69), bundledPluginCallsite("github-copilot", "login.ts", 101), bundledPluginCallsite("googlechat", "src/auth.ts", 83), - bundledPluginCallsite("huggingface", "models.ts", 142), + bundledPluginCallsite("huggingface", "models.ts", 143), bundledPluginCallsite("kilocode", "provider-models.ts", 130), bundledPluginCallsite("matrix", "src/matrix/sdk/transport.ts", 112), bundledPluginCallsite("microsoft-foundry", "onboard.ts", 479), diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index 60623a8f723..8686901b230 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -13313,7 +13313,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [ }, "streaming.nativeTransport": { label: "Slack Native Streaming", - help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Requires a reply thread target; top-level DMs stay on the non-thread fallback path.", + help: "Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming.", }, "streaming.preview.toolProgress": { label: "Slack Draft Tool Progress", diff --git a/src/plugins/contracts/extension-runtime-dependencies.contract.test.ts b/src/plugins/contracts/extension-runtime-dependencies.contract.test.ts index 193db8198f9..b7d63c03652 100644 --- a/src/plugins/contracts/extension-runtime-dependencies.contract.test.ts +++ b/src/plugins/contracts/extension-runtime-dependencies.contract.test.ts @@ -25,6 +25,11 @@ const INDIRECT_RUNTIME_DEPENDENCIES = new Map>([ // Baileys loads jimp as an optional peer when it needs media thumbnails. new Set(["jimp"]), ], + [ + "extensions/tlon", + // The Tlon plugin manifest exposes the bundled skill from this package path. + new Set(["@tloncorp/tlon-skill"]), + ], ]); type PackageManifest = {