From 21d35334601658ff5f624b1db2bebbfc74bbf699 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 30 Jul 2026 17:58:46 +0900 Subject: [PATCH] fix(channels): ack mentions in groups that do not require them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The group-mentions ack gate returned false whenever a group did not require mentions, conflating group policy with whether this message mentioned the agent. Under the default scope, mentioning the agent in a group configured to answer everything produced no ack at all, so the user had no signal the turn was picked up until the reply arrived. Behavior change on a default path: those groups now show the 👀 ack (and the lifecycle status reactions when messages.statusReactions.enabled is true) on mentions. Mention-required groups, group-all, unmentioned messages, and off/none are unchanged. The gate no longer reads requireMention, so the parameter is gone from the shared type and all channel call sites. Proven live on Telegram across three configurations, including a no-mention control confirming group-mentions did not become group-all. --- docs/gateway/config-agents.md | 1 + .../message-handler.process-reactions.ts | 2 - .../src/matrix/monitor/handler-context.ts | 1 - .../signal/src/monitor/event-handler.ts | 1 - .../src/monitor/message-handler/prepare.ts | 1 - .../telegram/src/bot-message-context.ts | 1 - src/channels/ack-reactions.test.ts | 41 +++++++++++++------ src/channels/ack-reactions.ts | 7 +--- src/config/schema.help.automation.ts | 2 +- 9 files changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 046614bd6f0d..34a465b16d6e 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -1331,6 +1331,7 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`. - Per-channel overrides: `channels..ackReaction`, `channels..accounts..ackReaction`. - Resolution order: account → channel → `messages.ackReaction` → identity fallback. - Scope: `group-mentions` (default), `group-all`, `direct`, `all`, or `off`/`none` (disables ack reactions entirely). +- `group-mentions` acks group messages that mention the agent, including in groups with `requireMention: false`. Use `group-all` to ack every group message. - `messages.statusReactions.enabled`: enables lifecycle status reactions on Slack, Discord, Signal, Telegram, and WhatsApp. On Discord, unset keeps status reactions enabled when ack reactions are active. On Slack, Signal, Telegram, and WhatsApp, set it explicitly to `true` to enable lifecycle status reactions. diff --git a/extensions/discord/src/monitor/message-handler.process-reactions.ts b/extensions/discord/src/monitor/message-handler.process-reactions.ts index 4676444d3b79..895e086ada6a 100644 --- a/extensions/discord/src/monitor/message-handler.process-reactions.ts +++ b/extensions/discord/src/monitor/message-handler.process-reactions.ts @@ -49,7 +49,6 @@ export function createDiscordMessageReactionRuntime(params: { isGuildMessage, isDirectMessage, isGroupDm, - shouldRequireMention, canDetectMention, effectiveWasMentioned, shouldBypassMention, @@ -67,7 +66,6 @@ export function createDiscordMessageReactionRuntime(params: { isDirect: isDirectMessage, isGroup: isGuildMessage || isGroupDm, isMentionableGroup: isGuildMessage, - requireMention: shouldRequireMention, canDetectMention, effectiveWasMentioned, shouldBypassMention, diff --git a/extensions/matrix/src/matrix/monitor/handler-context.ts b/extensions/matrix/src/matrix/monitor/handler-context.ts index acd9d5e29c56..cb489b96c3c5 100644 --- a/extensions/matrix/src/matrix/monitor/handler-context.ts +++ b/extensions/matrix/src/matrix/monitor/handler-context.ts @@ -334,7 +334,6 @@ export async function resolveMatrixInboundContext(config: { isDirect: isDirectMessage, isGroup: isRoom, isMentionableGroup: isRoom, - requireMention: shouldRequireMention, canDetectMention, effectiveWasMentioned, shouldBypassMention, diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 9eb0a6e3473c..f4b5c76235c4 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -344,7 +344,6 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { isDirect: !entry.isGroup, isGroup: entry.isGroup, isMentionableGroup: entry.isGroup, - requireMention: entry.requireMention === true, canDetectMention: entry.canDetectMention === true, effectiveWasMentioned: entry.wasMentioned === true, }), diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 946daaf1459d..434ff1f3d04a 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -1348,7 +1348,6 @@ export async function prepareSlackMessage(params: { isDirect: isDirectMessage, isGroup: isRoomish, isMentionableGroup: isRoom, - requireMention: shouldRequireMention, canDetectMention, effectiveWasMentioned, shouldBypassMention, diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index 3705613dc5f8..49e3ffe1f282 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -545,7 +545,6 @@ export const buildTelegramMessageContext = async ({ isDirect: !isGroup, isGroup, isMentionableGroup: isGroup, - requireMention: Boolean(requireMention), canDetectMention: bodyResult.canDetectMention, effectiveWasMentioned: bodyResult.effectiveWasMentioned, shouldBypassMention: bodyResult.shouldBypassMention, diff --git a/src/channels/ack-reactions.test.ts b/src/channels/ack-reactions.test.ts index 56001119944f..5e8f683ef77f 100644 --- a/src/channels/ack-reactions.test.ts +++ b/src/channels/ack-reactions.test.ts @@ -20,7 +20,6 @@ describe("shouldAckReaction", () => { isDirect: true, isGroup: false, isMentionableGroup: false, - requireMention: false, canDetectMention: false, effectiveWasMentioned: false, }), @@ -32,7 +31,6 @@ describe("shouldAckReaction", () => { isDirect: false, isGroup: true, isMentionableGroup: true, - requireMention: false, canDetectMention: false, effectiveWasMentioned: false, }), @@ -46,7 +44,6 @@ describe("shouldAckReaction", () => { isDirect: true, isGroup: true, isMentionableGroup: true, - requireMention: true, canDetectMention: true, effectiveWasMentioned: true, }), @@ -67,7 +64,6 @@ describe("shouldAckReaction", () => { isDirect: false, isGroup: true, isMentionableGroup: true, - requireMention: false, canDetectMention: true, effectiveWasMentioned: false, }), @@ -81,7 +77,6 @@ describe("shouldAckReaction", () => { isDirect: false, isGroup: true, isMentionableGroup: true, - requireMention: true, canDetectMention: true, effectiveWasMentioned: true, }), @@ -94,17 +89,13 @@ describe("shouldAckReaction", () => { isDirect: false, isGroup: true, isMentionableGroup: true, - requireMention: true, canDetectMention: true, effectiveWasMentioned: true, }; - expect( - shouldAckReaction({ - ...groupMentionsScope, - requireMention: false, - }), - ).toBe(false); + // A group that answers every message still acks the ones addressing the + // agent: whether the group requires a mention is a separate policy. + expect(shouldAckReaction(groupMentionsScope)).toBe(true); expect( shouldAckReaction({ @@ -137,6 +128,32 @@ describe("shouldAckReaction", () => { }); describe("shouldAckReactionForWhatsApp", () => { + it("acks a mention in a group that does not require mentions", () => { + // Regression: the gate used to return false whenever the group did not + // require mentions, so an explicitly mentioned message the agent answered + // got no ack under the default scope. + expect( + shouldAckReaction({ + scope: "group-mentions", + isDirect: false, + isGroup: true, + isMentionableGroup: true, + canDetectMention: true, + effectiveWasMentioned: true, + }), + ).toBe(true); + expect( + shouldAckReaction({ + scope: "group-mentions", + isDirect: false, + isGroup: true, + isMentionableGroup: true, + canDetectMention: true, + effectiveWasMentioned: false, + }), + ).toBe(false); + }); + it("respects direct and group modes", () => { expect( shouldAckReactionForWhatsApp({ diff --git a/src/channels/ack-reactions.ts b/src/channels/ack-reactions.ts index a4ac7adf0b9e..7f5185650483 100644 --- a/src/channels/ack-reactions.ts +++ b/src/channels/ack-reactions.ts @@ -26,7 +26,6 @@ export type AckReactionGateParams = { isDirect: boolean; isGroup: boolean; isMentionableGroup: boolean; - requireMention: boolean; canDetectMention: boolean; effectiveWasMentioned: boolean; shouldBypassMention?: boolean; @@ -56,12 +55,11 @@ export function shouldAckReaction(params: AckReactionGateParams): boolean { if (!params.isMentionableGroup) { return false; } - if (!params.requireMention) { - return false; - } if (!params.canDetectMention) { return false; } + // Whether the group *requires* a mention is a separate policy: a group that + // answers everything still acks the messages that address the agent. // Group activation can stand in for a literal mention when another gate already established // that this inbound message belongs to the active conversation. return params.effectiveWasMentioned || params.shouldBypassMention === true; @@ -101,7 +99,6 @@ export function shouldAckReactionForWhatsApp(params: { isDirect: false, isGroup: true, isMentionableGroup: true, - requireMention: true, canDetectMention: true, effectiveWasMentioned: params.wasMentioned, shouldBypassMention: params.groupActivated, diff --git a/src/config/schema.help.automation.ts b/src/config/schema.help.automation.ts index f42c29ab7ddc..ad508dba645d 100644 --- a/src/config/schema.help.automation.ts +++ b/src/config/schema.help.automation.ts @@ -326,7 +326,7 @@ export const AUTOMATION_FIELD_HELP: Record = { "When true, suppress ⚠️ tool-error warnings from being shown to the user. The agent already sees errors in context and can retry. Default: false.", "messages.ackReaction": "Emoji reaction used to acknowledge inbound messages (empty disables).", "messages.ackReactionScope": - 'When to send ack reactions ("group-mentions", "group-all", "direct", "all", "off", "none"). "off"/"none" disables ack reactions entirely.', + 'When to send ack reactions ("group-mentions", "group-all", "direct", "all", "off", "none"). "group-mentions" acks group messages that mention the agent, whether or not the group requires mentions; "group-all" acks every group message. "off"/"none" disables ack reactions entirely.', "messages.statusReactions": "Lifecycle status reactions that update the emoji on the trigger message as the agent progresses (queued → thinking → tool → done/error).", "messages.statusReactions.enabled":