From a4e45a9e0510f490e3e1c57f2339531b952c4cc7 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomes da Silva Date: Thu, 29 Jan 2026 15:30:28 -0300 Subject: [PATCH 1/4] feat(whatsapp): add reply-based activation modes for groups This commit adds more granular control over group activation with new modes: - 'replies': Bot only responds when someone replies to one of its messages - 'mention+replies': Bot responds to @mentions OR replies (most flexible) - 'never': Bot never responds except for control commands from owners The existing 'mention' (default) and 'always' modes remain unchanged. **Implementation details:** - Added new GroupActivationMode types: 'replies', 'mention+replies', 'never' - Enhanced group-gating logic to detect replies to bot messages - Updated all command help messages and documentation - Reply detection uses both JID and E.164 matching for reliability **Use cases:** - 'replies': Reduces noise in busy groups, bot only joins when explicitly engaged - 'mention+replies': Allows natural conversation flow after initial @mention - 'never': Temporarily disable bot in a group without removing it Closes: N/A (feature request) --- docs/channels/whatsapp.md | 5 ++++- src/acp/commands.ts | 2 +- src/auto-reply/group-activation.ts | 5 ++++- src/auto-reply/reply/commands-session.ts | 2 +- src/tui/commands.ts | 2 +- src/tui/tui-command-handlers.ts | 2 +- src/web/auto-reply/monitor/group-gating.ts | 22 ++++++++++++++++++---- 7 files changed, 30 insertions(+), 10 deletions(-) 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}`, ); From 4d1d550845fc1bd0387b34f08fcd258e1d31f236 Mon Sep 17 00:00:00 2001 From: Rodrigo Gomes da Silva Date: Thu, 29 Jan 2026 15:38:13 -0300 Subject: [PATCH 2/4] style: fix code formatting (oxfmt) --- src/acp/commands.ts | 5 ++++- src/web/auto-reply/monitor/group-gating.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/acp/commands.ts b/src/acp/commands.ts index 2892b2f02..6448a3374 100644 --- a/src/acp/commands.ts +++ b/src/acp/commands.ts @@ -21,7 +21,10 @@ 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|replies|mention+replies|never)." }, + { + 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/web/auto-reply/monitor/group-gating.ts b/src/web/auto-reply/monitor/group-gating.ts index 8b2c81ae2..44df28fe7 100644 --- a/src/web/auto-reply/monitor/group-gating.ts +++ b/src/web/auto-reply/monitor/group-gating.ts @@ -101,7 +101,7 @@ export function applyGroupGating(params: { sessionKey: params.sessionKey, conversationId: params.conversationId, }); - + // Check if this message is a reply to the bot const selfJid = params.msg.selfJid?.replace(/:\\d+/, ""); const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, ""); @@ -113,17 +113,18 @@ export function applyGroupGating(params: { (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; + 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, @@ -133,7 +134,7 @@ export function applyGroupGating(params: { shouldBypassMention, }); params.msg.wasMentioned = mentionGate.effectiveWasMentioned; - + if (!shouldProcess) { params.logVerbose( `Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`, From dd5fd12f5213da0e8324edfb77013aa8a2ca0d9e Mon Sep 17 00:00:00 2001 From: Rodrigo Gomes da Silva Date: Thu, 29 Jan 2026 15:47:27 -0300 Subject: [PATCH 3/4] fix: update GroupActivationMode types across codebase Extended type definitions to support new activation modes: - SessionEntry.groupActivation - StatusArgs.groupActivation - sessions-patch error message All TypeScript compilation errors resolved. --- src/auto-reply/status.ts | 2 +- src/config/sessions/types.ts | 2 +- src/gateway/sessions-patch.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index e69941cd8..3a9a4efd9 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -59,7 +59,7 @@ type StatusArgs = { sessionEntry?: SessionEntry; sessionKey?: string; sessionScope?: SessionScope; - groupActivation?: "mention" | "always"; + groupActivation?: "mention" | "always" | "replies" | "mention+replies" | "never"; resolvedThink?: ThinkLevel; resolvedVerbose?: VerboseLevel; resolvedReasoning?: ReasoningLevel; diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 48ce428c1..11f691a90 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -54,7 +54,7 @@ export type SessionEntry = { authProfileOverride?: string; authProfileOverrideSource?: "auto" | "user"; authProfileOverrideCompactionCount?: number; - groupActivation?: "mention" | "always"; + groupActivation?: "mention" | "always" | "replies" | "mention+replies" | "never"; groupActivationNeedsSystemIntro?: boolean; sendPolicy?: "allow" | "deny"; queueMode?: diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 46b5e7c40..852882cda 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -303,7 +303,9 @@ export async function applySessionsPatchToStore(params: { } else if (raw !== undefined) { const normalized = normalizeGroupActivation(String(raw)); if (!normalized) { - return invalid('invalid groupActivation (use "mention"|"always")'); + return invalid( + 'invalid groupActivation (use "mention"|"always"|"replies"|"mention+replies"|"never")', + ); } next.groupActivation = normalized; } From 9f96b6146858a56b58bf71bc0b1da5320d45b862 Mon Sep 17 00:00:00 2001 From: moltbot-bot Date: Thu, 29 Jan 2026 17:43:45 -0300 Subject: [PATCH 4/4] fix(whatsapp): safe handling for new activation modes (reply/never) and explicit mention gating --- src/web/auto-reply/monitor/group-gating.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/web/auto-reply/monitor/group-gating.ts b/src/web/auto-reply/monitor/group-gating.ts index 44df28fe7..5f2be9e0c 100644 --- a/src/web/auto-reply/monitor/group-gating.ts +++ b/src/web/auto-reply/monitor/group-gating.ts @@ -114,30 +114,34 @@ export function applyGroupGating(params: { (selfE164 && replySenderE164 && selfE164 === replySenderE164), ); + // Ensure safe default for shouldBypassMention in case it's undefined in some contexts + const safeShouldBypassMention = typeof shouldBypassMention !== "undefined" ? shouldBypassMention : false; + // 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 === "never") return safeShouldBypassMention; + if (activation === "replies") return isReplyToBot || safeShouldBypassMention; if (activation === "mention+replies") - return wasMentioned || isReplyToBot || shouldBypassMention; + return wasMentioned || isReplyToBot || safeShouldBypassMention; // Default to "mention" mode - return wasMentioned || shouldBypassMention; + return wasMentioned || safeShouldBypassMention; })(); - const requireMention = activation !== "always" && activation !== "replies"; + // require mention only in strict 'mention' mode + const requireMention = activation === "mention"; const mentionGate = resolveMentionGating({ requireMention, canDetectMention: true, wasMentioned, - implicitMention: isReplyToBot, - shouldBypassMention, + implicitMention: isReplyToBot, // treat reply-to-bot as an implicit mention + shouldBypassMention: safeShouldBypassMention, }); params.msg.wasMentioned = mentionGate.effectiveWasMentioned; if (!shouldProcess) { params.logVerbose( - `Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`, + `Group message stored for context (no activation pass) in ${params.conversationId}: ${params.msg.body}`, ); const sender = params.msg.senderName && params.msg.senderE164