fix: treat reply-to-bot as implicit mention across channels
This commit is contained in:
@@ -126,6 +126,106 @@ describe("discord tool result dispatch", () => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
}, 20_000);
|
||||
|
||||
it("accepts guild reply-to-bot messages as implicit mentions", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
channels: {
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
groupPolicy: "open",
|
||||
guilds: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
cfg,
|
||||
discordConfig: cfg.channels.discord,
|
||||
accountId: "default",
|
||||
token: "token",
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
},
|
||||
botUserId: "bot-id",
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 10_000,
|
||||
textLimit: 2000,
|
||||
replyToMode: "off",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: false,
|
||||
guildEntries: { "*": { requireMention: true } },
|
||||
});
|
||||
|
||||
const client = {
|
||||
fetchChannel: vi.fn().mockResolvedValue({
|
||||
type: ChannelType.GuildText,
|
||||
name: "general",
|
||||
}),
|
||||
} as unknown as Client;
|
||||
|
||||
await handler(
|
||||
{
|
||||
message: {
|
||||
id: "m3",
|
||||
content: "following up",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
referencedMessage: {
|
||||
id: "m2",
|
||||
channelId: "c1",
|
||||
content: "bot reply",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
author: { id: "bot-id", bot: true, username: "Clawdbot" },
|
||||
},
|
||||
},
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
member: { nickname: "Ada" },
|
||||
guild: { id: "g1", name: "Guild" },
|
||||
guild_id: "g1",
|
||||
channel: { id: "c1", type: ChannelType.GuildText },
|
||||
client,
|
||||
data: {
|
||||
id: "m3",
|
||||
content: "following up",
|
||||
channel_id: "c1",
|
||||
guild_id: "g1",
|
||||
type: MessageType.Default,
|
||||
mentions: [],
|
||||
},
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
const payload = dispatchMock.mock.calls[0]?.[0]?.ctx as Record<string, unknown>;
|
||||
expect(payload.WasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("forks thread sessions and injects starter context", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
let capturedCtx:
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { resolveMentionGating } from "../../channels/mention-gating.js";
|
||||
import { sendMessageDiscord } from "../send.js";
|
||||
import {
|
||||
allowListMatches,
|
||||
@@ -164,6 +165,12 @@ export async function preflightDiscordMessage(
|
||||
!isDirectMessage &&
|
||||
(Boolean(botId && message.mentionedUsers?.some((user: User) => user.id === botId)) ||
|
||||
matchesMentionPatterns(baseText, mentionRegexes));
|
||||
const implicitMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
botId &&
|
||||
message.referencedMessage?.author?.id &&
|
||||
message.referencedMessage.author.id === botId,
|
||||
);
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(
|
||||
`discord: inbound id=${message.id} guild=${message.guild?.id ?? "dm"} channel=${message.channelId} mention=${wasMentioned ? "yes" : "no"} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"} content=${messageText ? "yes" : "no"}`,
|
||||
@@ -327,10 +334,17 @@ export async function preflightDiscordMessage(
|
||||
!hasAnyMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommand(baseText, params.cfg);
|
||||
const effectiveWasMentioned = wasMentioned || shouldBypassMention;
|
||||
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
||||
const mentionGate = resolveMentionGating({
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
shouldBypassMention,
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (isGuildMessage && shouldRequireMention) {
|
||||
if (botId && !wasMentioned && !shouldBypassMention) {
|
||||
if (botId && mentionGate.shouldSkip) {
|
||||
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
|
||||
logger.info(
|
||||
{
|
||||
|
||||
@@ -56,10 +56,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
isGroupDm,
|
||||
baseText,
|
||||
messageText,
|
||||
wasMentioned,
|
||||
shouldRequireMention,
|
||||
canDetectMention,
|
||||
shouldBypassMention,
|
||||
effectiveWasMentioned,
|
||||
historyEntry,
|
||||
threadChannel,
|
||||
@@ -94,7 +92,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
if (!isGuildMessage) return false;
|
||||
if (!shouldRequireMention) return false;
|
||||
if (!canDetectMention) return false;
|
||||
return wasMentioned || shouldBypassMention;
|
||||
return effectiveWasMentioned;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user