diff --git a/extensions/mattermost/src/mattermost/monitor.test.ts b/extensions/mattermost/src/mattermost/monitor.test.ts index 3394dc2c995..49f71e46d9a 100644 --- a/extensions/mattermost/src/mattermost/monitor.test.ts +++ b/extensions/mattermost/src/mattermost/monitor.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../runtime-api.js"; import { resolveMattermostAccount } from "./accounts.js"; import { + buildMattermostModelPickerSelectMessageSid, evaluateMattermostMentionGate, MattermostRetryableInboundError, processMattermostReplayGuardedPost, @@ -378,6 +379,41 @@ describe("processMattermostReplayGuardedPost", () => { }); }); +describe("buildMattermostModelPickerSelectMessageSid", () => { + it("stays stable for the same picker selection", () => { + expect( + buildMattermostModelPickerSelectMessageSid({ + postId: "post-1", + provider: "OpenAI", + model: " GPT-5 ", + }), + ).toBe("interaction:post-1:select:openai/gpt-5"); + expect( + buildMattermostModelPickerSelectMessageSid({ + postId: "post-1", + provider: "openai", + model: "gpt-5", + }), + ).toBe("interaction:post-1:select:openai/gpt-5"); + }); + + it("keeps different model selections distinct", () => { + expect( + buildMattermostModelPickerSelectMessageSid({ + postId: "post-1", + provider: "openai", + model: "gpt-5", + }), + ).not.toBe( + buildMattermostModelPickerSelectMessageSid({ + postId: "post-1", + provider: "openai", + model: "gpt-4.1", + }), + ); + }); +}); + describe("resolveMattermostReactionChannelId", () => { it("prefers broadcast channel_id when present", () => { expect( diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index bf1279596bc..811db94ded9 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -139,6 +139,16 @@ export class MattermostRetryableInboundError extends Error { } } +export function buildMattermostModelPickerSelectMessageSid(params: { + postId: string; + provider: string; + model: string; +}): string { + const provider = normalizeLowercaseStringOrEmpty(params.provider); + const model = normalizeLowercaseStringOrEmpty(params.model); + return `interaction:${params.postId}:select:${provider}/${model}`; +} + function buildMattermostInboundReplayKeys(params: { accountId: string; messageIds: string[]; @@ -698,6 +708,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} roomLabel: string; teamId?: string; postId: string; + messageSid?: string; effectiveReplyToId?: string; deliverReplies?: boolean; }): Promise => { @@ -731,7 +742,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} SenderId: params.senderId, Provider: "mattermost" as const, Surface: "mattermost" as const, - MessageSid: `interaction:${params.postId}:${Date.now()}`, + MessageSid: params.messageSid ?? `interaction:${params.postId}:${Date.now()}`, ReplyToId: params.effectiveReplyToId, MessageThreadId: params.effectiveReplyToId, Timestamp: Date.now(), @@ -1023,6 +1034,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} roomLabel, teamId, postId: params.payload.post_id, + messageSid: buildMattermostModelPickerSelectMessageSid({ + postId: params.payload.post_id, + provider: pickerState.provider, + model: pickerState.model, + }), effectiveReplyToId: threadContext.effectiveReplyToId, deliverReplies: true, });