diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 802960deda2..861ffe8dceb 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -190,14 +190,17 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag replyToMode: prepared.replyToMode, }); + const reactionMessageTs = prepared.ackReactionMessageTs; const messageTs = message.ts ?? message.event_ts; const incomingThreadTs = message.thread_ts; let didSetStatus = false; const statusReactionsEnabled = - Boolean(prepared.ackReactionPromise) && cfg.messages?.statusReactions?.enabled !== false; + Boolean(prepared.ackReactionPromise) && + Boolean(reactionMessageTs) && + cfg.messages?.statusReactions?.enabled !== false; const slackStatusAdapter: StatusReactionAdapter = { setReaction: async (emoji) => { - await reactSlackMessage(message.channel, message.ts ?? "", toSlackEmojiName(emoji), { + await reactSlackMessage(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), { token: ctx.botToken, client: ctx.app.client, }).catch((err) => { @@ -208,7 +211,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }); }, removeReaction: async (emoji) => { - await removeSlackReaction(message.channel, message.ts ?? "", toSlackEmojiName(emoji), { + await removeSlackReaction(message.channel, reactionMessageTs ?? "", toSlackEmojiName(emoji), { token: ctx.botToken, client: ctx.app.client, }).catch((err) => { diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index a57614afaeb..7da7786a943 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -214,6 +214,33 @@ describe("slack prepareSlackMessage inbound contract", () => { expectInboundContextContract(prepared!.ctxPayload as any); }); + it("does not enable Slack status reactions when the message timestamp is missing", async () => { + const slackCtx = createInboundSlackCtx({ + cfg: { + messages: { + ackReaction: "👀", + ackReactionScope: "all", + statusReactions: { enabled: true }, + }, + channels: { slack: { enabled: true } }, + } as OpenClawConfig, + }); + // oxlint-disable-next-line typescript/no-explicit-any + slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any; + + const prepared = await prepareMessageWith(slackCtx, defaultAccount, { + channel: "D123", + channel_type: "im", + user: "U1", + text: "hi", + event_ts: "1.000", + } as SlackMessageEvent); + + expect(prepared).toBeTruthy(); + expect(prepared?.ackReactionMessageTs).toBeUndefined(); + expect(prepared?.ackReactionPromise).toBeNull(); + }); + it("includes forwarded shared attachment text in raw body", async () => { const prepared = await prepareWithDefaultCtx( createSlackMessage({ diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 1d43c1324e1..a571f5e8ce7 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -556,7 +556,9 @@ export async function prepareSlackMessage(params: { const ackReactionMessageTs = message.ts; const statusReactionsWillHandle = - cfg.messages?.statusReactions?.enabled !== false && shouldAckReaction(); + Boolean(ackReactionMessageTs) && + cfg.messages?.statusReactions?.enabled !== false && + shouldAckReaction(); const ackReactionPromise = !statusReactionsWillHandle && shouldAckReaction() && ackReactionMessageTs && ackReactionValue ? reactSlackMessage(message.channel, ackReactionMessageTs, ackReactionValue, { diff --git a/src/channels/status-reactions.slack-lifecycle.test.ts b/src/channels/status-reactions.slack-lifecycle.test.ts index 52a5250b01b..54918267153 100644 --- a/src/channels/status-reactions.slack-lifecycle.test.ts +++ b/src/channels/status-reactions.slack-lifecycle.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createStatusReactionController, DEFAULT_EMOJIS, @@ -36,6 +36,10 @@ describe("Slack status reaction lifecycle", () => { vi.useFakeTimers(); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("queued -> thinking -> tool -> done -> clear", async () => { const { adapter, active, log } = createSlackMockAdapter(); const ctrl = createStatusReactionController({