openclaw - ✅(Solved) Fix cronScheduleIdentity should not throw on malformed job entries (kills scheduler tick) [2 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#75886Fetched 2026-05-02 05:28:30
View on GitHub
Comments
2
Participants
3
Timeline
7
Reactions
2
Author
Timeline (top)
commented ×2cross-referenced ×2referenced ×2closed ×1

Error Message

if (!schedule) throw new Error("Unsupported cron schedule kind"); // ← throws cronSchedulingInputsEqual should use tryCronScheduleIdentity (which returns undefined on error) instead of the throwing cronScheduleIdentity. Treat undefined === undefined as "schedules differ, invalidate nextRunAtMs" or skip the comparison entirely. console.error([cron] Skipping malformed job ${job.name || job.id}: ${err.message});

  • The error message "Unsupported cron schedule kind" appears to be the only place this throw occurs
  • Detection: Poor — The only visible symptom is jobs not running and error logs that may be missed

Root Cause

In dist/store-DKoc1lYY.js:

function cronScheduleIdentity(job) {
    const schedule = resolveSchedulePayload(job);
    if (!schedule) throw new Error("Unsupported cron schedule kind");  // ← throws
    return JSON.stringify({
        version: 1,
        enabled: job.enabled ?? true,
        schedule
    });
}

function cronSchedulingInputsEqual(previous, next) {
    return cronScheduleIdentity(previous) === cronScheduleIdentity(next);  // ← uncaught throw
}

When resolveSchedulePayload returns undefined (for string-shaped or otherwise invalid schedule fields), the throw aborts the entire scheduler tick.

Fix Action

Fixed

PR fix notes

PR #75896: fix(cron): make cronSchedulingInputsEqual non-throwing for malformed schedule entries

Description (problem / solution / changelog)

Fixes #75886.

Problem

cronScheduleIdentity throws "Unsupported cron schedule kind" when no schedule payload can be resolved from a persisted job (e.g. a bare string-shaped schedule field left by a legacy migration or manual edit).

ensureLoaded calls invalidateStaleNextRunOnScheduleChange → cronSchedulingInputsEqual → cronScheduleIdentity for every persisted job during force-reload. A malformed job throws inside that loop, aborts the tick before onTimer can collect due jobs, and kills the scheduler for all subsequent entries until the next reload.

Fix

Change cronSchedulingInputsEqual to use tryCronScheduleIdentity (the non-throwing variant that returns undefined for an unrecognized schedule shape) instead of the throwing cronScheduleIdentity. When either side resolves to undefined, the function returns true (treat as equal / no schedule change), which preserves the stored nextRunAtMs and lets the reload continue.

tryCronScheduleIdentity already exists and is used by the outer store.ts for the scheduleIdentity fingerprint. This change brings reload-comparison in line with the same non-throwing contract.

Changes

  • src/cron/schedule-identity.tscronSchedulingInputsEqual uses tryCronScheduleIdentity; returns true if either side is undefined.
  • src/cron/service/store.test.ts — regression test: write a string-shaped schedule, force-reload, assert ensureLoaded resolves without throwing and nextRunAtMs is preserved.
  • CHANGELOG.md — Fixes entry under Unreleased.

Tests

pnpm test src/cron/service/store.test.ts src/cron/store.test.ts src/cron/service/timer.regression.test.ts
# 65/65 passed (64 existing + 1 new regression)

pnpm exec oxfmt --check --threads=1 src/cron/schedule-identity.ts src/cron/service/store.ts src/cron/normalize.ts src/cron/service/store.test.ts src/cron/store.test.ts CHANGELOG.md
# All matched files use the correct format.

Changed files

  • CHANGELOG.md (modified, +1/-3)
  • src/cron/schedule-identity.ts (modified, +6/-1)
  • src/cron/service/store.test.ts (modified, +40/-0)

PR #75926: fix(cron): use non-throwing schedule identity in store reload (#75886)

Description (problem / solution / changelog)

Closes #75886.

What changed

  • cronScheduleIdentity no longer throws on malformed/legacy persisted jobs. The internal helper is replaced with cronScheduleIdentityOrNull, which returns a stable identity string for any input. Malformed schedules (e.g. a bare-string schedule field) hash to a marked sentinel keyed off the raw value.
  • cronSchedulingInputsEqual is now total (never throws) and accepts unknown-shaped inputs, so the store-reload comparison in ensureLoaded can no longer kill the scheduler tick.
  • ensureLoaded isolates malformed jobs with a structured one-shot warning per jobId (deduped via the new state.warnedMalformedScheduleJobIds), keeping other valid jobs scheduled and runnable.
  • tryCronScheduleIdentity is unchanged in behavior and is still used as the malformed-detection probe.

Why

Per the issue: a single malformed persisted job (e.g. schedule: "0 6 * * *" instead of { kind: "cron", expr: "0 6 * * *" }) caused cronScheduleIdentity to throw Unsupported cron schedule kind. The throw propagated through cronSchedulingInputsEqualinvalidateStaleNextRunOnScheduleChangeensureLoaded, which is invoked on every tick. Result: nextWakeAtMs froze and no due jobs ran until restart. This matches the clawsweeper recommendation: make reload comparison non-throwing, isolate the bad entry, preserve existing schedule-change invalidation semantics.

Validation

pnpm test src/cron/service/store.test.ts src/cron/store.test.ts src/cron/service/timer.regression.test.ts

→ 67 tests, all passing (3 new regression tests added).

pnpm exec oxfmt --check --threads=1 src/cron/schedule-identity.ts src/cron/service/store.ts src/cron/normalize.ts src/cron/service/store.test.ts src/cron/store.test.ts CHANGELOG.md

→ All matched files use the correct format.

Regression tests added (src/cron/service/store.test.ts)

  1. Reload does not throw on string-shaped schedule — both initial load and forceReload return cleanly; structured warning is emitted with jobId and storePath.
  2. One malformed entry does not stop other jobs — the valid every job still gets nextRunAtMs computed when a malformed entry is present in the same store.
  3. Schedule-change invalidation preserved across malformed shapes — transitioning between two different malformed schedule values still clears stale nextRunAtMs, so a future repair to the persisted entry won't carry over a stale next-run target.

Existing schedule-change invalidation semantics (cron expr change, enabled flag flip, every-anchor change, at-target change, key-order-only no-op) all still pass.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/cron/schedule-identity.ts (modified, +50/-9)
  • src/cron/service.test-harness.ts (modified, +1/-0)
  • src/cron/service/state.ts (modified, +7/-0)
  • src/cron/service/store.test.ts (modified, +153/-0)
  • src/cron/service/store.ts (modified, +24/-1)

Code Example

{
  "name": "Check Failed Alerts",
  "schedule": "0 17 * * *",
  "enabled": false,
  "task": { "message": "Run: bash /path/to/script.sh" },
  "payload": { "kind": "agentTurn" },
  "sessionTarget": "isolated",
  "state": {}
}

---

function cronScheduleIdentity(job) {
    const schedule = resolveSchedulePayload(job);
    if (!schedule) throw new Error("Unsupported cron schedule kind");  // ← throws
    return JSON.stringify({
        version: 1,
        enabled: job.enabled ?? true,
        schedule
    });
}

function cronSchedulingInputsEqual(previous, next) {
    return cronScheduleIdentity(previous) === cronScheduleIdentity(next);  // ← uncaught throw
}

---

function cronSchedulingInputsEqual(previous, next) {
    const prevId = tryCronScheduleIdentity(previous);
    const nextId = tryCronScheduleIdentity(next);
    if (!prevId || !nextId) return false;  // treat malformed as always-changed
    return prevId === nextId;
}

---

try {
    invalidateStaleNextRunOnScheduleChange(...);
} catch (err) {
    console.error(`[cron] Skipping malformed job ${job.name || job.id}: ${err.message}`);
    // continue processing other jobs
}
RAW_BUFFERClick to expand / collapse

Problem

The OpenClaw cron scheduler's cronScheduleIdentity function throws "Unsupported cron schedule kind" when handed a malformed job entry (e.g., string-shaped schedule field instead of an object). This throw propagates up through cronSchedulingInputsEqual and kills the entire scheduler tick, preventing all due jobs from running and freezing nextWakeAtMs in the past.

Reproduction

Add a malformed entry to ~/.openclaw/cron/jobs.json:

{
  "name": "Check Failed Alerts",
  "schedule": "0 17 * * *",
  "enabled": false,
  "task": { "message": "Run: bash /path/to/script.sh" },
  "payload": { "kind": "agentTurn" },
  "sessionTarget": "isolated",
  "state": {}
}

Note:

  • schedule is a string instead of { kind: "cron", expr: "...", tz: "..." }
  • No id field

Once this entry exists, the scheduler will:

  1. Throw "Unsupported cron schedule kind" on every tick
  2. Abort tick processing before running any due jobs
  3. Leave nextWakeAtMs stuck in the past
  4. Retry every 2 seconds (due to MIN_REFIRE_GAP_MS), logging errors continuously

Real-World Incident

This occurred on 2026-04-30/2026-05-01 in production. Multiple cron jobs scheduled for 6 AM, 7 AM, 8 AM, and 1 PM failed to run on time. The scheduler logged repeated "Unsupported cron schedule kind" errors until the malformed entry was manually removed from jobs.json.

Full incident report: See workspace report at /Users/sam/.openclaw/workspace/reports/cron-reliability-2026-05-01.md

Root Cause

In dist/store-DKoc1lYY.js:

function cronScheduleIdentity(job) {
    const schedule = resolveSchedulePayload(job);
    if (!schedule) throw new Error("Unsupported cron schedule kind");  // ← throws
    return JSON.stringify({
        version: 1,
        enabled: job.enabled ?? true,
        schedule
    });
}

function cronSchedulingInputsEqual(previous, next) {
    return cronScheduleIdentity(previous) === cronScheduleIdentity(next);  // ← uncaught throw
}

When resolveSchedulePayload returns undefined (for string-shaped or otherwise invalid schedule fields), the throw aborts the entire scheduler tick.

Proposed Fixes

Any one of these would prevent the incident:

Option A: Use non-throwing variant in equality check

cronSchedulingInputsEqual should use tryCronScheduleIdentity (which returns undefined on error) instead of the throwing cronScheduleIdentity. Treat undefined === undefined as "schedules differ, invalidate nextRunAtMs" or skip the comparison entirely.

function cronSchedulingInputsEqual(previous, next) {
    const prevId = tryCronScheduleIdentity(previous);
    const nextId = tryCronScheduleIdentity(next);
    if (!prevId || !nextId) return false;  // treat malformed as always-changed
    return prevId === nextId;
}

Option B: Normalize malformed schedules at config load time

normalizeCronJobInput should coerce a string schedule field into { kind: "cron", expr: schedule } rather than leaving it as-is. Currently the code only handles isRecord(base.schedule).

Option C: Wrap invalidation in try/catch

ensureLoaded should wrap invalidateStaleNextRunOnScheduleChange in a try/catch and log+skip a single broken entry rather than aborting the entire load.

try {
    invalidateStaleNextRunOnScheduleChange(...);
} catch (err) {
    console.error(`[cron] Skipping malformed job ${job.name || job.id}: ${err.message}`);
    // continue processing other jobs
}

Recommendation

Implement Option B + Option C for defense-in-depth:

  • B prevents the bad data from entering the store
  • C ensures a single bad entry can't kill the entire tick

Related

  • A non-throwing variant tryCronScheduleIdentity already exists in the codebase
  • The error message "Unsupported cron schedule kind" appears to be the only place this throw occurs
  • MIN_REFIRE_GAP_MS = 2000 causes rapid retry loops when the scheduler is stuck

Impact

  • Severity: High — A single malformed cron entry can silently disable the entire scheduler
  • Detection: Poor — The only visible symptom is jobs not running and error logs that may be missed
  • Recovery: Manual — Requires identifying and removing the bad entry from jobs.json

extent analysis

TL;DR

Implementing Option B (normalizing malformed schedules at config load time) and Option C (wrapping invalidation in try/catch) can prevent the scheduler from being disabled by a single malformed cron entry.

Guidance

  • Identify and remove any existing malformed entries from jobs.json to immediately restore scheduler functionality.
  • Implement Option B by modifying normalizeCronJobInput to coerce string schedule fields into the correct object format.
  • Implement Option C by wrapping invalidateStaleNextRunOnScheduleChange in a try/catch block to log and skip individual broken entries.
  • Consider adding additional logging or monitoring to detect and alert on malformed cron entries in the future.

Example

function normalizeCronJobInput(job) {
    if (typeof job.schedule === 'string') {
        job.schedule = { kind: "cron", expr: job.schedule };
    }
    // ... other normalization logic ...
}

Notes

The proposed fixes assume that the tryCronScheduleIdentity function is already implemented and functional. If this is not the case, additional work may be required to create this non-throwing variant.

Recommendation

Apply the combined fix of Option B and Option C to prevent the scheduler from being disabled by malformed cron entries and to ensure that individual broken entries can be logged and skipped without aborting the entire scheduler tick. This approach provides defense-in-depth and helps prevent similar incidents in the future.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix cronScheduleIdentity should not throw on malformed job entries (kills scheduler tick) [2 pull requests, 2 comments, 3 participants]