From f659244fe4e6dcca1a61fb40a0c5d36fa6e044d9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 11:59:55 +0100 Subject: [PATCH] fix: limit status reaction restore cleanup --- CHANGELOG.md | 1 + .../status-reactions.slack-lifecycle.test.ts | 22 ++++++++++++++++++ src/channels/status-reactions.ts | 23 ++++--------------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 338bb5d6162..c2a532d9b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Discord/Slack: defer status-reaction cleanup until run finalization so queued, thinking, tool, and terminal reactions no longer flicker during normal progress updates. (#75582) - Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram. - Plugins/runtime-deps: prune legacy version-scoped plugin runtime-deps roots during bundled dependency repair and cover the path in Package Acceptance's upgrade-survivor matrix, so upgrades from 2026.4.x no longer leave stale per-plugin runtime trees after doctor runs. Thanks @vincentkoc. - Plugins/runtime-deps: keep Gateway startup plugin imports and runtime plugin fallback loads verify-only after startup/config repair planning, so packaged installs no longer spawn package-manager repair from hot paths after readiness. Refs #75283 and #75069. Thanks @brokemac79 and @xiaohuaxi. diff --git a/src/channels/status-reactions.slack-lifecycle.test.ts b/src/channels/status-reactions.slack-lifecycle.test.ts index 7c8441d77fb..1eebc1f4632 100644 --- a/src/channels/status-reactions.slack-lifecycle.test.ts +++ b/src/channels/status-reactions.slack-lifecycle.test.ts @@ -139,6 +139,28 @@ describe("Slack status reaction lifecycle", () => { expect(active.has(DEFAULT_EMOJIS.thinking)).toBe(false); }); + it("restoreInitial removes only tracked active reactions", async () => { + const { adapter, active } = createSlackMockAdapter(); + const ctrl = createStatusReactionController({ + enabled: true, + adapter, + initialEmoji: "eyes", + timing: { debounceMs: 0, stallSoftMs: 99999, stallHardMs: 99999 }, + }); + + void ctrl.setQueued(); + await vi.advanceTimersByTimeAsync(10); + await ctrl.setDone(); + + await ctrl.restoreInitial(); + + expect(active.has("eyes")).toBe(true); + expect(active.has(DEFAULT_EMOJIS.done)).toBe(false); + expect(adapter.removeReaction).toHaveBeenCalledTimes(1); + expect(adapter.removeReaction).toHaveBeenCalledWith(DEFAULT_EMOJIS.done); + expect(adapter.removeReaction).not.toHaveBeenCalledWith(DEFAULT_EMOJIS.thinking); + }); + it("restoreInitial still applies initial emoji when it is only debounced", async () => { const { adapter, active } = createSlackMockAdapter(); const ctrl = createStatusReactionController({ diff --git a/src/channels/status-reactions.ts b/src/channels/status-reactions.ts index 1426d8de708..27d1d00523d 100644 --- a/src/channels/status-reactions.ts +++ b/src/channels/status-reactions.ts @@ -160,21 +160,6 @@ export function createStatusReactionController(params: { let chainPromise = Promise.resolve(); const activeEmojis = new Set(); - // Known emojis for clear operation - const knownEmojis = new Set([ - initialEmoji, - emojis.queued, - emojis.thinking, - emojis.tool, - emojis.coding, - emojis.web, - emojis.done, - emojis.error, - emojis.stallSoft, - emojis.stallHard, - emojis.compacting, - ]); - /** * Serialize async operations to prevent race conditions. */ @@ -231,12 +216,12 @@ export function createStatusReactionController(params: { }, timing.stallHardMs); } - async function removeKnownEmojis(options: { keepEmoji?: string } = {}): Promise { + async function removeActiveEmojis(options: { keepEmoji?: string } = {}): Promise { if (!adapter.removeReaction) { return; } - for (const emoji of knownEmojis) { + for (const emoji of Array.from(activeEmojis)) { if (emoji === options.keepEmoji) { continue; } @@ -378,7 +363,7 @@ export function createStatusReactionController(params: { await enqueue(async () => { if (adapter.removeReaction) { - await removeKnownEmojis(); + await removeActiveEmojis(); } else { // For platforms without removeReaction, set empty or just skip // (Telegram handles this atomically on the next setReaction) @@ -409,7 +394,7 @@ export function createStatusReactionController(params: { await enqueue(async () => { await applyEmoji(initialEmoji); - await removeKnownEmojis({ keepEmoji: initialEmoji }); + await removeActiveEmojis({ keepEmoji: initialEmoji }); pendingEmoji = ""; }); }