fix: suppress Mattermost quoted reasoning replies (#69927) (thanks @lawrence3699)

This commit is contained in:
Peter Steinberger
2026-04-22 03:58:04 +01:00
parent bb43c7b89f
commit 23a017be7c
10 changed files with 86 additions and 39 deletions

View File

@@ -257,7 +257,7 @@ describe("deliverMattermostReplyWithDraftPreview", () => {
const deliverFinal = vi.fn(async () => {});
await deliverMattermostReplyWithDraftPreview({
payload: { text: " \n Reasoning:\n_hidden_" } as never,
payload: { text: " \n > Reasoning:\n> _hidden_" } as never,
info: { kind: "final" },
client: createMattermostClientMock(),
draftStream,

View File

@@ -1,5 +1,6 @@
import { deliverFinalizableDraftPreview } from "openclaw/plugin-sdk/channel-lifecycle";
import { createClaimableDedupe, type ClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
import { isReasoningReplyPayload } from "openclaw/plugin-sdk/reply-payload";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import {
normalizeLowercaseStringOrEmpty,
@@ -55,10 +56,7 @@ import {
type MattermostWebSocketFactory,
} from "./monitor-websocket.js";
import { runWithReconnect } from "./reconnect.js";
import {
deliverMattermostReplyPayload,
shouldSuppressMattermostReasoningReply,
} from "./reply-delivery.js";
import { deliverMattermostReplyPayload } from "./reply-delivery.js";
import type {
ChannelAccountSnapshot,
ChatType,
@@ -292,7 +290,7 @@ type MattermostDraftPreviewDeliverParams = {
export async function deliverMattermostReplyWithDraftPreview(
params: MattermostDraftPreviewDeliverParams,
): Promise<void> {
if (shouldSuppressMattermostReasoningReply(params.payload)) {
if (isReasoningReplyPayload(params.payload)) {
return;
}

View File

@@ -80,6 +80,27 @@ describe("deliverMattermostReplyPayload", () => {
expect(sendMessage).not.toHaveBeenCalled();
});
it("suppresses reasoning payloads formatted as a Mattermost blockquote", async () => {
const sendMessage = vi.fn(async () => undefined);
const cfg = {} satisfies OpenClawConfig;
const core = createReplyDeliveryCore();
await deliverMattermostReplyPayload({
core,
cfg,
payload: { text: "> 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;

View File

@@ -1,8 +1,8 @@
import {
deliverTextOrMediaReply,
isReasoningReplyPayload,
resolveSendableOutboundReplyParts,
} from "openclaw/plugin-sdk/reply-payload";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
getAgentScopedMediaLocalRoots,
type OpenClawConfig,
@@ -24,19 +24,6 @@ type SendMattermostMessage = (
},
) => Promise<unknown>;
const REASONING_PREFIX = "reasoning:";
export function shouldSuppressMattermostReasoningReply(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;
@@ -49,7 +36,7 @@ export async function deliverMattermostReplyPayload(params: {
tableMode: MarkdownTableMode;
sendMessage: SendMattermostMessage;
}): Promise<void> {
if (shouldSuppressMattermostReasoningReply(params.payload)) {
if (isReasoningReplyPayload(params.payload)) {
return;
}
const reply = resolveSendableOutboundReplyParts(params.payload, {

View File

@@ -101,6 +101,10 @@ describe("deliverWebReply", () => {
await expectReplySuppressed({ text: " \n Reasoning:\n_hidden_" });
});
it("suppresses payloads that start with a quoted reasoning prefix", async () => {
await expectReplySuppressed({ text: " > Reasoning:\n> _hidden_" });
});
it("does not suppress messages that mention Reasoning: mid-text", async () => {
const msg = makeMsg();

View File

@@ -2,11 +2,11 @@ import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-chunking";
import {
isReasoningReplyPayload,
resolveOutboundMediaUrls,
sendMediaWithLeadingCaption,
} from "openclaw/plugin-sdk/reply-payload";
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { loadWebMedia } from "../media.js";
import { newConnectionId } from "../reconnect.js";
import { formatError } from "../session.js";
@@ -16,19 +16,6 @@ import { whatsappOutboundLog } from "./loggers.js";
import type { WebInboundMsg } from "./types.js";
import { elide } from "./util.js";
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 deliverWebReply(params: {
replyResult: ReplyPayload;
msg: WebInboundMsg;
@@ -46,7 +33,7 @@ export async function deliverWebReply(params: {
}) {
const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
const replyStarted = Date.now();
if (shouldSuppressReasoningReply(replyResult)) {
if (isReasoningReplyPayload(replyResult)) {
whatsappOutboundLog.debug(`Suppressed reasoning payload to ${msg.from}`);
return;
}