fix: remove ack reactions after reply (#633) (thanks @levifig)

This commit is contained in:
Peter Steinberger
2026-01-10 02:11:51 +01:00
parent b5858c0148
commit 38e2362be6
8 changed files with 108 additions and 74 deletions

View File

@@ -27,6 +27,7 @@
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc - Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro - Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
- Slack: configurable reply threading (`slack.replyToMode`) + proper mrkdwn formatting for outbound messages. (#464) — thanks @austinm911 - Slack: configurable reply threading (`slack.replyToMode`) + proper mrkdwn formatting for outbound messages. (#464) — thanks @austinm911
- Providers: remove ack reactions after reply on Discord/Slack/Telegram. (#633) — thanks @levifig
- Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete - Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete
- Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow - Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow
- Discord: log gateway disconnect/reconnect events at info and add verbose gateway metrics. (#595) — thanks @steipete - Discord: log gateway disconnect/reconnect events at info and add verbose gateway metrics. (#595) — thanks @steipete

View File

@@ -948,7 +948,8 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
messages: { messages: {
responsePrefix: "🦞", // or "auto" responsePrefix: "🦞", // or "auto"
ackReaction: "👀", ackReaction: "👀",
ackReactionScope: "group-mentions" ackReactionScope: "group-mentions",
removeAckAfterReply: false
} }
} }
``` ```
@@ -975,6 +976,9 @@ active agents `identity.emoji` when set, otherwise `"👀"`. Set it to `""` t
- `direct`: direct messages only - `direct`: direct messages only
- `all`: all messages - `all`: all messages
`removeAckAfterReply` removes the bots ack reaction after a reply is sent
(Slack/Discord/Telegram only). Default: `false`.
### `talk` ### `talk`
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset. Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.

View File

@@ -225,7 +225,8 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
``` ```
Ack reactions are controlled globally via `messages.ackReaction` + Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`. `messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
ack reaction after the bot replies.
- `dm.enabled`: set `false` to ignore all DMs (default `true`). - `dm.enabled`: set `false` to ignore all DMs (default `true`).
- `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`. - `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`.

View File

@@ -192,7 +192,8 @@ Tokens can also be supplied via env vars:
- `SLACK_APP_TOKEN` - `SLACK_APP_TOKEN`
Ack reactions are controlled globally via `messages.ackReaction` + Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`. `messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
ack reaction after the bot replies.
## Limits ## Limits
- Outbound text is chunked to `slack.textChunkLimit` (default 4000). - Outbound text is chunked to `slack.textChunkLimit` (default 4000).

View File

@@ -287,4 +287,4 @@ Related global options:
- `agents.list[].groupChat.mentionPatterns` (mention gating patterns). - `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
- `messages.groupChat.mentionPatterns` (global fallback). - `messages.groupChat.mentionPatterns` (global fallback).
- `commands.native`, `commands.text`, `commands.useAccessGroups` (command behavior). - `commands.native`, `commands.text`, `commands.useAccessGroups` (command behavior).
- `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`. - `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`.

View File

@@ -977,17 +977,19 @@ export function createDiscordMessageHandler(params: {
} }
return false; return false;
}; };
let didAddAckReaction = false; const ackReactionPromise = shouldAckReaction()
if (shouldAckReaction()) { ? reactMessageDiscord(message.channelId, message.id, ackReaction, {
reactMessageDiscord(message.channelId, message.id, ackReaction, { rest: client.rest,
rest: client.rest, }).then(
}).catch((err) => { () => true,
logVerbose( (err) => {
`discord react failed for channel ${message.channelId}: ${String(err)}`, logVerbose(
); `discord react failed for channel ${message.channelId}: ${String(err)}`,
}); );
didAddAckReaction = true; return false;
} },
)
: null;
const fromLabel = isDirectMessage const fromLabel = isDirectMessage
? buildDirectLabel(author) ? buildDirectLabel(author)
@@ -1208,13 +1210,22 @@ export function createDiscordMessageHandler(params: {
`discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`, `discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
); );
} }
if (removeAckAfterReply && didAddAckReaction && ackReaction) { if (removeAckAfterReply && ackReactionPromise && ackReaction) {
removeReactionDiscord(message.channelId, message.id, ackReaction, { const ackReactionValue = ackReaction;
rest: client.rest, void ackReactionPromise.then((didAck) => {
}).catch((err) => { if (!didAck) return;
logVerbose( removeReactionDiscord(
`discord: failed to remove ack reaction from ${message.channelId}/${message.id}: ${String(err)}`, message.channelId,
); message.id,
ackReactionValue,
{
rest: client.rest,
},
).catch((err) => {
logVerbose(
`discord: failed to remove ack reaction from ${message.channelId}/${message.id}: ${String(err)}`,
);
});
}); });
} }
if ( if (

View File

@@ -913,6 +913,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
const rawBody = (message.text ?? "").trim() || media?.placeholder || ""; const rawBody = (message.text ?? "").trim() || media?.placeholder || "";
if (!rawBody) return; if (!rawBody) return;
const ackReaction = resolveAckReaction(cfg, route.agentId); const ackReaction = resolveAckReaction(cfg, route.agentId);
const ackReactionValue = ackReaction ?? "";
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
const shouldAckReaction = () => { const shouldAckReaction = () => {
if (!ackReaction) return false; if (!ackReaction) return false;
@@ -928,18 +929,27 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
} }
return false; return false;
}; };
let didAddAckReaction = false; const ackReactionMessageTs = message.ts;
if (shouldAckReaction() && message.ts) { const ackReactionPromise =
reactSlackMessage(message.channel, message.ts, ackReaction, { shouldAckReaction() && ackReactionMessageTs && ackReactionValue
token: botToken, ? reactSlackMessage(
client: app.client, message.channel,
}).catch((err) => { ackReactionMessageTs,
logVerbose( ackReactionValue,
`slack react failed for channel ${message.channel}: ${String(err)}`, {
); token: botToken,
}); client: app.client,
didAddAckReaction = true; },
} ).then(
() => true,
(err) => {
logVerbose(
`slack react failed for channel ${message.channel}: ${String(err)}`,
);
return false;
},
)
: null;
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
@@ -1160,14 +1170,18 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
`slack: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`, `slack: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
); );
} }
if (removeAckAfterReply && didAddAckReaction && ackReaction && message.ts) { if (removeAckAfterReply && ackReactionPromise && ackReactionMessageTs) {
removeSlackReaction(message.channel, message.ts, ackReaction, { const messageTs = ackReactionMessageTs;
token: botToken, void ackReactionPromise.then((didAck) => {
client: app.client, if (!didAck) return;
}).catch((err) => { removeSlackReaction(message.channel, messageTs, ackReactionValue, {
logVerbose( token: botToken,
`slack: failed to remove ack reaction from ${message.channel}/${message.ts}: ${String(err)}`, client: app.client,
); }).catch((err) => {
logVerbose(
`slack: failed to remove ack reaction from ${message.channel}/${message.ts}: ${String(err)}`,
);
});
}); });
} }
}; };

View File

@@ -591,28 +591,31 @@ export function createTelegramBot(opts: TelegramBotOptions) {
} }
return false; return false;
}; };
let didAddAckReaction = false; const api = bot.api as unknown as {
if (shouldAckReaction() && msg.message_id) { setMessageReaction?: (
const api = bot.api as unknown as { chatId: number | string,
setMessageReaction?: ( messageId: number,
chatId: number | string, reactions: Array<{ type: "emoji"; emoji: string }>,
messageId: number, ) => Promise<void>;
reactions: Array<{ type: "emoji"; emoji: string }>, };
) => Promise<void>; const reactionApi =
}; typeof api.setMessageReaction === "function"
if (typeof api.setMessageReaction === "function") { ? api.setMessageReaction.bind(api)
api : null;
.setMessageReaction(chatId, msg.message_id, [ const ackReactionPromise =
shouldAckReaction() && msg.message_id && reactionApi
? reactionApi(chatId, msg.message_id, [
{ type: "emoji", emoji: ackReaction }, { type: "emoji", emoji: ackReaction },
]) ]).then(
.catch((err) => { () => true,
logVerbose( (err) => {
`telegram react failed for chat ${chatId}: ${String(err)}`, logVerbose(
); `telegram react failed for chat ${chatId}: ${String(err)}`,
}); );
didAddAckReaction = true; return false;
} },
} )
: null;
let placeholder = ""; let placeholder = "";
if (msg.photo) placeholder = "<media:image>"; if (msg.photo) placeholder = "<media:image>";
@@ -857,21 +860,20 @@ export function createTelegramBot(opts: TelegramBotOptions) {
markDispatchIdle(); markDispatchIdle();
draftStream?.stop(); draftStream?.stop();
if (!queuedFinal) return; if (!queuedFinal) return;
if (removeAckAfterReply && didAddAckReaction && msg.message_id) { if (
const api = bot.api as unknown as { removeAckAfterReply &&
setMessageReaction?: ( ackReactionPromise &&
chatId: number | string, msg.message_id &&
messageId: number, reactionApi
reactions: Array<{ type: "emoji"; emoji: string }>, ) {
) => Promise<void>; void ackReactionPromise.then((didAck) => {
}; if (!didAck) return;
if (typeof api.setMessageReaction === "function") { reactionApi(chatId, msg.message_id, []).catch((err) => {
api.setMessageReaction(chatId, msg.message_id, []).catch((err) => {
logVerbose( logVerbose(
`telegram: failed to remove ack reaction from ${chatId}/${msg.message_id}: ${String(err)}`, `telegram: failed to remove ack reaction from ${chatId}/${msg.message_id}: ${String(err)}`,
); );
}); });
} });
} }
}; };