fix(matrix): pass agentId to buildMentionRegexes for agent-level mention patterns

The Matrix monitor called buildMentionRegexes(cfg) without agentId,
causing agent-level groupChat.mentionPatterns to be silently ignored.
Messages matching agent-specific patterns were dropped as no-mention
in rooms with requireMention: true.

Defer the mention-required drop until after route resolution so the
agentId is available for buildMentionRegexes(cfg, route.agentId).
Media-only and poll events still drop early (no text to match).

Fixes #51082
This commit is contained in:
Matt Van Horn 2026-03-20 08:28:57 -07:00
parent 50ce9ac1c6
commit bffe4276b4
No known key found for this signature in database
3 changed files with 73 additions and 8 deletions

View File

@ -27,6 +27,7 @@ type MatrixHandlerTestHarnessOptions = {
accountAllowBots?: boolean | "mentions";
configuredBotUserIds?: Set<string>;
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),
},

View File

@ -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();
});
});

View File

@ -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,