fix(channels): clear stale terminal status reactions

This commit is contained in:
Peter Steinberger
2026-05-01 22:35:58 +01:00
parent d2ae2a3fb0
commit 4373103c22
4 changed files with 40 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.
- Gateway/config: allow `gateway config.patch` to update documented subagent thinking defaults. Fixes #75764. (#75802) Thanks @kAIborg24.
- Plugins/CLI: keep git plugin install paths credential-free, preserve existing git checkouts until replacement succeeds, honor duplicate npm install mode, and remove managed git repos on uninstall. Thanks @vincentkoc.
- Channels/status reactions: remove stale non-terminal lifecycle reactions when a run reaches done or error, so Discord does not leave a permanent thinking emoji after completion. Fixes #75458. Thanks @davelutztx.
## 2026.4.30

View File

@@ -65,7 +65,8 @@ describe("Slack status reaction lifecycle", () => {
await ctrl.setDone();
expect(active.has(DEFAULT_EMOJIS.done)).toBe(true);
expect(active.has(DEFAULT_EMOJIS.web)).toBe(true);
expect(active.has(DEFAULT_EMOJIS.web)).toBe(false);
expect(active.has(DEFAULT_EMOJIS.thinking)).toBe(false);
await ctrl.clear();
expect(active.size).toBe(0);
@@ -87,7 +88,7 @@ describe("Slack status reaction lifecycle", () => {
await ctrl.setError();
expect(active.has(DEFAULT_EMOJIS.error)).toBe(true);
expect(active.has("eyes")).toBe(true);
expect(active.has("eyes")).toBe(false);
await ctrl.restoreInitial();
expect(active.has("eyes")).toBe(true);
@@ -156,7 +157,8 @@ describe("Slack status reaction lifecycle", () => {
expect(active.has("eyes")).toBe(true);
expect(active.has(DEFAULT_EMOJIS.done)).toBe(false);
expect(adapter.removeReaction).toHaveBeenCalledTimes(1);
expect(adapter.removeReaction).toHaveBeenCalledTimes(2);
expect(adapter.removeReaction).toHaveBeenCalledWith("eyes");
expect(adapter.removeReaction).toHaveBeenCalledWith(DEFAULT_EMOJIS.done);
expect(adapter.removeReaction).not.toHaveBeenCalledWith(DEFAULT_EMOJIS.thinking);
});

View File

@@ -294,6 +294,39 @@ describe("createStatusReactionController", () => {
expect(calls).toContainEqual({ method: "remove", emoji: DEFAULT_EMOJIS.thinking });
});
it("should remove tracked non-terminal emojis when setting done", async () => {
const { calls, controller } = createEnabledController();
void controller.setQueued();
await vi.runAllTimersAsync();
void controller.setThinking();
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
void controller.setTool("exec");
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
await controller.setDone();
const removeEmojis = calls.filter((call) => call.method === "remove").map((call) => call.emoji);
expect(removeEmojis).toEqual(
expect.arrayContaining(["👀", DEFAULT_EMOJIS.thinking, DEFAULT_EMOJIS.coding]),
);
expect(removeEmojis).not.toContain(DEFAULT_EMOJIS.done);
});
it("should not remove reactions on terminal state when adapter lacks removeReaction", async () => {
const { calls, controller } = createSetOnlyController();
void controller.setThinking();
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
await controller.setDone();
expect(calls).toEqual([
{ method: "set", emoji: DEFAULT_EMOJIS.thinking },
{ method: "set", emoji: DEFAULT_EMOJIS.done },
]);
});
it("should not re-add an already active reaction when returning to it", async () => {
const { calls, controller } = createEnabledController();

View File

@@ -341,6 +341,7 @@ export function createStatusReactionController(params: {
// Directly enqueue to ensure we return the updated promise
return enqueue(async () => {
await applyEmoji(emoji);
await removeActiveEmojis({ keepEmoji: emoji });
pendingEmoji = "";
});
}