diff --git a/src/tasks/detached-task-runtime.test.ts b/src/tasks/detached-task-runtime.test.ts index e3f0f072297..1645dfab504 100644 --- a/src/tasks/detached-task-runtime.test.ts +++ b/src/tasks/detached-task-runtime.test.ts @@ -17,6 +17,24 @@ import { } from "./detached-task-runtime.js"; import type { TaskRecord } from "./task-registry.types.js"; +const { mockLogWarn } = vi.hoisted(() => ({ + mockLogWarn: vi.fn(), +})); +vi.mock("../logging/subsystem.js", () => ({ + createSubsystemLogger: () => ({ + subsystem: "tasks/detached-runtime", + isEnabled: () => true, + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: mockLogWarn, + error: vi.fn(), + fatal: vi.fn(), + raw: vi.fn(), + child: vi.fn(), + }), +})); + function createFakeTaskRecord(overrides?: Partial): TaskRecord { return { taskId: "task-fake", @@ -37,6 +55,7 @@ function createFakeTaskRecord(overrides?: Partial): TaskRecord { describe("detached-task-runtime", () => { afterEach(() => { resetDetachedTaskLifecycleRuntimeForTests(); + mockLogWarn.mockClear(); }); it("dispatches lifecycle operations through the installed runtime", async () => { @@ -200,6 +219,10 @@ describe("detached-task-runtime", () => { task, }); expect(result).toEqual({ recovered: false }); + expect(mockLogWarn).toHaveBeenCalledWith( + "onBeforeMarkLost hook threw, proceeding with markTaskLost", + expect.objectContaining({ taskId: "task-throw", runtime: "acp" }), + ); }); }); }); diff --git a/src/tasks/detached-task-runtime.ts b/src/tasks/detached-task-runtime.ts index 0cfc32debe1..d5fd8bf6c94 100644 --- a/src/tasks/detached-task-runtime.ts +++ b/src/tasks/detached-task-runtime.ts @@ -119,7 +119,11 @@ export async function onBeforeMarkLost(params: { return { recovered: false }; } try { - return await hook(params); + const result = await hook(params); + if (result && typeof result.recovered === "boolean") { + return result; + } + return { recovered: false }; } catch (err) { log.warn("onBeforeMarkLost hook threw, proceeding with markTaskLost", { taskId: params.taskId, diff --git a/src/tasks/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts index 7b657cbcd6a..f00af95e10b 100644 --- a/src/tasks/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -252,6 +252,9 @@ export function reconcileTaskLookupToken(token: string): TaskRecord | undefined return task ? reconcileTaskRecordForOperatorInspection(task) : undefined; } +// Preview is synchronous and cannot call the async onBeforeMarkLost hook, +// so recovered tasks are counted under reconciled here. The real sweep +// in runTaskRegistryMaintenance splits them into reconciled vs recovered. export function previewTaskRegistryMaintenance(): TaskRegistryMaintenanceSummary { taskRegistryMaintenanceRuntime.ensureTaskRegistryReady(); const now = Date.now(); @@ -314,18 +317,23 @@ export async function runTaskRegistryMaintenance(): Promise