Files
openclaw/src/cron/pacing.ts
Peter Steinberger 8e1c238c1c fix(cron): prevent invalid timestamps from stranding jobs (#121394)
* fix(cron): harden scheduling timestamp boundaries

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* test(prompts): refresh cron tool snapshots

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* test(cron): keep config revision fixture Date-valid

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* refactor(cron): consolidate scheduling lifecycle

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* refactor(cron): keep task history dependencies acyclic

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* fix(cron): canonicalize timestamp auto-disable

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* test(cron): verify startup overflow notifications

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

* chore(ci): repair main baseline gates

Amp-Thread-ID: https://ampcode.com/threads/T-019fe949-92e4-76bd-8cfa-aea44fcfaebe

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 03:20:23 -07:00

53 lines
1.8 KiB
TypeScript

import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion";
import { parseDurationMs } from "../cli/parse-duration.js";
import type { CronPacing } from "./types.js";
/** Parsed positive pacing bounds used for validation and next-run clamping. */
type CronPacingBounds = {
minMs?: number;
maxMs?: number;
};
function parsePositivePacingDuration(value: string, field: "min" | "max"): number {
try {
const durationMs = parseDurationMs(value);
if (durationMs > 0) {
return durationMs;
}
} catch {
// Normalize parser details into the cron configuration contract below.
}
throw new Error(`cron pacing ${field} must be a positive duration`);
}
/** Validates pacing strings and returns their millisecond bounds. */
export function parseCronPacingBounds(pacing: CronPacing): CronPacingBounds {
if (pacing.min === undefined && pacing.max === undefined) {
throw new Error("cron pacing requires at least one of min or max");
}
const minMs =
pacing.min === undefined ? undefined : parsePositivePacingDuration(pacing.min, "min");
const maxMs =
pacing.max === undefined ? undefined : parsePositivePacingDuration(pacing.max, "max");
if (minMs !== undefined && maxMs !== undefined && minMs > maxMs) {
throw new Error("cron pacing min must not exceed max");
}
return { minMs, maxMs };
}
/** Clamps one successful run's proposal against its job-local pacing bounds. */
export function resolvePacedNextRunAtMs(params: {
nowMs: number;
delayMs: number;
pacing: CronPacing;
}): number | undefined {
const { minMs, maxMs } = parseCronPacingBounds(params.pacing);
const proposedAtMs = params.nowMs + params.delayMs;
return asDateTimestampMs(
Math.min(
params.nowMs + (maxMs ?? Number.POSITIVE_INFINITY),
Math.max(params.nowMs + (minMs ?? 0), proposedAtMs),
),
);
}