diff --git a/src/slack/monitor/commands.ts b/src/slack/monitor/commands.ts index f26be177d1d..a50b75704eb 100644 --- a/src/slack/monitor/commands.ts +++ b/src/slack/monitor/commands.ts @@ -1,5 +1,16 @@ import type { SlackSlashCommandConfig } from "../../config/config.js"; +/** + * Strip Slack mentions (<@U123>, <@U123|name>) so command detection works on + * normalized text. Use in both prepare and debounce gate for consistency. + */ +export function stripSlackMentionsForCommandDetection(text: string): string { + return (text ?? "") + .replace(/<@[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + export function normalizeSlackSlashCommandName(raw: string) { return raw.replace(/^\/+/, ""); } diff --git a/src/slack/monitor/message-handler.ts b/src/slack/monitor/message-handler.ts index f87c14ccc86..e974dbeebe3 100644 --- a/src/slack/monitor/message-handler.ts +++ b/src/slack/monitor/message-handler.ts @@ -6,6 +6,7 @@ import { createInboundDebouncer, resolveInboundDebounceMs, } from "../../auto-reply/inbound-debounce.js"; +import { stripSlackMentionsForCommandDetection } from "./commands.js"; import { dispatchPreparedSlackMessage } from "./message-handler/dispatch.js"; import { prepareSlackMessage } from "./message-handler/prepare.js"; import { createSlackThreadTsResolver } from "./thread-resolution.js"; @@ -50,7 +51,8 @@ export function createSlackMessageHandler(params: { if (entry.message.files && entry.message.files.length > 0) { return false; } - return !hasControlCommand(text, ctx.cfg); + const textForCommandDetection = stripSlackMentionsForCommandDetection(text); + return !hasControlCommand(textForCommandDetection, ctx.cfg); }, onFlush: async (entries) => { const last = entries.at(-1); diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 850b425cc34..900a0484c9b 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -42,6 +42,7 @@ import { resolveSlackThreadContext } from "../../threading.js"; import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js"; import { resolveSlackEffectiveAllowFrom } from "../auth.js"; import { resolveSlackChannelConfig } from "../channel-config.js"; +import { stripSlackMentionsForCommandDetection } from "../commands.js"; import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js"; @@ -250,10 +251,7 @@ export async function prepareSlackMessage(params: { surface: "slack", }); // Strip Slack mentions (<@U123>) before command detection so "@Labrador /new" is recognized - const textForCommandDetection = (message.text ?? "") - .replace(/<@[^>]+>/g, " ") - .replace(/\s+/g, " ") - .trim(); + const textForCommandDetection = stripSlackMentionsForCommandDetection(message.text ?? ""); const hasControlCommandInMessage = hasControlCommand(textForCommandDetection, cfg); const ownerAuthorized = resolveSlackAllowListMatch({