-
-
Notifications
You must be signed in to change notification settings - Fork 52.7k
Description
Summary
Cron jobs with schedule.kind: "every" never execute automatically because recomputeNextRuns() always recalculates nextRunAtMs based on current time, pushing all scheduled times forward before runDueJobs() can find any due jobs.
Steps to reproduce
- Create a cron job with
schedule: { kind: "every", everyMs: 1020000 }(17 minutes) - Wait for the scheduled time to arrive
- Observe that the job never executes automatically
- Check
nextWakeAtMs- it keeps getting pushed forward by exactly the interval
Expected behavior
Cron jobs should execute automatically when nextRunAtMs is reached.
Actual behavior
The timer fires, but recomputeNextRuns() is called in onTimer() before runDueJobs(), which:
- Recalculates all
nextRunAtMsbased on current time - For "every" schedules without
anchorMs, uses current time as anchor (line 2932) - Pushes all
nextRunAtMsforward runDueJobs()finds no jobs due because times were just pushed forwardarmTimer()is called with the new (pushed forward)nextWakeAtMs
Root cause: recomputeNextRuns() at line 3025 always recalculates nextRunAtMs for all jobs, even if they already have valid future times.
Environment
- Clawdbot version: 2026.2.3-1
- OS: Windows 10 (build 26200)
- Install method: npm (global)
Proposed Fix
recomputeNextRuns() should preserve existing nextRunAtMs if they are valid and in the future, only recalculating if:
- The job does not have a
nextRunAtMs - The
nextRunAtMsis in the past - The job was just modified
Suggested fix in src/cron/service/jobs.ts:
function recomputeNextRuns(state) {
if (!state.store) return;
const now = state.deps.nowMs();
for (const job of state.store.jobs) {
if (!job.state) job.state = {};
if (!job.enabled) {
job.state.nextRunAtMs = void 0;
job.state.runningAtMs = void 0;
continue;
}
const runningAt = job.state.runningAtMs;
if (typeof runningAt === "number" && now - runningAt > STUCK_RUN_MS) {
state.deps.log.warn({
jobId: job.id,
runningAtMs: runningAt
}, "cron: clearing stuck running marker");
job.state.runningAtMs = void 0;
}
// PRESERVE existing nextRunAtMs if valid and in the future
const existingNext = job.state.nextRunAtMs;
if (typeof existingNext === "number" && existingNext > now) {
continue; // Keep existing valid future time
}
// Only recalculate if missing or in the past
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
}
}Workaround
Set anchorMs for all "every" schedules to prevent recalculation from pushing times forward:
{
"schedule": {
"kind": "every",
"everyMs": 1020000,
"anchorMs": 1770381715751
}
}This ensures computeNextRunAtMs() uses a fixed anchor instead of current time. Use createdAtMs or lastRunAtMs as the anchor value.
Code References
- Bug location:
dist/gateway-cli-D_8miTjF.jsline 3025 inrecomputeNextRuns() - Related:
dist/gateway-cli-D_8miTjF.jsline 2932 incomputeNextRunAtMs()(usesnowMsas anchor ifanchorMsnot set) - Timer flow:
onTimer()→ensureLoaded()→recomputeNextRuns()→runDueJobs()→armTimer()
Logs
The timer fires (evidenced by nextWakeAtMs changing), but no jobs execute. No onTimer, runDueJobs, or executeJob entries in logs around scheduled times.