From 33cac4c33f24d12a53189c4de01a39d0a6c2ada1 Mon Sep 17 00:00:00 2001 From: Muhammed Mukhthar CM Date: Tue, 10 Mar 2026 06:40:15 +0000 Subject: [PATCH] fix(mattermost): normalize send action reply fallback --- CHANGELOG.md | 1 + extensions/mattermost/src/channel.test.ts | 51 +++++++++++++++++++++++ extensions/mattermost/src/channel.ts | 24 +++++++---- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2799f0d8c7a..73eb5a99673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai - Agents/memory flush: forward `memoryFlushWritePath` through `runEmbeddedPiAgent` so memory-triggered flush turns keep the append-only write guard without aborting before tool setup. Follows up on #38574. (#41761) Thanks @frankekn. - CI/CodeQL Swift toolchain: select Xcode 26.1 before installing Swift build tools so the CodeQL Swift job uses Swift tools 6.2 on `macos-latest`. (#41787) thanks @BunsDev. - Sandbox/subagents: pass the real configured workspace through `sessions_spawn` inheritance when a parent agent runs in a copied-workspace sandbox, so child `/agent` mounts point at the configured workspace instead of the parent sandbox copy. (#40757) Thanks @dsantoreis. +- Mattermost/plugin send actions: normalize direct `replyTo` fallback handling so threaded plugin sends trim blank IDs and reuse the correct reply target again. (#41176) Thanks @hnykda. ## 2026.3.8 diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index 97314f5e13b..c3ff193896f 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -214,6 +214,57 @@ describe("mattermostPlugin", () => { ]); expect(result?.details).toEqual({}); }); + + it("maps replyTo to replyToId for send actions", async () => { + const cfg = createMattermostTestConfig(); + + await mattermostPlugin.actions?.handleAction?.({ + channel: "mattermost", + action: "send", + params: { + to: "channel:CHAN1", + message: "hello", + replyTo: "post-root", + }, + cfg, + accountId: "default", + } as any); + + expect(sendMessageMattermostMock).toHaveBeenCalledWith( + "channel:CHAN1", + "hello", + expect.objectContaining({ + accountId: "default", + replyToId: "post-root", + }), + ); + }); + + it("falls back to trimmed replyTo when replyToId is blank", async () => { + const cfg = createMattermostTestConfig(); + + await mattermostPlugin.actions?.handleAction?.({ + channel: "mattermost", + action: "send", + params: { + to: "channel:CHAN1", + message: "hello", + replyToId: " ", + replyTo: " post-root ", + }, + cfg, + accountId: "default", + } as any); + + expect(sendMessageMattermostMock).toHaveBeenCalledWith( + "channel:CHAN1", + "hello", + expect.objectContaining({ + accountId: "default", + replyToId: "post-root", + }), + ); + }); }); describe("outbound", () => { diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index eeabf631893..4ff847ea30d 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -157,15 +157,9 @@ const mattermostMessageActions: ChannelMessageActionAdapter = { } const message = typeof params.message === "string" ? params.message : ""; - // The message tool passes the reply target as "replyTo", not "replyToId". - // handleSendAction reads params.replyTo into a local var but never writes - // it back, so the plugin handler must check both property names. - const replyToId = - typeof params.replyToId === "string" - ? params.replyToId - : typeof params.replyTo === "string" - ? params.replyTo - : undefined; + // Match the shared runner semantics: trim empty reply IDs away before + // falling back from replyToId to replyTo on direct plugin calls. + const replyToId = readMattermostReplyToId(params); const resolvedAccountId = accountId || undefined; const mediaUrl = @@ -209,6 +203,18 @@ const meta = { quickstartAllowFrom: true, } as const; +function readMattermostReplyToId(params: Record): string | undefined { + const readNormalizedValue = (value: unknown) => { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; + }; + + return readNormalizedValue(params.replyToId) ?? readNormalizedValue(params.replyTo); +} + function normalizeAllowEntry(entry: string): string { return entry .trim()