#8379: fix(cron): handle past-due one-shot 'at' jobs that haven't run yet
stale
Cluster:
Cron Job Enhancements
## Problem
One-shot `at` schedule cron jobs never fire when created with `atMs` in the past.
**Root cause**: In `src/cron/schedule.ts`, `computeNextRunAtMs` returns `undefined` when `schedule.atMs <= nowMs`. This causes `job.state.nextRunAtMs` to be `undefined`, and `isJobDue` checks `typeof job.state.nextRunAtMs === 'number'` which fails.
## Solution
Added a special case in `isJobDue` to handle past-due one-shot jobs that haven't run yet:
```typescript
// Handle past-due one-shot "at" jobs that haven't run yet:
// When atMs <= nowMs, computeNextRunAtMs returns undefined, so nextRunAtMs is never set.
// But these jobs should still fire immediately if they've never run.
if (
job.schedule.kind === 'at' &&
!job.state.lastRunAtMs &&
nowMs >= job.schedule.atMs
) {
return true;
}
```
This allows:
- Jobs scheduled for the past to fire immediately on next tick
- Jobs that failed before running to retry
- Normal future-scheduled jobs to continue working as before
## Testing
- Ran the full test suite (`pnpm test`) - all cron tests pass including `src/cron/service.runs-one-shot-main-job-disables-it.test.ts`
## Why this approach
Instead of modifying `computeNextRunAtMs` to return `atMs` even when past, I chose to handle this edge case in `isJobDue` because:
1. It's more explicit about the intent ("job is due because it never ran")
2. It doesn't change the semantics of `nextRunAtMs` for other code paths
3. The fix is localized and easy to understand
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR updates `src/cron/service/jobs.ts` to treat one-shot `at` jobs with `atMs` in the past as immediately due when they have never run, by extending `isJobDue` with a special-case check.
The change aims to address a scheduling edge case where `computeNextRunAtMs` (in `src/cron/schedule.ts`) returns `undefined` for `atMs <= nowMs`, preventing `nextRunAtMs` from being set and thus preventing the job from being considered due.
<h3>Confidence Score: 2/5</h3>
- This PR has a meaningful risk of not actually fixing the reported bug in the main execution path.
- Although `isJobDue` now considers past-due one-shot `at` jobs due, the scheduler loop (`runDueJobs` in timer.ts) appears to decide due-ness directly from `nextRunAtMs` without calling `isJobDue`, so past-due `at` jobs with `nextRunAtMs` unset would still never execute. If there are other call sites that use `isJobDue`, the fix may be partial or inconsistent across paths.
- src/cron/service/timer.ts (due-job selection) and src/cron/service/jobs.ts (due semantics)
<!-- greptile_other_comments_section -->
<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#12443: fix(cron): don't advance past-due jobs that haven't been executed
by rummangeminicode · 2026-02-09
86.0%
#5428: fix(Cron): prevent one-shot loop on skip
by imshrishk · 2026-01-31
85.4%
#8034: fix(cron): run past-due one-shot jobs immediately on startup
by FelixFoster · 2026-02-03
84.5%
#11108: fix(cron): prevent missed jobs from being skipped on timer recompute
by Bentlybro · 2026-02-07
84.4%
#10918: fix(cron): add tolerance for timer precision and skip due jobs in r...
by Cherwayway · 2026-02-07
84.0%
#13796: fix: skip recomputing nextRunAtMs for running cron jobs (#13739)
by echoVic · 2026-02-11
83.7%
#12448: fix: prevent cron list/status from silently skipping due jobs
by Yida-Dev · 2026-02-09
83.6%
#14667: fix: preserve missed cron runs when updating job schedule
by WalterSumbon · 2026-02-12
83.5%
#12982: fix(cron): prevent status/list from advancing overdue job nextRunAtMs
by hclsys · 2026-02-10
83.5%
#12747: fix: catch up missed cron-expression job runs on restart
by obin94-commits · 2026-02-09
82.8%