fix: limit status reaction restore cleanup

This commit is contained in:
Peter Steinberger
2026-05-01 11:59:55 +01:00
parent b3e6f8c5ee
commit f659244fe4
3 changed files with 27 additions and 19 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -160,21 +160,6 @@ export function createStatusReactionController(params: {
let chainPromise = Promise.resolve();
const activeEmojis = new Set<string>();
// Known emojis for clear operation
const knownEmojis = new Set<string>([
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<void> {
async function removeActiveEmojis(options: { keepEmoji?: string } = {}): Promise<void> {
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 = "";
});
}