diff --git a/CHANGELOG.md b/CHANGELOG.md index 685b1ea16d6..208822084ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai - Gateway/config restart guard: validate config before service start/restart and keep post-SIGUSR1 startup failures from crashing the gateway process, reducing invalid-config restart loops and macOS permission loss. Landed from contributor PR #38699 by @lml2468. Thanks @lml2468. - Gateway/launchd respawn detection: treat `XPC_SERVICE_NAME` as a launchd supervision hint so macOS restarts exit cleanly under launchd instead of attempting detached self-respawn. Landed from contributor PR #20555 by @dimat. Thanks @dimat. - Telegram/poll restart cleanup: abort the in-flight Telegram API fetch when shutdown or forced polling restarts stop a runner, preventing stale `getUpdates` long polls from colliding with the replacement runner. Landed from contributor PR #23950 by @Gkinthecodeland. Thanks @Gkinthecodeland. +- Cron/restart catch-up staggering: limit immediate missed-job replay on startup and reschedule the deferred remainder from the post-catchup clock so restart bursts do not starve the gateway or silently skip overdue recurring jobs. Landed from contributor PR #18925 by @rexlunae. Thanks @rexlunae. - Cron/owner-only tools: pass trusted isolated cron runs into the embedded agent with owner context so `cron`/`gateway` tooling remains available after the owner-auth hardening narrowed direct-message ownership inference. ## 2026.3.7 diff --git a/src/cron/service.restart-catchup.test.ts b/src/cron/service.restart-catchup.test.ts index 6dff6efc530..f0c9c3e4dc9 100644 --- a/src/cron/service.restart-catchup.test.ts +++ b/src/cron/service.restart-catchup.test.ts @@ -2,9 +2,9 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { CronService } from "./service.js"; -import { createCronServiceState } from "./state.js"; import { setupCronServiceSuite } from "./service.test-harness.js"; -import { runMissedJobs } from "./timer.js"; +import { createCronServiceState } from "./service/state.js"; +import { runMissedJobs } from "./service/timer.js"; const { logger: noopLogger, makeStorePath } = setupCronServiceSuite({ prefix: "openclaw-cron-", @@ -406,8 +406,9 @@ describe("CronService restart catch-up", () => { expect(staggeredJobs[1]?.state.nextRunAtMs).toBeGreaterThan( staggeredJobs[0]?.state.nextRunAtMs ?? 0, ); - expect((staggeredJobs[1]?.state.nextRunAtMs ?? 0) - (staggeredJobs[0]?.state.nextRunAtMs ?? 0)) - .toBe(5_000); + expect( + (staggeredJobs[1]?.state.nextRunAtMs ?? 0) - (staggeredJobs[0]?.state.nextRunAtMs ?? 0), + ).toBe(5_000); await store.cleanup(); }); diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 8f005bd8dbb..08b4b6be206 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -858,7 +858,9 @@ export async function runMissedJobs( startupCandidates: [] as Array<{ jobId: string; job: CronJob }>, }; } - const sorted = missed.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0)); + const sorted = missed.toSorted( + (a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0), + ); const startupCandidates = sorted.slice(0, maxImmediate); const deferred = sorted.slice(maxImmediate); if (deferred.length > 0) {