diff --git a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts index 585ce851b0a..eaeaf176360 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts @@ -27,6 +27,7 @@ type MatrixHandlerTestHarnessOptions = { accountAllowBots?: boolean | "mentions"; configuredBotUserIds?: Set; mentionRegexes?: MatrixMonitorHandlerParams["mentionRegexes"]; + buildMentionRegexes?: MatrixMonitorHandlerParams["core"]["channel"]["mentions"]["buildMentionRegexes"]; groupPolicy?: "open" | "allowlist" | "disabled"; replyToMode?: ReplyToMode; threadReplies?: "off" | "inbound" | "always"; @@ -177,6 +178,9 @@ export function createMatrixHandlerTestHarness( } }), }, + mentions: { + buildMentionRegexes: options.buildMentionRegexes ?? (() => []), + }, reactions: { shouldAckReaction: options.shouldAckReaction ?? (() => false), }, diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 8e842e38baa..e5e02bf4dad 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -426,8 +426,8 @@ describe("matrix monitor handler pairing account scope", () => { expect(recordInboundSession).not.toHaveBeenCalled(); }); - it("drops forged metadata-only mentions before agent routing", async () => { - const { handler, recordInboundSession, resolveAgentRoute } = createMatrixHandlerTestHarness({ + it("drops forged metadata-only mentions without processing", async () => { + const { handler, recordInboundSession } = createMatrixHandlerTestHarness({ isDirectMessage: false, mentionRegexes: [/@bot/i], getMemberDisplayName: async () => "sender", @@ -442,7 +442,6 @@ describe("matrix monitor handler pairing account scope", () => { }), ); - expect(resolveAgentRoute).not.toHaveBeenCalled(); expect(recordInboundSession).not.toHaveBeenCalled(); }); @@ -1012,6 +1011,38 @@ describe("matrix monitor handler pairing account scope", () => { expect(resolveAgentRoute).toHaveBeenCalledTimes(1); }); + + it("re-resolves mentions with agent-level patterns after route resolution (#51082)", async () => { + const buildMentionRegexes = vi.fn((_cfg: unknown, agentId?: string) => { + if (!agentId) return []; + return [/@mybot/i]; + }); + const dispatchReplyFromConfig = vi.fn(async () => ({ + queuedFinal: false, + counts: { final: 0, block: 0, tool: 0 }, + })); + const { handler } = createMatrixHandlerTestHarness({ + mentionRegexes: [], + buildMentionRegexes, + roomsConfig: { + "!room:example.org": { requireMention: true }, + }, + isDirectMessage: false, + groupPolicy: "open", + dispatchReplyFromConfig, + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$agent-mention", + body: "hey @mybot can you help?", + }), + ); + + expect(buildMentionRegexes).toHaveBeenCalledWith(expect.anything(), "ops"); + expect(dispatchReplyFromConfig).toHaveBeenCalled(); + }); }); describe("matrix monitor handler durable inbound dedupe", () => { diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 40c386e3820..e2b4d929638 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -527,7 +527,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam return; } - const { wasMentioned, hasExplicitMention } = resolveMentions({ + let { wasMentioned, hasExplicitMention } = resolveMentions({ content, userId: selfUserId, text: mentionPrecheckText, @@ -588,12 +588,23 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam !hasExplicitMention && commandAuthorized && hasControlCommandInMessage; - const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention; - if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) { + let canDetectMention = mentionRegexes.length > 0 || hasExplicitMention; + // When there is message text, defer mention drop until after route + // resolution so agent-level mentionPatterns are checked (#51082). + // Media-only/poll events have no text - drop them immediately. + if ( + isRoom && + shouldRequireMention && + !wasMentioned && + !shouldBypassMention && + !mentionPrecheckText + ) { logger.info("skipping room message", { roomId, reason: "no-mention" }); await commitInboundEventIfClaimed(); return; } + const mentionDropDeferred = + isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention; if (isPollEvent) { const pollSnapshot = await fetchMatrixPollSnapshot(client, roomId, event).catch((err) => { @@ -698,6 +709,29 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam eventTs: eventTs ?? undefined, resolveAgentRoute: core.channel.routing.resolveAgentRoute, }); + + // Re-resolve mentions with agent-specific mentionPatterns now that the + // route (and agentId) is known (#51082). + if (mentionDropDeferred) { + const agentMentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId); + if (agentMentionRegexes.length > 0) { + const agentMentionResult = resolveMentions({ + content, + userId: selfUserId, + text: mentionPrecheckText, + mentionRegexes: agentMentionRegexes, + }); + if (agentMentionResult.wasMentioned) { + wasMentioned = true; + canDetectMention = true; + } + } + if (!wasMentioned) { + logger.info("skipping room message", { roomId, reason: "no-mention" }); + return; + } + } + if (configuredBinding) { const ensured = await ensureConfiguredAcpBindingReady({ cfg,