mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(cron): restore interval cadence after restart
This commit is contained in:
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/Timer: keep a watchdog recheck timer armed while `onTimer` is actively executing so the scheduler continues polling even if a due-run tick stalls for an extended period. (#23628) Thanks @dsgraves.
|
||||
- Cron/Run: enforce the same per-job timeout guard for manual `cron.run` executions as timer-driven runs, including abort propagation for isolated agent jobs, so forced runs cannot wedge indefinitely. (#23704) Thanks @tkuehnl.
|
||||
- Cron/Status: split execution outcome (`lastRunStatus`) from delivery outcome (`lastDeliveryStatus`) in persisted cron state, finished events, and run history so failed/unknown announcement delivery is visible without conflating it with run errors.
|
||||
- Cron/Schedule: for `every` jobs, prefer `lastRunAtMs + everyMs` when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber.
|
||||
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
|
||||
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)
|
||||
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
|
||||
|
||||
51
src/cron/service.issue-22895-every-next-run.test.ts
Normal file
51
src/cron/service.issue-22895-every-next-run.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeJobNextRunAtMs } from "./service/jobs.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
const EVERY_30_MIN_MS = 30 * 60_000;
|
||||
const ANCHOR_MS = Date.parse("2026-02-22T09:14:00.000Z");
|
||||
|
||||
function createEveryJob(state: CronJob["state"]): CronJob {
|
||||
return {
|
||||
id: "issue-22895",
|
||||
name: "every-30-min",
|
||||
enabled: true,
|
||||
createdAtMs: ANCHOR_MS,
|
||||
updatedAtMs: ANCHOR_MS,
|
||||
schedule: { kind: "every", everyMs: EVERY_30_MIN_MS, anchorMs: ANCHOR_MS },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "agentTurn", message: "check cadence" },
|
||||
delivery: { mode: "none" },
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Cron issue #22895 interval scheduling", () => {
|
||||
it("uses lastRunAtMs cadence when the next interval is still in the future", () => {
|
||||
const nowMs = Date.parse("2026-02-22T10:10:00.000Z");
|
||||
const job = createEveryJob({
|
||||
lastRunAtMs: Date.parse("2026-02-22T10:04:00.000Z"),
|
||||
});
|
||||
|
||||
const nextFromLast = computeJobNextRunAtMs(job, nowMs);
|
||||
const nextFromAnchor = computeJobNextRunAtMs(
|
||||
{ ...job, state: { ...job.state, lastRunAtMs: undefined } },
|
||||
nowMs,
|
||||
);
|
||||
|
||||
expect(nextFromLast).toBe(job.state.lastRunAtMs! + EVERY_30_MIN_MS);
|
||||
expect(nextFromAnchor).toBe(Date.parse("2026-02-22T10:14:00.000Z"));
|
||||
expect(nextFromLast).toBeGreaterThan(nextFromAnchor!);
|
||||
});
|
||||
|
||||
it("falls back to anchor scheduling when lastRunAtMs cadence is already in the past", () => {
|
||||
const nowMs = Date.parse("2026-02-22T10:40:00.000Z");
|
||||
const job = createEveryJob({
|
||||
lastRunAtMs: Date.parse("2026-02-22T10:04:00.000Z"),
|
||||
});
|
||||
|
||||
const next = computeJobNextRunAtMs(job, nowMs);
|
||||
expect(next).toBe(Date.parse("2026-02-22T10:44:00.000Z"));
|
||||
});
|
||||
});
|
||||
@@ -113,11 +113,19 @@ export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | und
|
||||
return undefined;
|
||||
}
|
||||
if (job.schedule.kind === "every") {
|
||||
const everyMs = Math.max(1, Math.floor(job.schedule.everyMs));
|
||||
const lastRunAtMs = job.state.lastRunAtMs;
|
||||
if (typeof lastRunAtMs === "number" && Number.isFinite(lastRunAtMs)) {
|
||||
const nextFromLastRun = Math.floor(lastRunAtMs) + everyMs;
|
||||
if (nextFromLastRun > nowMs) {
|
||||
return nextFromLastRun;
|
||||
}
|
||||
}
|
||||
const anchorMs = resolveEveryAnchorMs({
|
||||
schedule: job.schedule,
|
||||
fallbackAnchorMs: job.createdAtMs,
|
||||
});
|
||||
return computeNextRunAtMs({ ...job.schedule, anchorMs }, nowMs);
|
||||
return computeNextRunAtMs({ ...job.schedule, everyMs, anchorMs }, nowMs);
|
||||
}
|
||||
if (job.schedule.kind === "at") {
|
||||
// One-shot jobs stay due until they successfully finish.
|
||||
|
||||
Reference in New Issue
Block a user