fix: treat reply-to-bot as implicit mention across channels

This commit is contained in:
Peter Steinberger
2026-01-16 21:50:44 +00:00
parent 97a41a6509
commit 05d149a49b
19 changed files with 427 additions and 20 deletions

View File

@@ -498,6 +498,49 @@ describe("monitorSlackProvider tool results", () => {
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
});
it("treats replies to bot threads as implicit mentions", async () => {
config = {
channels: {
slack: {
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
channels: { C1: { allow: true, requireMention: true } },
},
},
};
replyMock.mockResolvedValue({ text: "hi" });
const controller = new AbortController();
const run = monitorSlackProvider({
botToken: "bot-token",
appToken: "app-token",
abortSignal: controller.signal,
});
await waitForEvent("message");
const handler = getSlackHandlers()?.get("message");
if (!handler) throw new Error("Slack message handler not registered");
await handler({
event: {
type: "message",
user: "U1",
text: "following up",
ts: "124",
thread_ts: "123",
parent_user_id: "bot-user",
channel: "C1",
channel_type: "channel",
},
});
await flush();
controller.abort();
await run;
expect(replyMock).toHaveBeenCalledTimes(1);
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
});
it("accepts channel messages without mention when channels.slack.requireMention is false", async () => {
config = {
channels: {

View File

@@ -10,6 +10,7 @@ import { buildPairingReply } from "../../../pairing/pairing-messages.js";
import { upsertChannelPairingRequest } from "../../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
import { resolveMentionGating } from "../../../channels/mention-gating.js";
import type { ResolvedSlackAccount } from "../../accounts.js";
import { reactSlackMessage } from "../../actions.js";
@@ -172,6 +173,12 @@ export async function prepareSlackMessage(params: {
(!isDirectMessage &&
(Boolean(ctx.botUserId && message.text?.includes(`<@${ctx.botUserId}>`)) ||
matchesMentionPatterns(message.text ?? "", mentionRegexes)));
const implicitMention = Boolean(
!isDirectMessage &&
ctx.botUserId &&
message.thread_ts &&
message.parent_user_id === ctx.botUserId,
);
const sender = message.user ? await ctx.resolveUserName(message.user) : null;
const senderName =
@@ -215,9 +222,16 @@ export async function prepareSlackMessage(params: {
commandAuthorized &&
hasControlCommand(message.text ?? "", cfg);
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
const canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0;
if (isRoom && shouldRequireMention && canDetectMention && !wasMentioned && !shouldBypassMention) {
const mentionGate = resolveMentionGating({
requireMention: Boolean(shouldRequireMention),
canDetectMention,
wasMentioned,
implicitMention,
shouldBypassMention,
});
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping room message");
return null;
}
@@ -242,7 +256,7 @@ export async function prepareSlackMessage(params: {
if (!isRoom) return false;
if (!shouldRequireMention) return false;
if (!canDetectMention) return false;
return wasMentioned || shouldBypassMention;
return effectiveWasMentioned;
}
return false;
};