#12303: fix(cron): correct nextRunAtMs calculation and prevent timer stall
stale
Cluster:
Cron Enhancements and Fixes
## Summary
Fixes the recurring cron jobs `nextRunAtMs` miscalculation that caused jobs to never fire on schedule.
**Root causes identified and fixed:**
1. **`computeNextRunAtMs` boundary handling** (`schedule.ts`): The 1ms lookback (`nowMs - 1`) was insufficient because croner compares at second granularity. A call at exactly HH:00:00.000 would still land in the same second, causing croner to skip the current match and return the next occurrence (e.g., +24h for daily crons). Changed to a 1-second lookback with a fallback to strict `nextRun(nowMs)` when the lookback returns a past match.
2. **Timer death on `state.running` guard** (`timer.ts`): When `onTimer` was called while a previous execution was still running (triggered by external `armTimer` calls from add/update/remove operations), it returned early **without re-arming the timer**. This caused the cron scheduler to silently stall until the running job finished. If the running job took longer than expected, subsequent timer ticks would be lost entirely. Now re-arms with `MAX_TIMER_DELAY_MS` (60s) to prevent both hot-looping and permanent timer death.
3. **Added debug logging** at `onTimer` entry and at the `state.running` early-return path so silent stalls are detectable in logs.
Fixes #12278. Related: #12263, #12276.
## Test plan
- [x] 5 new regression tests for hourly, daily, and 30-min cron expressions with timezones
- [x] Tests for exact-boundary and 1ms-after-boundary edge cases
- [x] Updated timer re-arm test to verify safe-delay behavior
- [x] Full cron test suite passes (93/93 tests across 20 files)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR updates the cron scheduler in two places:
- `src/cron/schedule.ts` changes `computeNextRunAtMs` to use a 1-second lookback (instead of 1ms) when calling Croner’s `nextRun`, and falls back to a strict `nextRun(now)` if the lookback result would be in the past. This addresses exact-boundary scheduling where Croner compares at second granularity.
- `src/cron/service/timer.ts` fixes a scheduler stall when `onTimer` is invoked while another `onTimer` execution is still running: the early-return path now re-arms the timer with a safe delay (`MAX_TIMER_DELAY_MS`, 60s) so the scheduler can recover.
The PR also adds regression tests for boundary/timezone cron expressions and adjusts an existing timer overlap regression test to assert the new re-arm behavior. `.gitignore` is updated to ignore `.cursor/`.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk.
- Changes are localized to cron scheduling and timer re-arming, and are covered by targeted regression tests for the boundary/timezone cases and the timer stall scenario. No correctness issues were found in the updated logic paths during review of the modified files and their call sites.
- No files require special attention
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
**Context used:**
- Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=fd949e91-5c3a-4ab5-90a1-cbe184fd6ce8))
- Context from `dashboard` - AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=0d0c8278-ef8e-4d6c-ab21-f5527e322f13))
<!-- /greptile_comment -->
Most Similar PRs
#10120: fix(cron): ensure next run is strictly in the future (#10035)
by zenchantlive · 2026-02-06
88.7%
#11813: fix(cron): ensure 'at' schedule type correctly registers nextWakeAt...
by leo-reifying · 2026-02-08
88.0%
#12086: fix(cron): ensure timer callback fires for scheduled jobs
by divol89 · 2026-02-08
87.8%
#12443: fix(cron): don't advance past-due jobs that haven't been executed
by rummangeminicode · 2026-02-09
87.1%
#9684: fix: cron race condition - run due jobs before recomputing nextRunA...
by divol89 · 2026-02-05
87.1%
#11108: fix(cron): prevent missed jobs from being skipped on timer recompute
by Bentlybro · 2026-02-07
86.2%
#18144: fix(cron): clear stuck runningAtMs after timeout and add maintenanc...
by taw0002 · 2026-02-16
86.2%
#8811: fix(cron): handle atMs fallback for kind=at jobs
by hlibr · 2026-02-04
86.0%
#12131: fix(cron): ensure timer callback fires for scheduled jobs
by divol89 · 2026-02-08
85.7%
#5428: fix(Cron): prevent one-shot loop on skip
by imshrishk · 2026-01-31
85.6%