fix(slack): enable preview streaming in flat DMs (replyToMode: off) (#76330)

Summary:
- The PR enables Slack draft preview streaming for flat DMs in all non-off modes, updates Slack streaming tests/docs/config metadata/changelog, and refreshes small guard baselines.
- Reproducibility: yes. source-reproducible on current main: the helper returns false for `mode: "partial"` in ...  current test asserts that disabled path. I did not run tests because this review was explicitly read-only.

Automerge notes:
- Ran the ClawSweeper repair loop before final review.
- Included post-review commit in the final squash: fix(slack): enable preview streaming in flat DMs (replyToMode: off)
- Included post-review commit in the final squash: fix(clawsweeper): address review for automerge-openclaw-openclaw-5654…

Validation:
- ClawSweeper review passed for head 52e5d74ef9.
- Required merge gates passed before the squash merge.

Prepared head SHA: 52e5d74ef9
Review: https://github.com/openclaw/openclaw/pull/76330#issuecomment-4365017023

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: bob <637186+HangGlidersRule@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-05-03 01:30:36 +00:00
committed by GitHub
parent 9765a5777c
commit e8df05ed4f
11 changed files with 45 additions and 41 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"`).

View File

@@ -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",

View File

@@ -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");
});
});

View File

@@ -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 &&

View File

@@ -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),

View File

@@ -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",

View File

@@ -25,6 +25,11 @@ const INDIRECT_RUNTIME_DEPENDENCIES = new Map<string, Set<string>>([
// 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 = {