diff --git a/src/config/types.ts b/src/config/types.ts index bbedafe72..2342da12c 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -999,6 +999,8 @@ export type MessagesConfig = { ackReaction?: string; /** When to send ack reactions. Default: "group-mentions". */ ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all"; + /** Remove ack reaction after reply is sent (default: false). */ + removeAckAfterReply?: boolean; }; export type CommandsConfig = { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 56266928e..36a808889 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -603,6 +603,7 @@ const MessagesSchema = z ackReactionScope: z .enum(["group-mentions", "group-all", "direct", "all"]) .optional(), + removeAckAfterReply: z.boolean().optional(), }) .optional(); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index dc30ab682..d6276ac9d 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -74,7 +74,11 @@ import { waitForDiscordGatewayStop, } from "./monitor.gateway.js"; import { fetchDiscordApplicationId } from "./probe.js"; -import { reactMessageDiscord, sendMessageDiscord } from "./send.js"; +import { + reactMessageDiscord, + removeReactionDiscord, + sendMessageDiscord, +} from "./send.js"; import { normalizeDiscordToken } from "./token.js"; export type MonitorDiscordOpts = { @@ -958,6 +962,7 @@ export function createDiscordMessageHandler(params: { return; } const ackReaction = resolveAckReaction(cfg, route.agentId); + const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const shouldAckReaction = () => { if (!ackReaction) return false; if (ackReactionScope === "all") return true; @@ -972,6 +977,7 @@ export function createDiscordMessageHandler(params: { } return false; }; + let didAddAckReaction = false; if (shouldAckReaction()) { reactMessageDiscord(message.channelId, message.id, ackReaction, { rest: client.rest, @@ -980,6 +986,7 @@ export function createDiscordMessageHandler(params: { `discord react failed for channel ${message.channelId}: ${String(err)}`, ); }); + didAddAckReaction = true; } const fromLabel = isDirectMessage @@ -1201,6 +1208,15 @@ export function createDiscordMessageHandler(params: { `discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`, ); } + if (removeAckAfterReply && didAddAckReaction && ackReaction) { + removeReactionDiscord(message.channelId, message.id, ackReaction, { + rest: client.rest, + }).catch((err) => { + logVerbose( + `discord: failed to remove ack reaction from ${message.channelId}/${message.id}: ${String(err)}`, + ); + }); + } if ( isGuildMessage && shouldClearHistory && diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index 5ddbd9de2..9589856a6 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -59,7 +59,7 @@ import { } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { resolveSlackAccount } from "./accounts.js"; -import { reactSlackMessage } from "./actions.js"; +import { reactSlackMessage, removeSlackReaction } from "./actions.js"; import { sendMessageSlack } from "./send.js"; import { resolveSlackThreadTargets } from "./threading.js"; import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js"; @@ -913,6 +913,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const rawBody = (message.text ?? "").trim() || media?.placeholder || ""; if (!rawBody) return; const ackReaction = resolveAckReaction(cfg, route.agentId); + const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const shouldAckReaction = () => { if (!ackReaction) return false; if (ackReactionScope === "all") return true; @@ -927,6 +928,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } return false; }; + let didAddAckReaction = false; if (shouldAckReaction() && message.ts) { reactSlackMessage(message.channel, message.ts, ackReaction, { token: botToken, @@ -936,6 +938,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { `slack react failed for channel ${message.channel}: ${String(err)}`, ); }); + didAddAckReaction = true; } const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; @@ -1157,6 +1160,16 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { `slack: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`, ); } + if (removeAckAfterReply && didAddAckReaction && ackReaction && message.ts) { + removeSlackReaction(message.channel, message.ts, ackReaction, { + token: botToken, + client: app.client, + }).catch((err) => { + logVerbose( + `slack: failed to remove ack reaction from ${message.channel}/${message.ts}: ${String(err)}`, + ); + }); + } }; app.event( diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 8ec6756b4..5631330a0 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -577,6 +577,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { // ACK reactions const ackReaction = resolveAckReaction(cfg, route.agentId); + const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const shouldAckReaction = () => { if (!ackReaction) return false; if (ackReactionScope === "all") return true; @@ -590,6 +591,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { } return false; }; + let didAddAckReaction = false; if (shouldAckReaction() && msg.message_id) { const api = bot.api as unknown as { setMessageReaction?: ( @@ -608,6 +610,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { `telegram react failed for chat ${chatId}: ${String(err)}`, ); }); + didAddAckReaction = true; } } @@ -854,6 +857,22 @@ export function createTelegramBot(opts: TelegramBotOptions) { markDispatchIdle(); draftStream?.stop(); if (!queuedFinal) return; + if (removeAckAfterReply && didAddAckReaction && msg.message_id) { + const api = bot.api as unknown as { + setMessageReaction?: ( + chatId: number | string, + messageId: number, + reactions: Array<{ type: "emoji"; emoji: string }>, + ) => Promise; + }; + if (typeof api.setMessageReaction === "function") { + api.setMessageReaction(chatId, msg.message_id, []).catch((err) => { + logVerbose( + `telegram: failed to remove ack reaction from ${chatId}/${msg.message_id}: ${String(err)}`, + ); + }); + } + } }; const nativeCommands = nativeEnabled ? listNativeCommandSpecs() : [];