Skip to content

Commit 17a48ce

Browse files
committed
fix(cron): suppress skipped restart slots
1 parent 992b50a commit 17a48ce

2 files changed

Lines changed: 54 additions & 49 deletions

File tree

src/cron/service.restart-catchup.test.ts

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -270,59 +270,64 @@ describe("CronService restart catch-up", () => {
270270
}
271271
});
272272

273-
it("does not defer an isolated cron agent-turn whose persisted due slot already succeeded", async () => {
274-
const store = await makeStorePath();
275-
const startNow = Date.parse("2025-12-13T11:00:00.000Z");
276-
const dueAt = Date.parse("2025-12-13T09:10:00.000Z");
277-
const completedAt = Date.parse("2025-12-13T09:10:30.000Z");
278-
const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const }));
279-
const enqueueSystemEvent = vi.fn();
280-
const requestHeartbeat = vi.fn();
281-
282-
await writeStoreJobs(store.storePath, [
283-
{
284-
id: "startup-isolated-agent-already-ok",
285-
name: "startup isolated agent already ok",
286-
enabled: true,
287-
createdAtMs: Date.parse("2025-12-10T12:00:00.000Z"),
288-
updatedAtMs: completedAt,
289-
schedule: { kind: "cron", expr: "10 9 * * *", tz: "UTC" },
290-
sessionTarget: "isolated",
291-
wakeMode: "next-heartbeat",
292-
payload: { kind: "agentTurn", message: "daily reminder" },
293-
state: {
294-
nextRunAtMs: dueAt,
295-
lastRunAtMs: completedAt,
296-
lastRunStatus: "ok",
273+
it.each(["ok", "skipped"] as const)(
274+
"does not defer an isolated cron agent-turn whose persisted due slot finished as %s",
275+
async (lastRunStatus) => {
276+
const store = await makeStorePath();
277+
const startNow = Date.parse("2025-12-13T11:00:00.000Z");
278+
const dueAt = Date.parse("2025-12-13T09:10:00.000Z");
279+
const completedAt = Date.parse("2025-12-13T09:10:30.000Z");
280+
const runIsolatedAgentJob = vi.fn(async () => ({ status: "ok" as const }));
281+
const enqueueSystemEvent = vi.fn();
282+
const requestHeartbeat = vi.fn();
283+
284+
await writeStoreJobs(store.storePath, [
285+
{
286+
id: `startup-isolated-agent-already-${lastRunStatus}`,
287+
name: `startup isolated agent already ${lastRunStatus}`,
288+
enabled: true,
289+
createdAtMs: Date.parse("2025-12-10T12:00:00.000Z"),
290+
updatedAtMs: completedAt,
291+
schedule: { kind: "cron", expr: "10 9 * * *", tz: "UTC" },
292+
sessionTarget: "isolated",
293+
wakeMode: "next-heartbeat",
294+
payload: { kind: "agentTurn", message: "daily reminder" },
295+
state: {
296+
nextRunAtMs: dueAt,
297+
lastRunAtMs: completedAt,
298+
lastRunStatus,
299+
},
297300
},
298-
},
299-
]);
301+
]);
300302

301-
const cron = createRestartCronService({
302-
storePath: store.storePath,
303-
enqueueSystemEvent,
304-
requestHeartbeat,
305-
runIsolatedAgentJob,
306-
nowMs: () => startNow,
307-
startupDeferredMissedAgentJobDelayMs: 120_000,
308-
});
303+
const cron = createRestartCronService({
304+
storePath: store.storePath,
305+
enqueueSystemEvent,
306+
requestHeartbeat,
307+
runIsolatedAgentJob,
308+
nowMs: () => startNow,
309+
startupDeferredMissedAgentJobDelayMs: 120_000,
310+
});
309311

310-
try {
311-
await cron.start();
312+
try {
313+
await cron.start();
312314

313-
expect(runIsolatedAgentJob).not.toHaveBeenCalled();
314-
expect(enqueueSystemEvent).not.toHaveBeenCalled();
315-
expect(requestHeartbeat).not.toHaveBeenCalled();
315+
expect(runIsolatedAgentJob).not.toHaveBeenCalled();
316+
expect(enqueueSystemEvent).not.toHaveBeenCalled();
317+
expect(requestHeartbeat).not.toHaveBeenCalled();
316318

317-
const listedJobs = await cron.list({ includeDisabled: true });
318-
const updated = listedJobs.find((job) => job.id === "startup-isolated-agent-already-ok");
319-
expect(updated?.state.lastRunStatus).toBe("ok");
320-
expect(updated?.state.nextRunAtMs).toBe(Date.parse("2025-12-14T09:10:00.000Z"));
321-
} finally {
322-
cron.stop();
323-
await store.cleanup();
324-
}
325-
});
319+
const listedJobs = await cron.list({ includeDisabled: true });
320+
const updated = listedJobs.find(
321+
(job) => job.id === `startup-isolated-agent-already-${lastRunStatus}`,
322+
);
323+
expect(updated?.state.lastRunStatus).toBe(lastRunStatus);
324+
expect(updated?.state.nextRunAtMs).toBe(Date.parse("2025-12-14T09:10:00.000Z"));
325+
} finally {
326+
cron.stop();
327+
await store.cleanup();
328+
}
329+
},
330+
);
326331

327332
it("replays a newer missed cron slot behind a completed persisted slot", async () => {
328333
vi.setSystemTime(new Date("2025-12-13T04:10:00.000Z"));

src/cron/service/timer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1840,7 +1840,7 @@ function isRunnableJob(params: {
18401840
const alreadyCompletedDueCronSlot =
18411841
params.allowCronMissedRunByLastRun &&
18421842
job.schedule.kind === "cron" &&
1843-
lastRunStatus === "ok" &&
1843+
(lastRunStatus === "ok" || lastRunStatus === "skipped") &&
18441844
typeof lastRunAtMs === "number" &&
18451845
Number.isFinite(lastRunAtMs) &&
18461846
lastRunAtMs >= next;

0 commit comments

Comments
 (0)