Skip to content

fix: prevent cron jobs from being skipped when timer fires#9573

Closed
lihn-ai-engineer wants to merge 1 commit intoopenclaw:mainfrom
lihn-ai-engineer:fix/cron-job-skip
Closed

fix: prevent cron jobs from being skipped when timer fires#9573
lihn-ai-engineer wants to merge 1 commit intoopenclaw:mainfrom
lihn-ai-engineer:fix/cron-job-skip

Conversation

@lihn-ai-engineer
Copy link

@lihn-ai-engineer lihn-ai-engineer commented Feb 5, 2026

When the cron timer fires, ensureLoaded() was calling recomputeNextRuns() which recalculates nextRunAtMs for all jobs based on current time. This caused jobs scheduled for 'now' to have their nextRunAtMs pushed to 'now + interval', making them appear not due and skipping execution.

Root cause

In store.ts, ensureLoaded() called recomputeNextRuns(state) on every load. When the timer fires and runDueJobs() is called, it eventually triggers ensureLoaded(), which then recalculates all job schedules. Jobs that were due at that exact moment get their next run time pushed forward.

Fix

Remove recomputeNextRuns from ensureLoaded() since it's already called in start() which is sufficient for initialization.

Fixes #9542

Greptile Overview

Greptile Summary

This PR removes the recomputeNextRuns(state) call from ensureLoaded() to prevent a timer tick from advancing jobs that are due “now” into the future (which made them appear not due and skip execution). The recomputation is left in start() for initial scheduler initialization.

However, onTimer() performs ensureLoaded(state, { forceReload: true }) before runDueJobs(). With recomputeNextRuns() removed from ensureLoaded(), a force reload no longer guarantees nextRunAtMs is populated/up-to-date after re-reading the store file, which can cause jobs to be skipped if the file was edited externally or lacks state.nextRunAtMs.

Confidence Score: 3/5

  • This PR fixes the reported skip-on-tick bug, but introduces a likely regression on force-reload paths where schedules may not be recomputed after loading.
  • Core change is small and targeted, but ensureLoaded(..., {forceReload:true}) is used on timer ticks and now won’t refresh nextRunAtMs from the loaded store, so due detection can break when the store content doesn’t already contain correct nextRunAtMs values.
  • src/cron/service/store.ts, src/cron/service/timer.ts

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

When the cron timer fires, ensureLoaded() was calling recomputeNextRuns()
which recalculates nextRunAtMs for all jobs based on current time. This
caused jobs scheduled for 'now' to have their nextRunAtMs pushed to
'now + interval', making them appear not due and skipping execution.

Remove recomputeNextRuns from ensureLoaded since it's already called in
start() which is sufficient for initialization.

Fixes openclaw#9542
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 254 to 260
state.storeLoadedAtMs = state.deps.nowMs();
state.storeFileMtimeMs = fileMtimeMs;

// Recompute next runs after loading to ensure accuracy
recomputeNextRuns(state);
// NOTE: recomputeNextRuns removed here - it was causing jobs to be skipped
// when timer fires because nextRunAtMs gets recalculated to future time.
// recomputeNextRuns is called in start() which is sufficient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force reload can miss due jobs

ensureLoaded() is used on timer ticks with { forceReload: true } (src/cron/service/timer.ts:40). After this change, a force reload no longer recomputes job.state.nextRunAtMs, so if the store file is edited externally (or comes from an older/hand-edited format missing state.nextRunAtMs), runDueJobs() (src/cron/service/timer.ts:55-64) will see nextRunAtMs as undefined/stale and will never consider the job due until the service is restarted (since recomputeNextRuns() only runs in start()).

The fix that avoids recomputing on every load still needs a way to recompute after a force reload / after loading a store that may not have nextRunAtMs populated.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/service/store.ts
Line: 254:260

Comment:
**Force reload can miss due jobs**

`ensureLoaded()` is used on timer ticks with `{ forceReload: true }` (`src/cron/service/timer.ts:40`). After this change, a force reload no longer recomputes `job.state.nextRunAtMs`, so if the store file is edited externally (or comes from an older/hand-edited format missing `state.nextRunAtMs`), `runDueJobs()` (`src/cron/service/timer.ts:55-64`) will see `nextRunAtMs` as `undefined`/stale and will never consider the job due until the service is restarted (since `recomputeNextRuns()` only runs in `start()`).

The fix that avoids recomputing on *every* load still needs a way to recompute after a force reload / after loading a store that may not have `nextRunAtMs` populated.

How can I resolve this? If you propose a fix, please make it concise.

@matthewpapa07
Copy link

found a different fix here #9393

@grp06
Copy link
Member

grp06 commented Feb 6, 2026

Closing: Superseded by #9393

@grp06 grp06 closed this Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cron scheduler timer not firing

3 participants

Comments