diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index fdfc3c3763b..491062de4fb 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -178,6 +178,7 @@ export function scheduleFollowupDrain( queue.draining = false; if (queue.items.length === 0 && queue.droppedCount === 0) { FOLLOWUP_QUEUES.delete(key); + clearFollowupDrainCallback(key); } else { scheduleFollowupDrain(key, effectiveRunFollowup); } diff --git a/src/auto-reply/reply/reply-flow.test.ts b/src/auto-reply/reply/reply-flow.test.ts index 69594a42bb0..b065d9dfd40 100644 --- a/src/auto-reply/reply/reply-flow.test.ts +++ b/src/auto-reply/reply/reply-flow.test.ts @@ -1685,6 +1685,33 @@ describe("followup queue drain restart after idle window", () => { expect(calls).toHaveLength(1); expect(calls[0]?.prompt).toBe("before-clear"); }); + + it("clears the remembered callback after a queue drains fully", async () => { + const key = `test-auto-clear-callback-${Date.now()}`; + const calls: FollowupRun[] = []; + const settings: QueueSettings = { mode: "followup", debounceMs: 0, cap: 50 }; + const firstProcessed = createDeferred(); + + const runFollowup = async (run: FollowupRun) => { + calls.push(run); + firstProcessed.resolve(); + }; + + enqueueFollowupRun(key, createRun({ prompt: "before-idle" }), settings); + scheduleFollowupDrain(key, runFollowup); + await firstProcessed.promise; + + // Let the idle drain finish and clear its callback. + await new Promise((resolve) => setImmediate(resolve)); + + // Enqueueing after a clean drain should not auto-start anything until a + // fresh finalize path supplies a new callback. + enqueueFollowupRun(key, createRun({ prompt: "after-idle" }), settings); + await new Promise((resolve) => setImmediate(resolve)); + + expect(calls).toHaveLength(1); + expect(calls[0]?.prompt).toBe("before-idle"); + }); }); const emptyCfg = {} as OpenClawConfig;