diff --git a/src/tasks/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts index 02bb518c841..afb47b075aa 100644 --- a/src/tasks/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -286,9 +286,10 @@ function startScheduledSweep() { return; } sweepInProgress = true; - void sweepTaskRegistry().finally(() => { + const clearSweepInProgress = () => { sweepInProgress = false; - }); + }; + sweepTaskRegistry().then(clearSweepInProgress, clearSweepInProgress); } export async function runTaskRegistryMaintenance(): Promise { diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index 01cefafac8a..c575b00668e 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -1453,6 +1453,55 @@ describe("task-registry", () => { }); }); + it("does not leak unhandled rejections when the scheduled maintenance sweep fails", async () => { + await withTaskRegistryTempDir(async (root) => { + vi.useFakeTimers(); + process.env.OPENCLAW_STATE_DIR = root; + resetTaskRegistryForTests(); + + const unhandled: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandled.push(reason); + }; + process.on("unhandledRejection", onUnhandledRejection); + + setTaskRegistryMaintenanceRuntimeForTests({ + readAcpSessionEntry: () => ({ + cfg: {} as never, + storePath: "", + sessionKey: "", + storeSessionKey: "", + entry: undefined, + storeReadFailed: false, + }), + loadSessionStore: () => ({}), + resolveStorePath: () => "", + parseAgentSessionKey: () => null, + isCronJobActive: () => false, + getAgentRunContext: () => undefined, + deleteTaskRecordById: () => false, + ensureTaskRegistryReady: () => {}, + getTaskById: () => undefined, + listTaskRecords: () => { + throw new Error("maintenance boom"); + }, + markTaskLostById: () => null, + maybeDeliverTaskTerminalUpdate: async () => null, + resolveTaskForLookupToken: () => undefined, + setTaskCleanupAfterById: () => null, + }); + + try { + startTaskRegistryMaintenance(); + await vi.advanceTimersByTimeAsync(5_000); + await flushAsyncWork(); + expect(unhandled).toEqual([]); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + } + }); + }); + it("rechecks current task state before marking a task lost", async () => { const now = Date.now(); const snapshotTask = createTaskRecord({