mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-22 09:01:46 +03:00
8fae55e8e0
* fix(cron): comprehensive cron scheduling and delivery fixes - Fix delivery target resolution for isolated agent cron jobs - Improve schedule parsing and validation - Add job retry logic and error handling - Enhance cron ops with better state management - Add timer improvements for more reliable cron execution - Add cron event type to protocol schema - Support cron events in heartbeat runner (skip empty-heartbeat check, use dedicated CRON_EVENT_PROMPT for relay) * fix: remove cron debug test and add changelog/docs notes (#11641) (thanks @tyler6204)
62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
import { Cron } from "croner";
|
|
import type { CronSchedule } from "./types.js";
|
|
import { parseAbsoluteTimeMs } from "./parse.js";
|
|
|
|
function resolveCronTimezone(tz?: string) {
|
|
const trimmed = typeof tz === "string" ? tz.trim() : "";
|
|
if (trimmed) {
|
|
return trimmed;
|
|
}
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
}
|
|
|
|
export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined {
|
|
if (schedule.kind === "at") {
|
|
// Handle both canonical `at` (string) and legacy `atMs` (number) fields.
|
|
// The store migration should convert atMs→at, but be defensive in case
|
|
// the migration hasn't run yet or was bypassed.
|
|
const sched = schedule as { at?: string; atMs?: number | string };
|
|
const atMs =
|
|
typeof sched.atMs === "number" && Number.isFinite(sched.atMs) && sched.atMs > 0
|
|
? sched.atMs
|
|
: typeof sched.atMs === "string"
|
|
? parseAbsoluteTimeMs(sched.atMs)
|
|
: typeof sched.at === "string"
|
|
? parseAbsoluteTimeMs(sched.at)
|
|
: null;
|
|
if (atMs === null) {
|
|
return undefined;
|
|
}
|
|
return atMs > nowMs ? atMs : undefined;
|
|
}
|
|
|
|
if (schedule.kind === "every") {
|
|
const everyMs = Math.max(1, Math.floor(schedule.everyMs));
|
|
const anchor = Math.max(0, Math.floor(schedule.anchorMs ?? nowMs));
|
|
if (nowMs < anchor) {
|
|
return anchor;
|
|
}
|
|
const elapsed = nowMs - anchor;
|
|
const steps = Math.max(1, Math.floor((elapsed + everyMs - 1) / everyMs));
|
|
return anchor + steps * everyMs;
|
|
}
|
|
|
|
const expr = schedule.expr.trim();
|
|
if (!expr) {
|
|
return undefined;
|
|
}
|
|
const cron = new Cron(expr, {
|
|
timezone: resolveCronTimezone(schedule.tz),
|
|
catch: false,
|
|
});
|
|
// Use a tiny lookback (1ms) so croner doesn't skip the current second
|
|
// boundary. Without this, a job updated at exactly its cron time would
|
|
// be scheduled for the *next* matching time (e.g. 24h later for daily).
|
|
const next = cron.nextRun(new Date(nowMs - 1));
|
|
if (!next) {
|
|
return undefined;
|
|
}
|
|
const nextMs = next.getTime();
|
|
return Number.isFinite(nextMs) && nextMs >= nowMs ? nextMs : undefined;
|
|
}
|