diff --git a/extensions/matrix/src/matrix/monitor/replies.test.ts b/extensions/matrix/src/matrix/monitor/replies.test.ts
index 09a0e7342de5..17523109990e 100644
--- a/extensions/matrix/src/matrix/monitor/replies.test.ts
+++ b/extensions/matrix/src/matrix/monitor/replies.test.ts
@@ -183,6 +183,9 @@ describe("deliverMatrixReplies", () => {
replies: [
{ text: "Reasoning:\n_hidden_" },
{ text: "still hidden" },
+ { text: "MiniMax private reasoning" },
+ { text: "MiniMax private thought" },
+ { text: "Anthropic private reasoning" },
{ text: "Visible answer" },
],
roomId: "room:5",
@@ -198,6 +201,90 @@ describe("deliverMatrixReplies", () => {
expect(sendOptions(0).cfg).toBe(cfg);
});
+ it("delivers literal reasoning tags inside Markdown code", async () => {
+ const text = "Use `example` literally.";
+
+ await deliverMatrixReplies({
+ cfg,
+ replies: [{ text }],
+ roomId: "room:5",
+ client: {} as MatrixClient,
+ runtime: runtimeEnv,
+ textLimit: 4000,
+ replyToMode: "off",
+ });
+
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
+ expect(sendCall(0)[1]).toBe(text);
+ });
+
+ it("strips namespaced reasoning while delivering visible Matrix replies", async () => {
+ await deliverMatrixReplies({
+ cfg,
+ replies: [
+ { text: "MiniMax private reasoningVisible MiniMax answer" },
+ { text: "Anthropic private reasoningVisible answer" },
+ { text: "
Visible HTML answerMiniMax private reasoning" },
+ { text: "Visible safe answerunfinished private reasoning" },
+ { text: "Visible answerold reasoningunfinished private reasoning" },
+ { text: "private reasoningVisible alias answer" },
+ { text: "Visible final answer" },
+ ],
+ roomId: "room:5",
+ client: {} as MatrixClient,
+ runtime: runtimeEnv,
+ textLimit: 4000,
+ replyToMode: "off",
+ });
+
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(7);
+ expect(sendCall(0)[1]).toBe("Visible MiniMax answer");
+ expect(sendCall(1)[1]).toBe("Visible answer");
+ expect(sendCall(2)[1]).toBe("
Visible HTML answer");
+ expect(sendCall(3)[1]).toBe("Visible safe answer");
+ expect(sendCall(4)[1]).toBe("Visible answer");
+ expect(sendCall(5)[1]).toBe("Visible alias answer");
+ expect(sendCall(6)[1]).toBe("Visible final answer");
+ });
+
+ it("preserves significant whitespace in visible Markdown replies", async () => {
+ const text = " indented Markdown code\n\nVisible line with a hard break \nnext line";
+
+ await deliverMatrixReplies({
+ cfg,
+ replies: [{ text }],
+ roomId: "room:5",
+ client: {} as MatrixClient,
+ runtime: runtimeEnv,
+ textLimit: 4000,
+ replyToMode: "off",
+ });
+
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
+ expect(sendCall(0)[1]).toBe(text.trim());
+ });
+
+ it("delivers Matrix media without a reasoning-only caption", async () => {
+ await deliverMatrixReplies({
+ cfg,
+ replies: [
+ {
+ text: "MiniMax private reasoning",
+ mediaUrl: "https://example.com/a.jpg",
+ },
+ ],
+ roomId: "room:5",
+ client: {} as MatrixClient,
+ runtime: runtimeEnv,
+ textLimit: 4000,
+ replyToMode: "off",
+ });
+
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
+ expect(sendCall(0)[1]).toBe("");
+ expect(sendOptions(0).mediaUrl).toBe("https://example.com/a.jpg");
+ });
+
it("uses supplied cfg for chunking and send delivery without reloading runtime config", async () => {
const explicitCfg = {
channels: {
diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts
index 695c939a4b46..377465cf1fb8 100644
--- a/extensions/matrix/src/matrix/monitor/replies.ts
+++ b/extensions/matrix/src/matrix/monitor/replies.ts
@@ -1,33 +1,24 @@
// Matrix plugin module implements replies behavior.
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
+import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-chunking";
import { getMatrixRuntime } from "../../runtime.js";
import type { MatrixClient } from "../sdk.js";
import { chunkMatrixText, sendMessageMatrix } from "../send.js";
import type { MarkdownTableMode, OpenClawConfig, ReplyPayload, RuntimeEnv } from "./runtime-api.js";
-const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
-const THINKING_BLOCK_RE =
- /<\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
-
-function shouldSuppressReasoningReplyText(text?: string): boolean {
+function resolveVisibleMatrixReplyText(text?: string): string | undefined {
if (typeof text !== "string") {
- return false;
+ return undefined;
}
const trimmedStart = text.trimStart();
if (!trimmedStart) {
- return false;
+ return text;
}
if (normalizeLowercaseStringOrEmpty(trimmedStart).startsWith("reasoning:")) {
- return true;
+ return undefined;
}
- THINKING_TAG_RE.lastIndex = 0;
- if (!THINKING_TAG_RE.test(text)) {
- return false;
- }
- THINKING_BLOCK_RE.lastIndex = 0;
- const withoutThinkingBlocks = text.replace(THINKING_BLOCK_RE, "");
- THINKING_TAG_RE.lastIndex = 0;
- return !withoutThinkingBlocks.replace(THINKING_TAG_RE, "").trim();
+ const visibleText = stripReasoningTagsFromText(text, { mode: "strict", trim: "none" });
+ return visibleText.trim() ? visibleText : undefined;
}
export async function deliverMatrixReplies(params: {
@@ -60,11 +51,12 @@ export async function deliverMatrixReplies(params: {
let hasReplied = false;
let deliveredAny = false;
for (const reply of params.replies) {
- if (reply.isReasoning === true || shouldSuppressReasoningReplyText(reply.text)) {
+ const visibleText = resolveVisibleMatrixReplyText(reply.text);
+ const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
+ if (reply.isReasoning === true || (!hasMedia && reply.text && visibleText === undefined)) {
logVerbose("matrix reply suppressed as reasoning-only");
continue;
}
- const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
if (!reply?.text && !hasMedia) {
if (reply?.audioAsVoice) {
logVerbose("matrix reply has audioAsVoice without media/text; skipping");
@@ -79,7 +71,7 @@ export async function deliverMatrixReplies(params: {
: params.replyToMode === "off"
? undefined
: replyToIdRaw;
- const rawText = reply.text ?? "";
+ const rawText = visibleText ?? "";
const mediaList = reply.mediaUrls?.length
? reply.mediaUrls
: reply.mediaUrl