fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult#30905
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bcc5ce6ce5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } catch (err) { | ||
| // If the schedule expression/timezone throws (croner edge cases), | ||
| // log a warning and fall through — the minNext safety net below | ||
| // will still advance nextRunAtMs so the state update is not lost. | ||
| state.deps.log.warn( |
There was a problem hiding this comment.
Avoid 2s hot-loop when schedule computation throws
Catching computeJobNextRunAtMs errors here and continuing means naturalNext stays undefined, so cron jobs fall through to the existing minNext safety net (endedAt + MIN_REFIRE_GAP_MS, currently 2s) instead of using normal cadence. In environments where cron next-time calculation consistently throws (the commit itself cites malformed cron/timezone edge cases), a successful job will now re-fire every ~2 seconds indefinitely, potentially spamming downstream actions and load; this also bypasses the existing recordScheduleComputeError auto-disable path used elsewhere in jobs.ts.
Useful? React with 👍 / 👎.
Greptile SummaryWraps two unguarded
The fix mirrors the error handling pattern already established in Confidence Score: 4/5
Last reviewed commit: bcc5ce6 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b36534b02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const naturalNext = computeJobNextRunAtMs(job, result.endedAt); | ||
| let naturalNext: number | undefined; | ||
| try { | ||
| naturalNext = computeJobNextRunAtMs(job, result.endedAt); |
There was a problem hiding this comment.
Clear schedule-error counter after successful next-run compute
applyJobResult now calls recordScheduleComputeError when computeJobNextRunAtMs throws, which increments job.state.scheduleErrorCount and auto-disables at 3, but this code path never clears that counter when a later computation succeeds (unlike recomputeJobNextRunAtMs in src/cron/service/jobs.ts). In environments with intermittent cron/timezone compute errors (for example DST edge cases), non-consecutive failures will accumulate across runs and can incorrectly auto-disable otherwise healthy jobs after the third incident.
Useful? React with 👍 / 👎.
|
The Windows CI failure ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04830848f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const normalNext = computeJobNextRunAtMs(job, result.endedAt); | ||
| let normalNext: number | undefined; | ||
| try { | ||
| normalNext = computeJobNextRunAtMs(job, result.endedAt); |
There was a problem hiding this comment.
Clear schedule error counter after successful compute
applyJobResult now increments job.state.scheduleErrorCount via recordScheduleComputeError, but this branch never clears that counter when computeJobNextRunAtMs succeeds on a later run. Because cron jobs keep a finite nextRunAtMs here, recomputeJobNextRunAtMs (the only path that resets the counter in jobs.ts) is typically skipped, so intermittent schedule-compute failures accumulate across non-consecutive runs and can incorrectly auto-disable otherwise healthy jobs after the third total failure.
Useful? React with 👍 / 👎.
Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload
Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs.
0483084 to
e6a4ce5
Compare
|
Landed via autoland as squash merge in 0f4d8e55e935f8f6a2eb3d6398e5aa0dd4d0ceaf. Verified locally in prep worktree with:
Changes made:
Why these changes were made:
|
|
Correction: merge commit SHA is |
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]> (cherry picked from commit 6df8bd9)
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]> (cherry picked from commit 6df8bd9)
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
…ult (openclaw#30905) * fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult Without this guard, if the croner library throws during schedule computation (timezone/expression edge cases), the exception propagates out of applyJobResult and the entire state update is lost — runningAtMs never clears, lastRunAtMs never advances, nextRunAtMs never recomputes. After STUCK_RUN_MS (2h), stuck detection clears runningAtMs and the job re-fires, creating a ~2h repeat cycle instead of the intended schedule. The sibling function recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in try-catch; this was an oversight in the applyJobResult call sites. Changes: - Error-backoff path: catch and fall back to backoff-only schedule - Success path: catch and fall through to the MIN_REFIRE_GAP_MS safety net - applyOutcomeToStoredJob: log a warning when job not found after forceReload * fix(cron): use recordScheduleComputeError in applyJobResult catch blocks Address review feedback: the original catch blocks only logged a warning, which meant a persistent computeJobNextRunAtMs throw would cause a MIN_REFIRE_GAP_MS (2s) hot loop on cron-kind jobs. Now both catch blocks call recordScheduleComputeError (exported from jobs.ts), which tracks consecutive schedule errors and auto-disables the job after 3 failures — matching the existing behavior in recomputeJobNextRunAtMs. * test(cron): cover applyJobResult schedule-throw fallback paths --------- Co-authored-by: Tak Hoffman <[email protected]>
) Port upstream OpenClaw fix openclaw#66083 ("stop unresolved next-run refire loops") into the fork's consolidated cron service. When computeJobNextRunAtMs cannot resolve a recurring cron job's next run, applyJobResult no longer synthesizes a phantom run time — the MIN_REFIRE_GAP_MS guard on the success path, or the error backoff delay on the error path. A synthesized time looks "due" on the next tick and refires the job forever even though its schedule never resolved. Instead the schedule is cleared; the fork's existing armTimer maintenance recheck plus recomputeNextRunsForMaintenance (which always repairs a missing nextRunAtMs) re-arm the job so it fires again on a later tick once the next run becomes resolvable — rather than silently stalling. Adds resolveCronNextRunWithLowerBound, applied to cron-kind jobs only; "every" jobs keep the arithmetic backoff fallback. The two openclaw#30905 throw-path tests are updated to the reconciled behavior (clear + scheduleErrorCount tracking, matching upstream). Regression coverage: unit-level applyJobResult cases (openclaw#66019) plus an onTimer integration test asserting both the no-spurious-refire and fires-once-resolvable paths. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Summary
applyJobResultintimer.tscallscomputeJobNextRunAtMs()without a try-catch in two places (the error-backoff path and the success path). If the croner library throws during schedule computation — which can happen with timezone/DST edge cases or malformed cron expressions — the exception propagates out and the entire state update is lost:runningAtMsis never cleared (job appears "stuck running")lastRunAtMsis never advancednextRunAtMsis never recomputed to the next scheduled timeAfter
STUCK_RUN_MS(2 hours), the stuck-run detection clearsrunningAtMs, and becausenextRunAtMsstill points to the past, the job immediately re-fires. This cycle repeats every ~2 hours instead of the intended schedule (e.g. daily).The sibling function
recomputeJobNextRunAtMsinjobs.tsalready wrapscomputeJobNextRunAtMsin a try-catch (usingrecordScheduleComputeError). The unguarded call sites inapplyJobResultwere an oversight.Changes
Three targeted fixes in
src/cron/service/timer.ts:Error-backoff path (~line 359): Wrap
computeJobNextRunAtMsin try-catch. On failure, log a warning and fall back to backoff-only schedule so the state update is preserved.Success path (~line 374): Wrap
computeJobNextRunAtMsin try-catch. On failure, log a warning and fall through to the existingMIN_REFIRE_GAP_MSsafety net, which still advancesnextRunAtMsso the state update is preserved.applyOutcomeToStoredJobjob-not-found: Add a warning log before the barereturnwhen the job is not found afterforceReload. Previously this was a silent discard, making debugging difficult.Root cause analysis
When the unguarded calls throw, the function exits before persisting any state changes (runningAtMs, lastRunAtMs, nextRunAtMs), leaving the job in a corrupted state that triggers the stuck-run detection loop.
Test plan
pnpm buildpasses (type-check)pnpm test -- src/cron/passes (all 311 cron tests, 45 files)computeJobNextRunAtMsthrow that state is preserved (runningAtMs cleared, lastRunAtMs advanced, nextRunAtMs set via safety net)🤖 Generated with Claude Code