Skip to content

fix(cron): clear unresolved next-run to stop refire loops (#2702)#2709

Merged
alexey-pelykh merged 1 commit into
mainfrom
fix/cron-unresolved-next-run-refire-2702
Jun 15, 2026
Merged

fix(cron): clear unresolved next-run to stop refire loops (#2702)#2709
alexey-pelykh merged 1 commit into
mainfrom
fix/cron-unresolved-next-run-refire-2702

Conversation

@alexey-pelykh

Copy link
Copy Markdown

Summary

Ports the upstream OpenClaw fix openclaw#66083 ("stop unresolved next-run refire loops") into the fork's consolidated cron service. Closes #2702.

Root cause. When computeJobNextRunAtMs cannot resolve a recurring cron job's next run (returns undefined, or throws → caught), applyJobResult synthesized a phantom nextRunAtMs:

  • success path → endedAt + MIN_REFIRE_GAP_MS (the 2 s spin-loop guard)
  • error path → endedAt + backoff (the error backoff delay)

A synthesized time looks due on the next tick, so the job refires forever even though its schedule never actually resolved.

Fix. Add resolveCronNextRunWithLowerBound (cron-kind only): when the natural next run is unresolved, clear the schedule (undefined) instead of synthesizing a time. every jobs keep the arithmetic backoff fallback unchanged. The fork already has the recovery half — armTimer keeps a maintenance recheck armed while enabled jobs exist, and recomputeNextRunsForMaintenance always repairs a missing nextRunAtMs — so once the masking phantom time is gone, an initially-unresolved job is correctly re-armed and fires on a later tick once its next run becomes resolvable, instead of silently stalling.

Scope note (the "entangled refactor" the issue warns about)

The issue's referenced commit e1fe71872c3 and follow-up #66113 are written against upstream's post-SQLite-migration cron service; their test-case-3 backoff-floor semantics depend on that larger timer refactor (which "does not apply cleanly to the fork"). This PR ports #66083 — the coherent, minimal fix that satisfies both acceptance criteria — and intentionally does not drag in the SQLite-era evolution.

Changes

  • src/cron/service/timer.ts — add resolveCronNextRunWithLowerBound; apply it in both branches of applyJobResult for cron-kind jobs.
  • src/cron/service.issue-66019-unresolved-next-run.test.ts (new)onTimer integration coverage: no spurious refire (success + errored), and reschedule-and-fire on a later tick once the next run resolves.
  • src/cron/service.issue-regressions.test.ts — reconcile the two #30905 throw-path tests to the new clear-on-unresolved behavior (matching upstream verbatim), and add the unit-level #66019 applyJobResult regression cases.

Acceptance criteria

  • A cron job whose next run is initially unresolved is correctly rescheduled and fires on a subsequent tick (positive integration test).
  • A regression test asserts the refire behavior and passes in isolation (verified: all 3 new integration tests fail without the fix, pass with it).

Verification

  • src/cron/ unit suite: 452 passed (49 files).
  • pnpm check (format + tsgo typecheck + oxlint type-aware + fork lint gates): clean.
  • Fork-integrity gates (throwing-stub-callers, stub-debt, rebrand): clean.
  • Touches src/cron/, not src/middleware/, so the LIVE smoke-test gate does not apply.

🤖 Generated with Claude Code

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]>
@alexey-pelykh
alexey-pelykh merged commit ec14511 into main Jun 15, 2026
17 checks passed
@alexey-pelykh
alexey-pelykh deleted the fix/cron-unresolved-next-run-refire-2702 branch June 15, 2026 09:05
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.

cron: port upstream fix for unresolved next-run not refiring

1 participant