fix(slack): suppress reasoning in native streams

This commit is contained in:
Peter Steinberger
2026-04-25 00:23:09 +01:00
parent 3dba3d8b35
commit 86856b88e3
3 changed files with 37 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
- Providers/GitHub Copilot: keep the plugin stream wrapper from claiming transport selection before OpenClaw picks a boundary-aware stream path, avoiding Pi's stale fallback Copilot headers on normal model turns. Thanks @steipete.
- Discord/subagents: pass runtime config into thread-bound native subagent binding and require it at the helper boundary so Discord channel resolution keeps account-aware config. Fixes #71054. (#70945) Thanks @jai.
- Slack/Assistant: accept Slack Assistant DM `message_changed` events when their metadata identifies the human sender, while continuing to drop self-authored bot edits. Fixes #55445. Thanks @AlfredPros.
- Slack/native streaming: suppress reasoning-only payloads before `chat.startStream`/`appendStream`, so Claude extended-thinking blocks no longer appear as visible Slack messages. Fixes #59687. Thanks @vision-ifc.
- Slack/block replies: keep multi-part block deliveries in the first Slack reply thread when `replyToMode` is `first`, matching text reply threading instead of leaking later blocks into the channel. Fixes #49341. Thanks @pholmstr and @xiwuqi.
- Agents/failover: stop body-less HTTP 400/422 proxy failures from defaulting to `"format"` classification, so embedded retries surface the opaque provider failure instead of falling into a compaction loop. Fixes #66462. (#67024) Thanks @altaywtf and @HongzhuLiu.
- Plugins/loader: use cached discovery-mode snapshot loads for read-only plugin capability lookups, keep snapshot caches isolated from active Gateway registries, and make same-plugin channel/HTTP route re-registration idempotent so repeated snapshot or hot-reload paths no longer rerun full plugin side effects or accumulate duplicate surfaces. Fixes #51781, #52031, #54181, and #57514. Thanks @livingghost, @okuyam2y, @ShionEria, and @bbshih.

View File

@@ -34,7 +34,13 @@ let mockedReplyThreadTs: string | undefined = THREAD_TS;
let mockedReplyThreadTsSequence: Array<string | undefined> | undefined;
let mockedDispatchSequence: Array<{
kind: "tool" | "block" | "final";
payload: { text: string; isError?: boolean; mediaUrl?: string; mediaUrls?: string[] };
payload: {
text: string;
isError?: boolean;
isReasoning?: boolean;
mediaUrl?: string;
mediaUrls?: string[];
};
}> = [];
const noop = () => {};
@@ -289,7 +295,13 @@ vi.mock("../reply.runtime.js", () => ({
replyOptions?: { disableBlockStreaming?: boolean };
dispatcher: {
deliver: (
payload: { text: string; isError?: boolean; mediaUrl?: string; mediaUrls?: string[] },
payload: {
text: string;
isError?: boolean;
isReasoning?: boolean;
mediaUrl?: string;
mediaUrls?: string[];
},
info: { kind: "tool" | "block" | "final" },
) => Promise<void>;
};
@@ -389,6 +401,25 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
expect(deliverRepliesMock).not.toHaveBeenCalled();
});
it("suppresses reasoning payloads before Slack native streaming delivery", async () => {
mockedNativeStreaming = true;
mockedDispatchSequence = [
{ kind: "block", payload: { text: "Reasoning:\n_hidden_", isReasoning: true } },
{ kind: "final", payload: { text: FINAL_REPLY_TEXT } },
];
await dispatchPreparedSlackMessage(createPreparedSlackMessage());
expect(startSlackStreamMock).toHaveBeenCalledTimes(1);
expect(startSlackStreamMock).toHaveBeenCalledWith(
expect.objectContaining({
text: FINAL_REPLY_TEXT,
}),
);
expect(appendSlackStreamMock).not.toHaveBeenCalled();
expect(deliverRepliesMock).not.toHaveBeenCalled();
});
it("keeps same-content tool and final payloads distinct after preview fallback", async () => {
mockedDispatchSequence = [
{ kind: "tool", payload: { text: SAME_TEXT } },

View File

@@ -589,6 +589,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
payload: ReplyPayload;
kind: ReplyDispatchKind;
}): Promise<void> => {
if (params.payload.isReasoning === true) {
return;
}
const reply = resolveSendableOutboundReplyParts(params.payload);
if (
streamFailed ||