openclaw - ✅(Solved) Fix cron: v2026.4.2 silently drops jobs.json (plain-array format) and first add clobbers all data [2 pull requests, 1 comments, 1 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#60799Fetched 2026-04-08 02:47:02
View on GitHub
Comments
1
Participants
1
Timeline
3
Reactions
0
Participants
Timeline (top)
commented ×1cross-referenced ×1referenced ×1

After upgrading to 2026.4.2, the gateway silently reports jobCount: 0 at startup despite ~/.openclaw/cron/jobs.json containing 37 valid, working cron jobs. The jobs are valid JSON, parse correctly, and were fully operational on the prior version. On first cron add or job write, only the new job is persisted — all 37 existing jobs are permanently lost.

Error Message

  • At minimum, the loader logs an ERROR explaining why it rejects the file.
  1. Does not log an error or warning when it fails to parse the file.
  2. Explicit error logging: If the file cannot be parsed, log a WARN or ERROR with the first 200 characters so operators can diagnose instead of seeing silent zero-count failures.

Root Cause

The 2026.4.2 cron loader appears to have changed the store format from a plain JSON array ([{job}, ...]) to a versioned envelope ({ "version": 1, "jobs": [{job}, ...] }). The loader:

  1. Does not recognize or migrate the old plain-array format.
  2. Does not log an error or warning when it fails to parse the file.
  3. Reports jobCount: 0 silently, making operators believe there were never jobs.
  4. Writes the new versioned format on first mutation, silently clobbering all old data.

Fix Action

Fix / Workaround

  1. Run a working OpenClaw instance on 2026.3.x with persisted cron jobs in ~/.openclaw/cron/jobs.json (plain JSON array format: [{job}, {job}, ...]).
  2. Upgrade to 2026.4.2.
  3. Restart the gateway.
  4. Observe cron list returns 0 jobs.
  5. Run cron add to create a test job.
  6. Observe the file is rewritten with only the new job — all previous jobs are gone.

PR fix notes

PR #61113: fix: normalize legacy plain-array cron stores on load

Description (problem / solution / changelog)

Fixes #60799

Root cause: parseCronStoreForBackupComparison() rejects top-level arrays and loadCronStore() converts them to empty objects, so legacy plain-array jobs.json files load as { version: 1, jobs: [] }. First cron write then overwrites the store with only newly-added jobs, dropping all existing ones.

Fix: Extract a normalizeCronStore(parsed) helper that accepts both legacy top-level arrays and the current { version: 1, jobs: [...] } envelope. Used in both parseCronStoreForBackupComparison() and loadCronStore().

Tests: Two new regression tests in store.test.ts:

  • Loads a legacy plain-array store correctly
  • Preserves legacy jobs through a load-add-save round trip

2 files changed, 72 insertions, 21 deletions.

Changed files

  • src/cron/store.test.ts (modified, +50/-0)
  • src/cron/store.ts (modified, +22/-21)

PR #68112: fix(cron): prevent scheduler death when startup catch-up fails

Description (problem / solution / changelog)

Summary

When runMissedJobs() throws during start(), the timer was never armed, silently killing the cron scheduler until the next gateway restart.

Root Cause

The start() function in src/cron/service/ops.ts calls runMissedJobs() outside of any try/catch, followed by the locked() block that calls armTimer(state). If runMissedJobs() throws (e.g. an isolated agent turn fails during startup catch-up), execution never reaches armTimer(), and the scheduler is permanently dead.

Changes

  1. Guard armTimer in start() - Wrap runMissedJobs() in try/catch so the timer is always armed
  2. Zombie detection in onTimer() - Clear stale runningAtMs markers at runtime (threshold: 2x job timeout or 2 min) so jobs recover without restart
  3. Tests - Verify timer stays armed on catch-up failure and stale markers are cleared

Fixes #67854 Related: #59056, #60799

Changed files

  • src/cron/service.start-error-resilience.test.ts (added, +409/-0)
  • src/cron/service/ops.ts (modified, +86/-5)
  • src/cron/service/timer.ts (modified, +78/-1)

Code Example

{"module":"cron","storePath":"/home/moltbot/.openclaw/cron/jobs.json"}
{"jobCount":0,"enabledCount":0,"withNextRun":0}
"cron: armTimer skipped - no jobs with nextRunAtMs"
"cron: started"

---

{"module":"cron","storePath":"/home/moltbot/.openclaw/cron/jobs.json"}
{"jobCount":0,"enabledCount":0,"withNextRun":0}
"cron: armTimer skipped - no jobs with nextRunAtMs"
"cron: started"

---

$ python3 -c "import json; jobs=json.load(open(\"/home/moltbot/.openclaw/cron/jobs.json\")); print(len(jobs))"
37
$ python3 -c "import json; jobs=json.load(open(\"/home/moltbot/.openclaw/cron/jobs.json\")); print(jobs[0].keys())"
["id","agentId","sessionKey","name","enabled","createdAtMs","updatedAtMs","schedule","sessionTarget","wakeMode","payload","delivery","state","failureAlert","description"]

---

{
  "version": 1,
  "jobs": [{ /* only the newly-added test job */ }]
}
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails) / Data Loss

OpenClaw version

OpenClaw 2026.4.2 (d74a122)

OS and install method

  • Linux 6.8.0-106-generic (x64),
  • Node.js v24.14.1,
  • npm global (~/.npm-global/lib/node_modules/openclaw),
  • systemd user service (systemctl --user).

Summary

After upgrading to 2026.4.2, the gateway silently reports jobCount: 0 at startup despite ~/.openclaw/cron/jobs.json containing 37 valid, working cron jobs. The jobs are valid JSON, parse correctly, and were fully operational on the prior version. On first cron add or job write, only the new job is persisted — all 37 existing jobs are permanently lost.

Steps to reproduce

  1. Run a working OpenClaw instance on 2026.3.x with persisted cron jobs in ~/.openclaw/cron/jobs.json (plain JSON array format: [{job}, {job}, ...]).
  2. Upgrade to 2026.4.2.
  3. Restart the gateway.
  4. Observe cron list returns 0 jobs.
  5. Run cron add to create a test job.
  6. Observe the file is rewritten with only the new job — all previous jobs are gone.

Expected behavior

  • The loader reads existing valid JSON arrays and loads all jobs, or
  • If a format migration is required, it migrates the old format and warns, or
  • At minimum, the loader logs an ERROR explaining why it rejects the file.

Actual behavior

Gateway logs show exactly:

{"module":"cron","storePath":"/home/moltbot/.openclaw/cron/jobs.json"}
{"jobCount":0,"enabledCount":0,"withNextRun":0}
"cron: armTimer skipped - no jobs with nextRunAtMs"
"cron: started"

Zero jobCount, zero warnings, zero errors. The file is valid JSON but silently skipped. The first subsequent write clobbers the file: it creates a new { "version": 1, "jobs": [...] } envelope containing only the newly-added job, destroying all pre-existing entries.

Logs and evidence

Gateway startup logjobCount: 0 despite valid file:

{"module":"cron","storePath":"/home/moltbot/.openclaw/cron/jobs.json"}
{"jobCount":0,"enabledCount":0,"withNextRun":0}
"cron: armTimer skipped - no jobs with nextRunAtMs"
"cron: started"

Before write — file is valid plain array with 37 items:

$ python3 -c "import json; jobs=json.load(open(\"/home/moltbot/.openclaw/cron/jobs.json\")); print(len(jobs))"
37
$ python3 -c "import json; jobs=json.load(open(\"/home/moltbot/.openclaw/cron/jobs.json\")); print(jobs[0].keys())"
["id","agentId","sessionKey","name","enabled","createdAtMs","updatedAtMs","schedule","sessionTarget","wakeMode","payload","delivery","state","failureAlert","description"]

After cron add — file overwritten with single job in new versioned format:

{
  "version": 1,
  "jobs": [{ /* only the newly-added test job */ }]
}

The add call itself succeeds (returns a valid job ID), but the write path ignores the pre-existing 37 jobs and replaces them.

Root cause analysis

The 2026.4.2 cron loader appears to have changed the store format from a plain JSON array ([{job}, ...]) to a versioned envelope ({ "version": 1, "jobs": [{job}, ...] }). The loader:

  1. Does not recognize or migrate the old plain-array format.
  2. Does not log an error or warning when it fails to parse the file.
  3. Reports jobCount: 0 silently, making operators believe there were never jobs.
  4. Writes the new versioned format on first mutation, silently clobbering all old data.

Impact

  • Data loss is automatic and irreversible: the first write after startup permanently destroys all pre-migration jobs. With 37 jobs in my case, all scheduling, delivery config, failure alerts, and job history are lost.
  • openclaw doctor --fix does not detect or migrate the issue.
  • This is the third distinct report of cron jobs being silently lost (see Related issues).

Related issues

  • #52569 — "Cron jobs silently lost after upgrading 2026.3.12 → 2026.3.13" (same symptom, older version)
  • #53746 — "saveCronStore overwrites jobs.json from partial in-memory state after restart" (same destructive-write pattern)
  • #60334 — "2026.4.2 update rejects legacy config keys and cron jobs.json may require manual backup restore" (same version, though reporter had malformed JSON)
  • #33451 — "legacy cron jobs with string schedule + top-level command are not migrated on load" (related migration gap)

None of the above have been resolved. This report adds the specific v2026.4.2 format-change migration gap with full evidence and repro.

Proposed fix

  1. Backward-compatible loader: The cron store loader should detect both formats (plain array and versioned envelope) and load them transparently.
  2. Graceful migration: If the file is a plain array, wrap it in { "version": 1, "jobs": [...] } on the next write.
  3. Fail-safe write path: Use a read-merge-write pattern instead of writing-only-in-memory-state (see #53746 for details).
  4. Explicit error logging: If the file cannot be parsed, log a WARN or ERROR with the first 200 characters so operators can diagnose instead of seeing silent zero-count failures.

extent analysis

TL;DR

To fix the issue of silently lost cron jobs after upgrading to OpenClaw 2026.4.2, implement a backward-compatible loader that detects and loads both plain array and versioned envelope formats, and use a read-merge-write pattern to prevent data loss.

Guidance

  1. Verify the issue: Confirm that the jobs.json file contains a plain JSON array and that the issue reproduces after upgrading to 2026.4.2.
  2. Manually migrate the file: Before running cron add, manually wrap the plain array in a versioned envelope ({ "version": 1, "jobs": [...] }) to prevent data loss.
  3. Test the proposed fix: Implement the proposed fix (backward-compatible loader, graceful migration, fail-safe write path, and explicit error logging) and verify that it resolves the issue.
  4. Monitor for related issues: Be aware of related issues (#52569, #53746, #60334, #33451) and test for similar symptoms to ensure a comprehensive fix.

Example

// Before migration
[
  { "id": 1, "agentId": 1, ... },
  { "id": 2, "agentId": 1, ... },
  ...
]

// After manual migration
{
  "version": 1,
  "jobs": [
    { "id": 1, "agentId": 1, ... },
    { "id": 2, "agentId": 1, ... },
    ...
  ]
}

Notes

The proposed fix assumes that the issue is caused by the changed store format in 2026.4.2. If the issue persists after implementing the fix, further investigation may be necessary to identify the root cause.

Recommendation

Apply the proposed fix (backward-compatible loader, graceful migration, fail-safe write path, and explicit error logging) to resolve the issue and prevent data loss. This approach addresses the specific format-change migration gap in 2026.4.2 and provides a more robust solution than a simple workaround.

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…

FAQ

Expected behavior

  • The loader reads existing valid JSON arrays and loads all jobs, or
  • If a format migration is required, it migrates the old format and warns, or
  • At minimum, the loader logs an ERROR explaining why it rejects the file.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING