diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index c0ffee77815..e2ddb143407 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -560,7 +560,6 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag }; let dispatchError: unknown; - let didAdvanceStatusReaction = false; let queuedFinal = false; let counts: { final?: number; block?: number } = {}; try { @@ -589,13 +588,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag onReasoningEnd: onDraftBoundary, onReasoningStream: statusReactionsEnabled ? async () => { - didAdvanceStatusReaction = true; await statusReactions.setThinking(); } : undefined, onToolStart: statusReactionsEnabled ? async (payload) => { - didAdvanceStatusReaction = true; await statusReactions.setTool(payload.name); } : undefined, @@ -646,9 +643,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag } else { void statusReactions.restoreInitial(); } - } else if (didAdvanceStatusReaction) { - // Silent success should preserve the original ack instead of looking like - // we delivered a visible reply. + } else { + // Silent success should preserve queued state and clear any stall timers + // instead of transitioning to terminal/stall reactions after return. await statusReactions.restoreInitial(); } } diff --git a/src/channels/status-reactions.slack-lifecycle.test.ts b/src/channels/status-reactions.slack-lifecycle.test.ts index 54918267153..629ee773743 100644 --- a/src/channels/status-reactions.slack-lifecycle.test.ts +++ b/src/channels/status-reactions.slack-lifecycle.test.ts @@ -94,6 +94,29 @@ describe("Slack status reaction lifecycle", () => { expect(active.has(DEFAULT_EMOJIS.error)).toBe(false); }); + it("restoreInitial clears stall timers without re-adding queued emoji", async () => { + const { adapter, active } = createSlackMockAdapter(); + const ctrl = createStatusReactionController({ + enabled: true, + adapter, + initialEmoji: "eyes", + timing: { debounceMs: 0, stallSoftMs: 10, stallHardMs: 20 }, + }); + + void ctrl.setQueued(); + await vi.advanceTimersByTimeAsync(1); + expect(active.has("eyes")).toBe(true); + expect(adapter.setReaction).toHaveBeenCalledTimes(1); + + await ctrl.restoreInitial(); + await vi.advanceTimersByTimeAsync(30); + + expect(adapter.setReaction).toHaveBeenCalledTimes(1); + expect(active.has("eyes")).toBe(true); + expect(active.has(DEFAULT_EMOJIS.stallSoft)).toBe(false); + expect(active.has(DEFAULT_EMOJIS.stallHard)).toBe(false); + }); + it("does nothing when disabled", async () => { const { adapter, active } = createSlackMockAdapter(); const ctrl = createStatusReactionController({ diff --git a/src/channels/status-reactions.ts b/src/channels/status-reactions.ts index 060555a997c..475b4cd1f27 100644 --- a/src/channels/status-reactions.ts +++ b/src/channels/status-reactions.ts @@ -379,7 +379,14 @@ export function createStatusReactionController(params: { return; } + const alreadyInitial = currentEmoji === initialEmoji; + const initialAlreadyPending = pendingEmoji === initialEmoji; clearAllTimers(); + if (alreadyInitial || initialAlreadyPending) { + pendingEmoji = ""; + return; + } + await enqueue(async () => { await applyEmoji(initialEmoji); pendingEmoji = "";