cron: document split runtime state store

This commit is contained in:
Gustavo Madeira Santana
2026-04-20 11:20:29 -04:00
parent 0844bd521e
commit a5385155f3
7 changed files with 36 additions and 5 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Plugins/tests: reuse plugin loader alias and Jiti config resolution across repeated same-context loads, reducing import-heavy test overhead. (#69316) Thanks @amknight.
- Cron: split runtime execution state into `jobs-state.json` so `jobs.json` stays stable for git-tracked job definitions. (#63105) Thanks @Feelw00.
### Fixes

View File

@@ -33,7 +33,8 @@ openclaw cron runs --id <job-id>
## How cron works
- Cron runs **inside the Gateway** process (not inside the model).
- Jobs persist at `~/.openclaw/cron/jobs.json` so restarts do not lose schedules.
- Job definitions persist at `~/.openclaw/cron/jobs.json` so restarts do not lose schedules.
- Runtime execution state persists next to it in `~/.openclaw/cron/jobs-state.json`. If you track cron definitions in git, track `jobs.json` and gitignore `jobs-state.json`.
- All cron executions create [background task](/automation/tasks) records.
- One-shot jobs (`--at`) auto-delete after success by default.
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.

View File

@@ -301,7 +301,7 @@ See [Task Flow](/automation/taskflow) for details.
### Tasks and cron
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`; runtime execution state lives beside it in `~/.openclaw/cron/jobs-state.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
See [Cron Jobs](/automation/cron-jobs).

View File

@@ -589,7 +589,6 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
deleteAfterRun,
createdAtMs: now,
updatedAtMs: now,
configUpdatedAtMs: now,
schedule,
sessionTarget: input.sessionTarget,
wakeMode: input.wakeMode,

View File

@@ -307,7 +307,6 @@ export async function update(state: CronServiceState, id: string, patch: CronJob
const enabledChanged = patch.enabled !== undefined;
job.updatedAtMs = now;
job.configUpdatedAtMs = now;
if (scheduleChanged || enabledChanged) {
if (isJobEnabled(job)) {
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);

View File

@@ -154,6 +154,38 @@ describe("cron store", () => {
await expect(fs.stat(`${store.storePath}.bak`)).rejects.toThrow();
});
it("keeps state separate for custom store paths without a json suffix", async () => {
const store = await makeStorePath();
const storePath = store.storePath.replace(/\.json$/, "");
const statePath = `${storePath}-state.json`;
const first = makeStore("job-1", true);
const second: CronStoreFile = {
...first,
jobs: first.jobs.map((job) => ({
...job,
updatedAtMs: job.updatedAtMs + 60_000,
state: {
...job.state,
nextRunAtMs: job.createdAtMs + 60_000,
},
})),
};
await saveCronStore(storePath, first);
await saveCronStore(storePath, second);
const config = JSON.parse(await fs.readFile(storePath, "utf-8"));
expect(Array.isArray(config.jobs)).toBe(true);
expect(config.jobs[0].id).toBe("job-1");
expect(config.jobs[0].state).toEqual({});
const stateFile = JSON.parse(await fs.readFile(statePath, "utf-8"));
expect(stateFile.jobs["job-1"].state.nextRunAtMs).toBe(first.jobs[0].createdAtMs + 60_000);
const loaded = await loadCronStore(storePath);
expect(loaded.jobs[0]?.state.nextRunAtMs).toBe(first.jobs[0].createdAtMs + 60_000);
});
it.skipIf(process.platform === "win32")(
"writes store and backup files with secure permissions",
async () => {

View File

@@ -9,7 +9,6 @@ export type CronJobBase<TSchedule, TSessionTarget, TWakeMode, TPayload, TDeliver
deleteAfterRun?: boolean;
createdAtMs: number;
updatedAtMs: number;
configUpdatedAtMs?: number;
schedule: TSchedule;
sessionTarget: TSessionTarget;
wakeMode: TWakeMode;