mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
cron: document split runtime state store
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user