diff --git a/src/tasks/task-registry.maintenance.issue-60299.test.ts b/src/tasks/task-registry.maintenance.issue-60299.test.ts index 191057c8a344..870cbcf806f9 100644 --- a/src/tasks/task-registry.maintenance.issue-60299.test.ts +++ b/src/tasks/task-registry.maintenance.issue-60299.test.ts @@ -420,6 +420,35 @@ describe("task-registry maintenance issue #60299", () => { expect(storedTask.terminalSummary).toBe("done"); }); + it("does not recover cron tasks from malformed run id timestamps", async () => { + const task = makeStaleTask({ + runtime: "cron", + sourceId: "cron-job-run-log-ok", + runId: "cron:cron-job-run-log-ok:1e3", + }); + + const { currentTasks } = createTaskRegistryMaintenanceHarness({ + tasks: [task], + cronRunLogEntries: { + "cron-job-run-log-ok": [ + { + ts: 1250, + jobId: "cron-job-run-log-ok", + action: "finished", + status: "ok", + summary: "done", + runAtMs: 1000, + durationMs: 250, + }, + ], + }, + }); + + expectMaintenanceCounts(previewTaskRegistryMaintenance(), { reconciled: 1, recovered: 0 }); + expectMaintenanceCounts(await runTaskRegistryMaintenance(), { reconciled: 1, recovered: 0 }); + expectTaskStatus(currentTasks, task.taskId, "lost"); + }); + it("recovers interrupted cron tasks from durable cron job state when run logs are absent", async () => { const startedAt = Date.now() - GRACE_EXPIRED_MS; const task = makeStaleTask({ diff --git a/src/tasks/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts index b018bce37d50..fe7c21284cd6 100644 --- a/src/tasks/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -18,6 +18,7 @@ import { loadCronStoreSync, resolveCronStorePath } from "../cron/store.js"; import type { CronJob, CronStoreFile } from "../cron/types.js"; import { getAgentRunContext } from "../infra/agent-events.js"; import { getSessionBindingService } from "../infra/outbound/session-binding-service.js"; +import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { isPluginStateDatabaseOpen, @@ -337,8 +338,8 @@ function parseCronExecutionId(task: TaskRecord): CronExecutionId | undefined { if (separator <= "cron:".length) { return undefined; } - const startedAt = Number(runId.slice(separator + 1)); - if (!Number.isFinite(startedAt)) { + const startedAt = parseStrictNonNegativeInteger(runId.slice(separator + 1)); + if (startedAt === undefined) { return undefined; } const jobId = runId.slice("cron:".length, separator).trim();