Skip to content

fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult#30905

Merged
Takhoffman merged 3 commits into
openclaw:mainfrom
wonderchook:fix/cron-applyJobResult-schedule-catch
Mar 2, 2026
Merged

fix(cron): wrap computeJobNextRunAtMs in try-catch inside applyJobResult#30905
Takhoffman merged 3 commits into
openclaw:mainfrom
wonderchook:fix/cron-applyJobResult-schedule-catch

Conversation

@wonderchook

Copy link
Copy Markdown
Contributor

Summary

applyJobResult in timer.ts calls computeJobNextRunAtMs() 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:

  • runningAtMs is never cleared (job appears "stuck running")
  • lastRunAtMs is never advanced
  • nextRunAtMs is never recomputed to the next scheduled time

After STUCK_RUN_MS (2 hours), the stuck-run detection clears runningAtMs, and because nextRunAtMs still 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 recomputeJobNextRunAtMs in jobs.ts already wraps computeJobNextRunAtMs in a try-catch (using recordScheduleComputeError). The unguarded call sites in applyJobResult were an oversight.

Changes

Three targeted fixes in src/cron/service/timer.ts:

  1. Error-backoff path (~line 359): Wrap computeJobNextRunAtMs in try-catch. On failure, log a warning and fall back to backoff-only schedule so the state update is preserved.

  2. Success path (~line 374): Wrap computeJobNextRunAtMs in try-catch. On failure, log a warning and fall through to the existing MIN_REFIRE_GAP_MS safety net, which still advances nextRunAtMs so the state update is preserved.

  3. applyOutcomeToStoredJob job-not-found: Add a warning log before the bare return when the job is not found after forceReload. Previously this was a silent discard, making debugging difficult.

Root cause analysis

applyJobResult (timer.ts)
  ├─ error path:   const normalNext = computeJobNextRunAtMs(...)   ← UNGUARDED, can throw
  └─ success path: const naturalNext = computeJobNextRunAtMs(...)  ← UNGUARDED, can throw

recomputeJobNextRunAtMs (jobs.ts)
  └─ try { computeJobNextRunAtMs(...) } catch { recordScheduleComputeError(...) }  ← GUARDED ✓

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 build passes (type-check)
  • pnpm test -- src/cron/ passes (all 311 cron tests, 45 files)
  • Deployed to a live gateway instance and confirmed cron jobs fire on schedule
  • Verify with a simulated computeJobNextRunAtMs throw that state is preserved (runningAtMs cleared, lastRunAtMs advanced, nextRunAtMs set via safety net)

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/cron/service/timer.ts Outdated
Comment on lines +387 to +391
} 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge 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-apps

greptile-apps Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Wraps two unguarded computeJobNextRunAtMs calls in applyJobResult with try-catch blocks to prevent state corruption when the croner library throws during timezone/DST edge cases or malformed cron expressions.

  • Error-backoff path (line 361): Falls back to backoff-only schedule when computation fails, ensuring state update completes
  • Success path (line 386): Falls through to MIN_REFIRE_GAP_MS safety net for cron jobs when computation fails
  • Job-not-found case (line 423): Adds warning log before silent return to aid debugging

The fix mirrors the error handling pattern already established in recomputeJobNextRunAtMs (jobs.ts:301-320). Prevents the ~2 hour stuck-run loop caused by incomplete state updates when computeJobNextRunAtMs throws an exception.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk
  • Targeted bug fix following established patterns in the codebase (recomputeJobNextRunAtMs). All existing tests pass and the fix has been validated in production. The changes are minimal (three small additions) and defensive in nature - they prevent exceptions from propagating while ensuring state updates complete successfully.
  • No files require special attention

Last reviewed commit: bcc5ce6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/cron/service/timer.ts
const naturalNext = computeJobNextRunAtMs(job, result.endedAt);
let naturalNext: number | undefined;
try {
naturalNext = computeJobNextRunAtMs(job, result.endedAt);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@wonderchook

Copy link
Copy Markdown
Contributor Author

The Windows CI failure (checks-windows node test 1/2) is pre-existing and unrelated to this PR. It's a flaky src/infra/fs-safe.test.ts test where expandHomePrefix resolves /tmp/fake-home-test to C:\tmp\fake-home-test on Windows. The same shard is also failing on main (run 22549744030). All Linux/macOS checks pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/cron/service/timer.ts
const normalNext = computeJobNextRunAtMs(job, result.endedAt);
let normalNext: number | undefined;
try {
normalNext = computeJobNextRunAtMs(job, result.endedAt);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

wonderchook and others added 3 commits March 2, 2026 07:28
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.
@Takhoffman
Takhoffman force-pushed the fix/cron-applyJobResult-schedule-catch branch from 0483084 to e6a4ce5 Compare March 2, 2026 13:38
@Takhoffman
Takhoffman merged commit 6df8bd9 into openclaw:main Mar 2, 2026
9 checks passed
@Takhoffman

Copy link
Copy Markdown
Contributor

Landed via autoland as squash merge in 0f4d8e55e935f8f6a2eb3d6398e5aa0dd4d0ceaf.

Verified locally in prep worktree with:

  • pnpm build
  • pnpm check
  • pnpm vitest src/cron/service.issue-regressions.test.ts
  • pnpm test:macmini

Changes made:

  • Rebased the PR branch onto latest main.
  • Kept the core applyJobResult hardening that catches schedule-computation throws and records schedule errors.
  • Added focused regression coverage for both catch paths:
    • successful run path falls back to MIN_REFIRE_GAP_MS when schedule computation throws
    • error path falls back to backoff schedule when schedule computation throws

Why these changes were made:

  • The fix addresses a real scheduler reliability failure mode (stuck/past-due loop after thrown next-run compute).
  • The added tests make the new fallback behavior explicit and guard against regressions in future timer/jobs refactors.

@Takhoffman

Copy link
Copy Markdown
Contributor

Correction: merge commit SHA is 6df8bd9741d7285f81522b566b54697d10ff2a5e.

hanqizheng pushed a commit to hanqizheng/openclaw that referenced this pull request Mar 2, 2026
…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]>
Linux2010 pushed a commit to Linux2010/openclaw that referenced this pull request Mar 2, 2026
…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]>
oskarfreye pushed a commit to oskarfreye/openclaw that referenced this pull request Mar 2, 2026
…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]>
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
…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]>
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
…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]>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 16, 2026
…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)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 16, 2026
…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)
lukeg826 pushed a commit to lukeg826/openclaw that referenced this pull request Mar 26, 2026
…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]>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…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]>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…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]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…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]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…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]>
alexey-pelykh added a commit to remoteclaw/remoteclaw that referenced this pull request Jun 15, 2026
)

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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants