From 367faac596b3b08724627cf042264d5aa7b5f575 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Wed, 22 Apr 2026 11:40:45 +1000 Subject: [PATCH] fix(mattermost): suppress reasoning-only replies --- .../src/mattermost/reply-delivery.test.ts | 72 +++++++++++++++++++ .../src/mattermost/reply-delivery.ts | 17 +++++ 2 files changed, 89 insertions(+) diff --git a/extensions/mattermost/src/mattermost/reply-delivery.test.ts b/extensions/mattermost/src/mattermost/reply-delivery.test.ts index ff033553acd..e2ed60ef4ea 100644 --- a/extensions/mattermost/src/mattermost/reply-delivery.test.ts +++ b/extensions/mattermost/src/mattermost/reply-delivery.test.ts @@ -38,6 +38,78 @@ function createReplyDeliveryCore(): DeliverMattermostReplyPayloadParams["core"] } describe("deliverMattermostReplyPayload", () => { + it("suppresses payloads flagged as reasoning", async () => { + const sendMessage = vi.fn(async () => undefined); + const cfg = {} satisfies OpenClawConfig; + const core = createReplyDeliveryCore(); + + await deliverMattermostReplyPayload({ + core, + cfg, + payload: { text: "Reasoning:\n_hidden_", isReasoning: true }, + to: "channel:town-square", + accountId: "default", + agentId: "agent-1", + replyToId: "root-post", + textLimit: 4000, + tableMode: "off", + sendMessage, + }); + + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it("suppresses reasoning-prefixed payloads even without an explicit flag", async () => { + const sendMessage = vi.fn(async () => undefined); + const cfg = {} satisfies OpenClawConfig; + const core = createReplyDeliveryCore(); + + await deliverMattermostReplyPayload({ + core, + cfg, + payload: { text: " \n Reasoning:\n_hidden_" }, + to: "channel:town-square", + accountId: "default", + agentId: "agent-1", + replyToId: "root-post", + textLimit: 4000, + tableMode: "off", + sendMessage, + }); + + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it("does not suppress messages that mention Reasoning: mid-text", async () => { + const sendMessage = vi.fn(async () => undefined); + const cfg = {} satisfies OpenClawConfig; + const core = createReplyDeliveryCore(); + + await deliverMattermostReplyPayload({ + core, + cfg, + payload: { text: "Intro line\nReasoning: appears in content but is not a prefix" }, + to: "channel:town-square", + accountId: "default", + agentId: "agent-1", + replyToId: "root-post", + textLimit: 4000, + tableMode: "off", + sendMessage, + }); + + expect(sendMessage).toHaveBeenCalledTimes(1); + expect(sendMessage).toHaveBeenCalledWith( + "channel:town-square", + "Intro line\nReasoning: appears in content but is not a prefix", + expect.objectContaining({ + cfg, + accountId: "default", + replyToId: "root-post", + }), + ); + }); + it("passes agent-scoped mediaLocalRoots when sending media paths", async () => { const previousStateDir = process.env.OPENCLAW_STATE_DIR; const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mm-state-")); diff --git a/extensions/mattermost/src/mattermost/reply-delivery.ts b/extensions/mattermost/src/mattermost/reply-delivery.ts index 81abeafb092..6ec685b4b33 100644 --- a/extensions/mattermost/src/mattermost/reply-delivery.ts +++ b/extensions/mattermost/src/mattermost/reply-delivery.ts @@ -2,6 +2,7 @@ import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts, } from "openclaw/plugin-sdk/reply-payload"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { getAgentScopedMediaLocalRoots, type OpenClawConfig, @@ -23,6 +24,19 @@ type SendMattermostMessage = ( }, ) => Promise; +const REASONING_PREFIX = "reasoning:"; + +function shouldSuppressReasoningReply(payload: ReplyPayload): boolean { + if (payload.isReasoning === true) { + return true; + } + const text = payload.text; + if (typeof text !== "string") { + return false; + } + return normalizeLowercaseStringOrEmpty(text.trimStart()).startsWith(REASONING_PREFIX); +} + export async function deliverMattermostReplyPayload(params: { core: PluginRuntime; cfg: OpenClawConfig; @@ -35,6 +49,9 @@ export async function deliverMattermostReplyPayload(params: { tableMode: MarkdownTableMode; sendMessage: SendMattermostMessage; }): Promise { + if (shouldSuppressReasoningReply(params.payload)) { + return; + } const reply = resolveSendableOutboundReplyParts(params.payload, { text: params.core.channel.text.convertMarkdownTables( params.payload.text ?? "",