Skip to content

fix #11246 - Cron job update calculates nextRunAtMs 24 hours late#11282

Closed
Lucky-Lodhi2004 wants to merge 1 commit intoopenclaw:mainfrom
Lucky-Lodhi2004:cron-job-update
Closed

fix #11246 - Cron job update calculates nextRunAtMs 24 hours late#11282
Lucky-Lodhi2004 wants to merge 1 commit intoopenclaw:mainfrom
Lucky-Lodhi2004:cron-job-update

Conversation

@Lucky-Lodhi2004
Copy link

@Lucky-Lodhi2004 Lucky-Lodhi2004 commented Feb 7, 2026

Description

Fixes issue #11246

Changes made

fixed a bug where cron jobs could be scheduled 24 hours late if they were updated or if the service restarted exactly at their scheduled run time.

Greptile Overview

Greptile Summary

This PR adjusts cron next-run computation to avoid a boundary case where jobs scheduled exactly at now could be pushed out (reported as “24 hours late”), including:

  • computeNextRunAtMs() now uses a 1ms lookback when calling Croner’s nextRun() to catch schedules that match exactly at the current time.
  • recomputeNextRuns() only recomputes nextRunAtMs when the job is due or missing.
  • ops.list() replaces toSorted() with a sort() on a copied array.
  • ops.update() only recalculates nextRunAtMs when schedule/enabled are patched.

Main concerns are behavioral changes around timezone defaulting and swallowing cron parse errors, plus recomputeNextRuns() no longer being authoritative which can leave stale nextRunAtMs values intact.

Confidence Score: 3/5

  • This PR is close to mergeable but includes a couple behavioral changes that can mis-schedule jobs if not addressed.
  • The boundary fix in computeNextRunAtMs() is plausible, but the patch also changes timezone fallback semantics and starts swallowing cron parsing errors, both of which can silently shift or disable schedules. Additionally, recomputeNextRuns() is no longer authoritative and can preserve stale nextRunAtMs values under realistic state-update scenarios.
  • src/cron/schedule.ts and src/cron/service/jobs.ts

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

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.

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 48 to 51
const cron = new Cron(expr, {
timezone: resolveCronTimezone(schedule.tz),
catch: false,
timezone: schedule.tz?.trim() || undefined,
catch: true,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Timezone fallback removed

computeNextRunAtMs previously defaulted to Intl.DateTimeFormat().resolvedOptions().timeZone when schedule.tz was missing/blank via resolveCronTimezone(). The new code passes timezone: schedule.tz?.trim() || undefined, which makes croner fall back to its default timezone (often UTC). For cron schedules stored without an explicit tz, this changes behavior and will shift next run times. If the intent is “use local timezone when tz is omitted”, this should continue calling resolveCronTimezone(schedule.tz) (or an equivalent fallback), not undefined.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/schedule.ts
Line: 48:51

Comment:
**Timezone fallback removed**

`computeNextRunAtMs` previously defaulted to `Intl.DateTimeFormat().resolvedOptions().timeZone` when `schedule.tz` was missing/blank via `resolveCronTimezone()`. The new code passes `timezone: schedule.tz?.trim() || undefined`, which makes croner fall back to *its* default timezone (often UTC). For cron schedules stored without an explicit `tz`, this changes behavior and will shift next run times. If the intent is “use local timezone when tz is omitted”, this should continue calling `resolveCronTimezone(schedule.tz)` (or an equivalent fallback), not `undefined`.


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

Comment on lines +50 to +54
catch: true,
});
let cursor = nowMs;
for (let attempt = 0; attempt < 3; attempt++) {
const next = cron.nextRun(new Date(cursor));
if (!next) {
return undefined;
}
const nextMs = next.getTime();
if (Number.isFinite(nextMs) && nextMs > nowMs) {
return nextMs;
}
cursor += 1_000;
// Use a tiny lookback to catch the boundary if we are exactly at it.
// Croner's nextRun(date) skips the current second if it matches exactly.
const next = cron.nextRun(new Date(nowMs - 1));
Copy link
Contributor

Choose a reason for hiding this comment

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

Cron parsing errors now swallowed

Changing catch: false to catch: true will cause invalid cron expressions to be silently ignored by croner rather than throwing. That can turn a bad schedule into nextRunAtMs = undefined with no error surfaced, which is a behavioral change from before. If this was added only to avoid crashing on invalid input, consider keeping catch: false and handling exceptions at the call site (so you can log/return a validation error), or at least log when nextRun can’t be computed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/schedule.ts
Line: 50:54

Comment:
**Cron parsing errors now swallowed**

Changing `catch: false` to `catch: true` will cause invalid cron expressions to be silently ignored by croner rather than throwing. That can turn a bad schedule into `nextRunAtMs = undefined` with no error surfaced, which is a behavioral change from before. If this was added only to avoid crashing on invalid input, consider keeping `catch: false` and handling exceptions at the call site (so you can log/return a validation error), or at least log when `nextRun` can’t be computed.


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

Comment on lines +121 to 129
const nextRun = job.state.nextRunAtMs;
const isDueOrMissing = nextRun === undefined || now >= nextRun;
if (isDueOrMissing) {
const newNext = computeJobNextRunAtMs(job, now);
if (job.state.nextRunAtMs !== newNext) {
job.state.nextRunAtMs = newNext;
changed = true;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Recompute can leave stale nextRun

recomputeNextRuns() now only updates job.state.nextRunAtMs when nextRunAtMs is missing or already due (now >= nextRunAtMs). If a job’s schedule was changed on disk (or via patch fields other than schedule/enabled) while keeping a future nextRunAtMs, this function will no longer correct it on startup/periodic recompute, and the job can run at the wrong time. If recompute is intended to be authoritative (as the name suggests), it should still recompute for enabled jobs (or at least when schedule-related fields changed), not gate solely on due/missing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/service/jobs.ts
Line: 121:129

Comment:
**Recompute can leave stale nextRun**

`recomputeNextRuns()` now only updates `job.state.nextRunAtMs` when `nextRunAtMs` is missing or already due (`now >= nextRunAtMs`). If a job’s schedule was changed on disk (or via patch fields other than `schedule`/`enabled`) while keeping a future `nextRunAtMs`, this function will no longer correct it on startup/periodic recompute, and the job can run at the wrong time. If recompute is intended to be authoritative (as the name suggests), it should still recompute for enabled jobs (or at least when schedule-related fields changed), not gate solely on due/missing.


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

@tyler6204
Copy link
Member

Superseded by #11641 (merge commit: 8fae55e). Closing to reduce duplicate PR noise. Please open a new PR only if there is additional scope beyond this fix.

@tyler6204 tyler6204 closed this Feb 8, 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.

2 participants

Comments