Fixes #7667 Task 1: Fix cron operation timeouts - Increase default gateway tool timeout from 10s to 30s - Increase cron-specific tool timeout to 60s - Increase CLI default timeout from 10s to 30s - Prevents timeouts when gateway is busy with long-running jobs Task 2: Add timestamp validation - New validateScheduleTimestamp() function in validate-timestamp.ts - Rejects atMs timestamps more than 1 minute in the past - Rejects atMs timestamps more than 10 years in the future - Applied to both cron.add and cron.update operations - Provides helpful error messages with current time and offset Task 3: Enable file sync for manual edits - Track file modification time (storeFileMtimeMs) in CronServiceState - Check file mtime in ensureLoaded() and reload if changed - Recompute next runs after reload to maintain accuracy - Update mtime after persist() to prevent reload loop - Dashboard now picks up manual edits to ~/.openclaw/cron/jobs.json
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
|
import type { CronJob, CronJobCreate, CronJobPatch, CronStoreFile } from "../types.js";
|
|
|
|
export type CronEvent = {
|
|
jobId: string;
|
|
action: "added" | "updated" | "removed" | "started" | "finished";
|
|
runAtMs?: number;
|
|
durationMs?: number;
|
|
status?: "ok" | "error" | "skipped";
|
|
error?: string;
|
|
summary?: string;
|
|
nextRunAtMs?: number;
|
|
};
|
|
|
|
export type Logger = {
|
|
debug: (obj: unknown, msg?: string) => void;
|
|
info: (obj: unknown, msg?: string) => void;
|
|
warn: (obj: unknown, msg?: string) => void;
|
|
error: (obj: unknown, msg?: string) => void;
|
|
};
|
|
|
|
export type CronServiceDeps = {
|
|
nowMs?: () => number;
|
|
log: Logger;
|
|
storePath: string;
|
|
cronEnabled: boolean;
|
|
enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void;
|
|
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
|
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;
|
|
runIsolatedAgentJob: (params: { job: CronJob; message: string }) => Promise<{
|
|
status: "ok" | "error" | "skipped";
|
|
summary?: string;
|
|
/** Last non-empty agent text output (not truncated). */
|
|
outputText?: string;
|
|
error?: string;
|
|
}>;
|
|
onEvent?: (evt: CronEvent) => void;
|
|
};
|
|
|
|
export type CronServiceDepsInternal = Omit<CronServiceDeps, "nowMs"> & {
|
|
nowMs: () => number;
|
|
};
|
|
|
|
export type CronServiceState = {
|
|
deps: CronServiceDepsInternal;
|
|
store: CronStoreFile | null;
|
|
timer: NodeJS.Timeout | null;
|
|
running: boolean;
|
|
op: Promise<unknown>;
|
|
warnedDisabled: boolean;
|
|
storeLoadedAtMs: number | null;
|
|
storeFileMtimeMs: number | null;
|
|
};
|
|
|
|
export function createCronServiceState(deps: CronServiceDeps): CronServiceState {
|
|
return {
|
|
deps: { ...deps, nowMs: deps.nowMs ?? (() => Date.now()) },
|
|
store: null,
|
|
timer: null,
|
|
running: false,
|
|
op: Promise.resolve(),
|
|
warnedDisabled: false,
|
|
storeLoadedAtMs: null,
|
|
storeFileMtimeMs: null,
|
|
};
|
|
}
|
|
|
|
export type CronRunMode = "due" | "force";
|
|
export type CronWakeMode = "now" | "next-heartbeat";
|
|
|
|
export type CronStatusSummary = {
|
|
enabled: boolean;
|
|
storePath: string;
|
|
jobs: number;
|
|
nextWakeAtMs: number | null;
|
|
};
|
|
|
|
export type CronRunResult =
|
|
| { ok: true; ran: true }
|
|
| { ok: true; ran: false; reason: "not-due" }
|
|
| { ok: false };
|
|
|
|
export type CronRemoveResult = { ok: true; removed: boolean } | { ok: false; removed: false };
|
|
|
|
export type CronAddResult = CronJob;
|
|
export type CronUpdateResult = CronJob;
|
|
|
|
export type CronListResult = CronJob[];
|
|
export type CronAddInput = CronJobCreate;
|
|
export type CronUpdateInput = CronJobPatch;
|