diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index b6ae260ce..167174720 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -200,7 +200,10 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted - Activation modes: - `mention` (default): requires @mention or regex match. - `always`: always triggers. -- `/activation mention|always` is owner-only and must be sent as a standalone message. + - `replies`: only triggers when someone replies to a bot message. + - `mention+replies`: triggers on @mention/regex match OR replies to bot messages. + - `never`: never triggers (except for control commands from owners). +- `/activation mention|always|replies|mention+replies|never` is owner-only and must be sent as a standalone message. - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset). - **History injection** (pending-only): - Recent *unprocessed* messages (default 50) inserted under: diff --git a/src/acp/commands.ts b/src/acp/commands.ts index 6bd8e85a8..2892b2f02 100644 --- a/src/acp/commands.ts +++ b/src/acp/commands.ts @@ -21,7 +21,7 @@ export function getAvailableCommands(): AvailableCommand[] { { name: "dock-telegram", description: "Route replies to Telegram." }, { name: "dock-discord", description: "Route replies to Discord." }, { name: "dock-slack", description: "Route replies to Slack." }, - { name: "activation", description: "Set group activation (mention|always)." }, + { name: "activation", description: "Set group activation (mention|always|replies|mention+replies|never)." }, { name: "send", description: "Set send mode (on|off|inherit)." }, { name: "reset", description: "Reset the session (/new)." }, { name: "new", description: "Reset the session (/reset)." }, diff --git a/src/auto-reply/group-activation.ts b/src/auto-reply/group-activation.ts index 7dcd2e696..dd3729e35 100644 --- a/src/auto-reply/group-activation.ts +++ b/src/auto-reply/group-activation.ts @@ -1,11 +1,14 @@ import { normalizeCommandBody } from "./commands-registry.js"; -export type GroupActivationMode = "mention" | "always"; +export type GroupActivationMode = "mention" | "always" | "replies" | "mention+replies" | "never"; export function normalizeGroupActivation(raw?: string | null): GroupActivationMode | undefined { const value = raw?.trim().toLowerCase(); if (value === "mention") return "mention"; if (value === "always") return "always"; + if (value === "replies") return "replies"; + if (value === "mention+replies") return "mention+replies"; + if (value === "never") return "never"; return undefined; } diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index fb46627f5..35e2d79e7 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -66,7 +66,7 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC if (!activationCommand.mode) { return { shouldContinue: false, - reply: { text: "⚙️ Usage: /activation mention|always" }, + reply: { text: "⚙️ Usage: /activation mention|always|replies|mention+replies|never" }, }; } if (params.sessionEntry && params.sessionStore && params.sessionKey) { diff --git a/src/tui/commands.ts b/src/tui/commands.ts index dfc419632..1f1a49580 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -150,7 +150,7 @@ export function helpText(options: SlashCommandOptions = {}): string { "/usage ", "/elevated ", "/elev ", - "/activation ", + "/activation ", "/new or /reset", "/abort", "/settings", diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index a14172809..6ccd37923 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -391,7 +391,7 @@ export function createCommandHandlers(context: CommandHandlerContext) { break; case "activation": if (!args) { - chatLog.addSystem("usage: /activation "); + chatLog.addSystem("usage: /activation "); break; } try { diff --git a/src/web/auto-reply/monitor/group-gating.ts b/src/web/auto-reply/monitor/group-gating.ts index 8d1a33645..8b2c81ae2 100644 --- a/src/web/auto-reply/monitor/group-gating.ts +++ b/src/web/auto-reply/monitor/group-gating.ts @@ -101,26 +101,40 @@ export function applyGroupGating(params: { sessionKey: params.sessionKey, conversationId: params.conversationId, }); - const requireMention = activation !== "always"; + + // Check if this message is a reply to the bot const selfJid = params.msg.selfJid?.replace(/:\\d+/, ""); const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, ""); const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null; const replySenderE164 = params.msg.replyToSenderE164 ? normalizeE164(params.msg.replyToSenderE164) : null; - const implicitMention = Boolean( + const isReplyToBot = Boolean( (selfJid && replySenderJid && selfJid === replySenderJid) || (selfE164 && replySenderE164 && selfE164 === replySenderE164), ); + + // Determine if we should process based on activation mode + const shouldProcess = (() => { + if (activation === "always") return true; + if (activation === "never") return shouldBypassMention; + if (activation === "replies") return isReplyToBot || shouldBypassMention; + if (activation === "mention+replies") return wasMentioned || isReplyToBot || shouldBypassMention; + // Default to "mention" mode + return wasMentioned || shouldBypassMention; + })(); + + const requireMention = activation !== "always" && activation !== "replies"; const mentionGate = resolveMentionGating({ requireMention, canDetectMention: true, wasMentioned, - implicitMention, + implicitMention: isReplyToBot, shouldBypassMention, }); params.msg.wasMentioned = mentionGate.effectiveWasMentioned; - if (!shouldBypassMention && requireMention && mentionGate.shouldSkip) { + + if (!shouldProcess) { params.logVerbose( `Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`, );