mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: suppress Mattermost quoted reasoning replies (#69927) (thanks @lawrence3699)
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Mattermost: suppress reasoning-only payloads even when they arrive as blockquoted `> Reasoning:` text, preventing `/reasoning on` from leaking thinking into channel posts. (#69927) Thanks @lawrence3699.
|
||||
- Browser/Chrome MCP: reset cached existing-session control sessions when a `navigate_page` call times out, so one stuck navigation no longer poisons the browser profile until a gateway restart. (#69733) Thanks @ayeshakhalid192007-dev.
|
||||
- Browser/Chrome MCP: propagate click timeouts and abort signals to existing-session actions so a stuck click fails fast and reconnects instead of poisoning the browser tool until gateway restart. (#63524) Thanks @dongseok0.
|
||||
- OpenCode Go: canonicalize stale bundled `opencode-go` base URLs from `/go` or `/go/v1` to `/zen/go` or `/zen/go/v1`, so older generated model metadata stops hitting the 404 HTML endpoint. (#69898)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
d7f6e6ecdfb78c73760689af5a684c20ec7ca28509d4f63bf0d990a2d739c6ce plugin-sdk-api-baseline.json
|
||||
584681e4436a4e84c2ff20196ff194a63915caf4dda70de9c27f34ab0d7bde0b plugin-sdk-api-baseline.jsonl
|
||||
8ac8add8354dc1af76b9aa6f15f7fdcc5265b0bdaf72ea7fc1d3d11bc9f74b8c plugin-sdk-api-baseline.json
|
||||
83310e1d3ea75e9216300ed36e61fdfcfdb6bba7d5c0df62cbfe03ec93565b73 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
hasOutboundMedia,
|
||||
hasOutboundReplyContent,
|
||||
hasOutboundText,
|
||||
isReasoningReplyPayload,
|
||||
isNumericTargetId,
|
||||
resolveOutboundMediaUrls,
|
||||
resolveSendableOutboundReplyParts,
|
||||
@@ -14,6 +15,22 @@ import {
|
||||
sendPayloadWithChunkedTextAndMedia,
|
||||
} from "./reply-payload.js";
|
||||
|
||||
describe("isReasoningReplyPayload", () => {
|
||||
it.each([
|
||||
{ name: "flagged", payload: { text: "Visible", isReasoning: true }, expected: true },
|
||||
{ name: "prefix", payload: { text: " \n Reasoning:\n_hidden_" }, expected: true },
|
||||
{ name: "blockquote", payload: { text: "> Reasoning:\n> _hidden_" }, expected: true },
|
||||
{
|
||||
name: "mid-message mention",
|
||||
payload: { text: "Intro\nReasoning: visible discussion" },
|
||||
expected: false,
|
||||
},
|
||||
{ name: "missing text", payload: {}, expected: false },
|
||||
])("$name", ({ payload, expected }) => {
|
||||
expect(isReasoningReplyPayload(payload)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendPayloadWithChunkedTextAndMedia", () => {
|
||||
it("returns empty result when payload has no text and no media", async () => {
|
||||
const result = await sendPayloadWithChunkedTextAndMedia({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChannelOutboundAdapter } from "../channels/plugins/outbound.types.js";
|
||||
import { readStringValue } from "../shared/string-coerce.js";
|
||||
import { normalizeLowercaseStringOrEmpty, readStringValue } from "../shared/string-coerce.js";
|
||||
|
||||
export type { MediaPayload, MediaPayloadInput } from "../channels/plugins/media-payload.js";
|
||||
export { buildMediaPayload } from "../channels/plugins/media-payload.js";
|
||||
@@ -12,6 +12,11 @@ export type OutboundReplyPayload = {
|
||||
replyToId?: string;
|
||||
};
|
||||
|
||||
export type ReasoningReplyPayload = {
|
||||
text?: string;
|
||||
isReasoning?: boolean;
|
||||
};
|
||||
|
||||
export type SendableOutboundReplyParts = {
|
||||
text: string;
|
||||
trimmedText: string;
|
||||
@@ -29,6 +34,33 @@ type SendPayloadAdapter = Pick<
|
||||
"sendMedia" | "sendText" | "chunker" | "textChunkLimit"
|
||||
>;
|
||||
|
||||
const REASONING_PREFIX = "reasoning:";
|
||||
|
||||
function trimLeadingMarkdownQuoteMarkers(text: string): string {
|
||||
let candidate = text.trimStart();
|
||||
while (candidate.startsWith(">")) {
|
||||
candidate = candidate.replace(/^(?:>[ \t]?)+/, "").trimStart();
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function isReasoningReplyPayload(payload: ReasoningReplyPayload): boolean {
|
||||
if (payload.isReasoning === true) {
|
||||
return true;
|
||||
}
|
||||
const text = payload.text;
|
||||
if (typeof text !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(text.trimStart());
|
||||
if (normalized.startsWith(REASONING_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
return normalizeLowercaseStringOrEmpty(trimLeadingMarkdownQuoteMarkers(text)).startsWith(
|
||||
REASONING_PREFIX,
|
||||
);
|
||||
}
|
||||
|
||||
/** Extract the supported outbound reply fields from loose tool or agent payload objects. */
|
||||
export function normalizeOutboundReplyPayload(
|
||||
payload: Record<string, unknown>,
|
||||
|
||||
Reference in New Issue
Block a user