mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:41:33 +00:00
fix(channels): ack mentions in groups that do not require them
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.
This commit is contained in:
@@ -1331,6 +1331,7 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
|
||||
- Per-channel overrides: `channels.<channel>.ackReaction`, `channels.<channel>.accounts.<id>.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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -334,7 +334,6 @@ export async function resolveMatrixInboundContext(config: {
|
||||
isDirect: isDirectMessage,
|
||||
isGroup: isRoom,
|
||||
isMentionableGroup: isRoom,
|
||||
requireMention: shouldRequireMention,
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -1348,7 +1348,6 @@ export async function prepareSlackMessage(params: {
|
||||
isDirect: isDirectMessage,
|
||||
isGroup: isRoomish,
|
||||
isMentionableGroup: isRoom,
|
||||
requireMention: shouldRequireMention,
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -326,7 +326,7 @@ export const AUTOMATION_FIELD_HELP: Record<string, string> = {
|
||||
"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":
|
||||
|
||||
Reference in New Issue
Block a user