fix: store group messages from non-allowlisted senders as context

When groupPolicy is 'allowlist', messages from senders not in
groupAllowFrom were completely dropped (allowed: false + continue).
This meant the agent couldn't see messages from other bots or
non-allowlisted group members in its context window.

The docs state that non-mentioned messages should be 'stored for
context only', but this only applied after access control passed.

Changes:
- Add storeForContext flag to InboundAccessControlResult
- When a group message fails groupAllowFrom, set storeForContext: true
  instead of silently dropping
- Pass contextOnly flag through to WebInboundMessage
- In on-message handler, store contextOnly messages in group history
  without triggering a reply
- Skip read receipts for context-only messages

Fixes: group members not in groupAllowFrom are now visible in the
agent's '[Chat messages since your last reply]' context block.
This commit is contained in:
Adam Holt 2026-01-30 05:08:04 +00:00
parent 4de0bae45a
commit 85cc968aab
4 changed files with 42 additions and 3 deletions

View File

@ -94,6 +94,32 @@ export function createWebOnMessageHandler(params: {
return;
}
// Context-only messages: store for group history but never trigger a reply.
// This happens when a group message passes group allowlist checks for the group
// itself but the sender is not in groupAllowFrom.
if (msg.contextOnly && msg.chatType === "group") {
const sender =
msg.senderName && msg.senderE164
? `${msg.senderName} (${msg.senderE164})`
: (msg.senderName ?? msg.senderE164 ?? "Unknown");
logVerbose(`Storing context-only group message from ${sender} in ${conversationId}`);
const { recordPendingHistoryEntryIfEnabled } =
await import("../../../auto-reply/reply/history.js");
recordPendingHistoryEntryIfEnabled({
historyMap: params.groupHistories,
historyKey: groupHistoryKey,
limit: params.groupHistoryLimit,
entry: {
sender,
body: msg.body,
timestamp: msg.timestamp,
id: msg.id,
senderJid: msg.senderJid,
},
});
return;
}
if (msg.chatType === "group") {
const metaCtx = {
From: msg.from,

View File

@ -10,6 +10,8 @@ import { resolveWhatsAppAccount } from "../accounts.js";
export type InboundAccessControlResult = {
allowed: boolean;
/** When true the message should be stored as pending group context even though it did not pass sender allowlists. */
storeForContext: boolean;
shouldMarkRead: boolean;
isSelfChat: boolean;
resolvedAccountId: string;
@ -84,6 +86,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked group message (groupPolicy: disabled)");
return {
allowed: false,
storeForContext: false,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -94,6 +97,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked group message (groupPolicy: allowlist, no groupAllowFrom)");
return {
allowed: false,
storeForContext: false,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -104,10 +108,11 @@ export async function checkInboundAccessControl(params: {
(params.senderE164 != null && normalizedGroupAllowFrom.includes(params.senderE164));
if (!senderAllowed) {
logVerbose(
`Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`,
`Group message from ${params.senderE164 ?? "unknown sender"} not in groupAllowFrom (storing for context)`,
);
return {
allowed: false,
storeForContext: true,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -121,6 +126,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Skipping outbound DM (fromMe); no pairing reply needed.");
return {
allowed: false,
storeForContext: false,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -130,6 +136,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked dm (dmPolicy: disabled)");
return {
allowed: false,
storeForContext: false,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -172,6 +179,7 @@ export async function checkInboundAccessControl(params: {
}
return {
allowed: false,
storeForContext: false,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
@ -182,6 +190,7 @@ export async function checkInboundAccessControl(params: {
return {
allowed: true,
storeForContext: false,
shouldMarkRead: true,
isSelfChat,
resolvedAccountId: account.accountId,

View File

@ -190,9 +190,10 @@ export async function monitorWebInbox(options: {
sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) },
remoteJid,
});
if (!access.allowed) continue;
if (!access.allowed && !access.storeForContext) continue;
const contextOnly = !access.allowed && access.storeForContext;
if (id && !access.isSelfChat && options.sendReadReceipts !== false) {
if (id && !contextOnly && !access.isSelfChat && options.sendReadReceipts !== false) {
const participant = msg.key?.participant;
try {
await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]);
@ -298,6 +299,7 @@ export async function monitorWebInbox(options: {
sendMedia,
mediaPath,
mediaType,
contextOnly,
};
try {
const task = Promise.resolve(debouncer.enqueue(inboundMessage));

View File

@ -39,4 +39,6 @@ export type WebInboundMessage = {
mediaType?: string;
mediaUrl?: string;
wasMentioned?: boolean;
/** When true, message should only be stored for group context — not trigger a reply. */
contextOnly?: boolean;
};