feat(whatsapp): add per-session ackReaction override

Allows controlling auto-reactions (ack emoji) per-session via sessions.json.
Session-level ackReaction can be set to 'always', 'mentions', or 'never'
to override global direct/group settings.

- Add sessionMode parameter to shouldAckReactionForWhatsApp
- Load session config in maybeSendAckReaction to check override
- Add ackReaction field to SessionEntry type
- Add tests for session override behavior
- Document per-session override in whatsapp.md
This commit is contained in:
Keith the Silly Goose 2026-01-28 22:23:54 +13:00
parent fdcac0ccf4
commit 387ab475e2
5 changed files with 99 additions and 0 deletions

View File

@ -239,6 +239,16 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- `"mentions"`: React only when bot is @mentioned
- `"never"`: Never react in groups
**Per-session override:**
The global and per-account configurations can be overridden for specific sessions (DMs or Groups) using session configuration (in `sessions.json`).
- `ackReaction`: `"always" | "mentions" | "never"`
- `"always"`: Enables reactions (overrides global off/never).
- `"never"`: Disables reactions (overrides global on/always/mentions).
- `"mentions"`: Enforces mention-only reactions.
- Useful for disabling reactions in specific DMs (`"never"`) or forcing them in specific groups.
> **Note:** Setting `"mentions"` on a DM session effectively disables reactions for that DM, since direct messages don't have @mentions. Use `"always"` or `"never"` for DMs.
**Per-account override:**
```json
{

View File

@ -225,6 +225,63 @@ describe("shouldAckReactionForWhatsApp", () => {
}),
).toBe(false);
});
it("honors session overrides", () => {
// Session: NEVER overrides Global: ALWAYS
expect(
shouldAckReactionForWhatsApp({
emoji: "👀",
isDirect: false,
isGroup: true,
directEnabled: true,
groupMode: "always",
wasMentioned: false,
groupActivated: false,
sessionMode: "never",
}),
).toBe(false);
// Session: ALWAYS overrides Global: NEVER
expect(
shouldAckReactionForWhatsApp({
emoji: "👀",
isDirect: false,
isGroup: true,
directEnabled: true,
groupMode: "never",
wasMentioned: false,
groupActivated: false,
sessionMode: "always",
}),
).toBe(true);
// Session: MENTIONS in DM (effectively OFF unless mentioned, which is false)
expect(
shouldAckReactionForWhatsApp({
emoji: "👀",
isDirect: true,
isGroup: false,
directEnabled: true, // Globally enabled
groupMode: "mentions",
wasMentioned: false,
groupActivated: false,
sessionMode: "mentions", // Override to strict mentions
}),
).toBe(false);
// Session: MENTIONS in Group (works if mentioned)
expect(
shouldAckReactionForWhatsApp({
emoji: "👀",
isDirect: false,
isGroup: true,
directEnabled: true,
groupMode: "never", // Global OFF
wasMentioned: true, // But mentioned
groupActivated: false,
sessionMode: "mentions", // Session override ON (if mentioned)
}),
).toBe(true);
});
});
describe("removeAckReactionAfterReply", () => {

View File

@ -36,8 +36,27 @@ export function shouldAckReactionForWhatsApp(params: {
groupMode: WhatsAppAckReactionMode;
wasMentioned: boolean;
groupActivated: boolean;
sessionMode?: "always" | "mentions" | "never";
}): boolean {
if (!params.emoji) return false;
// Session override
if (params.sessionMode === "never") return false;
if (params.sessionMode === "always") return true;
if (params.sessionMode === "mentions") {
return shouldAckReaction({
scope: "group-mentions",
isDirect: params.isDirect,
isGroup: params.isGroup,
isMentionableGroup: true,
requireMention: true,
canDetectMention: true,
effectiveWasMentioned: params.wasMentioned,
shouldBypassMention: params.groupActivated,
});
}
// Config fallback
if (params.isDirect) return params.directEnabled;
if (!params.isGroup) return false;
if (params.groupMode === "never") return false;

View File

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

View File

@ -1,4 +1,5 @@
import type { loadConfig } from "../../../config/config.js";
import { loadSessionStore, resolveStorePath } from "../../../config/sessions.js";
import { logVerbose } from "../../../globals.js";
import { shouldAckReactionForWhatsApp } from "../../../channels/ack-reactions.js";
import { sendReactionWhatsApp } from "../../outbound.js";
@ -25,6 +26,16 @@ export function maybeSendAckReaction(params: {
const groupMode = ackConfig?.group ?? "mentions";
const conversationIdForCheck = params.msg.conversationId ?? params.msg.from;
// Load session to check for per-session ackReaction override.
// Note: loadSessionStore uses an in-memory cache (45s TTL) so this is not
// doing disk I/O on every message in typical usage patterns.
const storePath = resolveStorePath(params.cfg.session?.store, {
agentId: params.agentId,
});
const store = loadSessionStore(storePath);
const sessionEntry = store[params.sessionKey];
const sessionMode = sessionEntry?.ackReaction;
const activation =
params.msg.chatType === "group"
? resolveGroupActivationFor({
@ -41,6 +52,7 @@ export function maybeSendAckReaction(params: {
isGroup: params.msg.chatType === "group",
directEnabled,
groupMode,
sessionMode,
wasMentioned: params.msg.wasMentioned === true,
groupActivated: activation === "always",
});