This commit is contained in:
Rodrigo Gomes da Silva 2026-01-30 13:47:39 -03:00 committed by GitHub
commit 960c321094
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 45 additions and 15 deletions

View File

@ -200,7 +200,10 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
- Activation modes: - Activation modes:
- `mention` (default): requires @mention or regex match. - `mention` (default): requires @mention or regex match.
- `always`: always triggers. - `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). - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
- **History injection** (pending-only): - **History injection** (pending-only):
- Recent *unprocessed* messages (default 50) inserted under: - Recent *unprocessed* messages (default 50) inserted under:

View File

@ -21,7 +21,10 @@ export function getAvailableCommands(): AvailableCommand[] {
{ name: "dock-telegram", description: "Route replies to Telegram." }, { name: "dock-telegram", description: "Route replies to Telegram." },
{ name: "dock-discord", description: "Route replies to Discord." }, { name: "dock-discord", description: "Route replies to Discord." },
{ name: "dock-slack", description: "Route replies to Slack." }, { 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: "send", description: "Set send mode (on|off|inherit)." },
{ name: "reset", description: "Reset the session (/new)." }, { name: "reset", description: "Reset the session (/new)." },
{ name: "new", description: "Reset the session (/reset)." }, { name: "new", description: "Reset the session (/reset)." },

View File

@ -1,11 +1,14 @@
import { normalizeCommandBody } from "./commands-registry.js"; 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 { export function normalizeGroupActivation(raw?: string | null): GroupActivationMode | undefined {
const value = raw?.trim().toLowerCase(); const value = raw?.trim().toLowerCase();
if (value === "mention") return "mention"; if (value === "mention") return "mention";
if (value === "always") return "always"; if (value === "always") return "always";
if (value === "replies") return "replies";
if (value === "mention+replies") return "mention+replies";
if (value === "never") return "never";
return undefined; return undefined;
} }

View File

@ -66,7 +66,7 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC
if (!activationCommand.mode) { if (!activationCommand.mode) {
return { return {
shouldContinue: false, 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) { if (params.sessionEntry && params.sessionStore && params.sessionKey) {

View File

@ -59,7 +59,7 @@ type StatusArgs = {
sessionEntry?: SessionEntry; sessionEntry?: SessionEntry;
sessionKey?: string; sessionKey?: string;
sessionScope?: SessionScope; sessionScope?: SessionScope;
groupActivation?: "mention" | "always"; groupActivation?: "mention" | "always" | "replies" | "mention+replies" | "never";
resolvedThink?: ThinkLevel; resolvedThink?: ThinkLevel;
resolvedVerbose?: VerboseLevel; resolvedVerbose?: VerboseLevel;
resolvedReasoning?: ReasoningLevel; resolvedReasoning?: ReasoningLevel;

View File

@ -54,7 +54,7 @@ export type SessionEntry = {
authProfileOverride?: string; authProfileOverride?: string;
authProfileOverrideSource?: "auto" | "user"; authProfileOverrideSource?: "auto" | "user";
authProfileOverrideCompactionCount?: number; authProfileOverrideCompactionCount?: number;
groupActivation?: "mention" | "always"; groupActivation?: "mention" | "always" | "replies" | "mention+replies" | "never";
groupActivationNeedsSystemIntro?: boolean; groupActivationNeedsSystemIntro?: boolean;
sendPolicy?: "allow" | "deny"; sendPolicy?: "allow" | "deny";
queueMode?: queueMode?:

View File

@ -303,7 +303,9 @@ export async function applySessionsPatchToStore(params: {
} else if (raw !== undefined) { } else if (raw !== undefined) {
const normalized = normalizeGroupActivation(String(raw)); const normalized = normalizeGroupActivation(String(raw));
if (!normalized) { if (!normalized) {
return invalid('invalid groupActivation (use "mention"|"always")'); return invalid(
'invalid groupActivation (use "mention"|"always"|"replies"|"mention+replies"|"never")',
);
} }
next.groupActivation = normalized; next.groupActivation = normalized;
} }

View File

@ -150,7 +150,7 @@ export function helpText(options: SlashCommandOptions = {}): string {
"/usage <off|tokens|full>", "/usage <off|tokens|full>",
"/elevated <on|off|ask|full>", "/elevated <on|off|ask|full>",
"/elev <on|off|ask|full>", "/elev <on|off|ask|full>",
"/activation <mention|always>", "/activation <mention|always|replies|mention+replies|never>",
"/new or /reset", "/new or /reset",
"/abort", "/abort",
"/settings", "/settings",

View File

@ -391,7 +391,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
break; break;
case "activation": case "activation":
if (!args) { if (!args) {
chatLog.addSystem("usage: /activation <mention|always>"); chatLog.addSystem("usage: /activation <mention|always|replies|mention+replies|never>");
break; break;
} }
try { try {

View File

@ -101,28 +101,47 @@ export function applyGroupGating(params: {
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
conversationId: params.conversationId, 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 selfJid = params.msg.selfJid?.replace(/:\\d+/, "");
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, ""); const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, "");
const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null; const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null;
const replySenderE164 = params.msg.replyToSenderE164 const replySenderE164 = params.msg.replyToSenderE164
? normalizeE164(params.msg.replyToSenderE164) ? normalizeE164(params.msg.replyToSenderE164)
: null; : null;
const implicitMention = Boolean( const isReplyToBot = Boolean(
(selfJid && replySenderJid && selfJid === replySenderJid) || (selfJid && replySenderJid && selfJid === replySenderJid) ||
(selfE164 && replySenderE164 && selfE164 === replySenderE164), (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 safeShouldBypassMention;
if (activation === "replies") return isReplyToBot || safeShouldBypassMention;
if (activation === "mention+replies")
return wasMentioned || isReplyToBot || safeShouldBypassMention;
// Default to "mention" mode
return wasMentioned || safeShouldBypassMention;
})();
// require mention only in strict 'mention' mode
const requireMention = activation === "mention";
const mentionGate = resolveMentionGating({ const mentionGate = resolveMentionGating({
requireMention, requireMention,
canDetectMention: true, canDetectMention: true,
wasMentioned, wasMentioned,
implicitMention, implicitMention: isReplyToBot, // treat reply-to-bot as an implicit mention
shouldBypassMention, shouldBypassMention: safeShouldBypassMention,
}); });
params.msg.wasMentioned = mentionGate.effectiveWasMentioned; params.msg.wasMentioned = mentionGate.effectiveWasMentioned;
if (!shouldBypassMention && requireMention && mentionGate.shouldSkip) {
if (!shouldProcess) {
params.logVerbose( 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 = const sender =
params.msg.senderName && params.msg.senderE164 params.msg.senderName && params.msg.senderE164