diff --git a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts index 3aa13a735a0..4344e4a0d61 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"; @@ -142,6 +143,9 @@ export function createMatrixHandlerTestHarness( resolveHumanDelayConfig: options.resolveHumanDelayConfig ?? (() => undefined), dispatchReplyFromConfig, }, + 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 289623631fa..d3cf39c4056 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(); }); @@ -477,9 +476,6 @@ describe("matrix monitor handler pairing account scope", () => { } as MatrixRawEvent); expect(downloadContent).not.toHaveBeenCalled(); - expect(getMemberDisplayName).not.toHaveBeenCalled(); - expect(getRoomInfo).not.toHaveBeenCalled(); - expect(resolveAgentRoute).not.toHaveBeenCalled(); }); it("skips poll snapshot fetches for unmentioned group poll responses", async () => { @@ -988,4 +984,36 @@ 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(); + }); }); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index b7295009bcd..b6388654cfd 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -494,7 +494,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam return; } - const { wasMentioned, hasExplicitMention } = resolveMentions({ + let { wasMentioned, hasExplicitMention } = resolveMentions({ content, userId: selfUserId, text: mentionPrecheckText, @@ -554,10 +554,21 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam commandAuthorized && hasControlCommandInMessage; const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention; - if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) { + // 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" }); return; } + const mentionDropDeferred = + isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention; if (isPollEvent) { const pollSnapshot = await fetchMatrixPollSnapshot(client, roomId, event).catch((err) => { @@ -661,6 +672,28 @@ 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; + } + } + if (!wasMentioned) { + logger.info("skipping room message", { roomId, reason: "no-mention" }); + return; + } + } + if (configuredBinding) { const ensured = await ensureConfiguredAcpBindingReady({ cfg,