-
-
Notifications
You must be signed in to change notification settings - Fork 52.7k
Description
Summary
After updating to OpenClaw 2026.2.3, the cron scheduler's setTimeout timer armed during start() never fires. All cron jobs silently stop executing. The gateway process stays alive and handles Discord messages, but the cron tick callback never runs.
Environment
- OpenClaw version: 2026.2.3 (upgraded from 2026.1.30)
- Node.js: 22.22.0
- Platform: macOS (Darwin 25.2.0, Apple Silicon)
- Gateway: launchd service, port 18789
Reproduction
- Have cron jobs configured (tested with
*/15 7-23 * * *and0 7-21 * * *) - Update OpenClaw to 2026.2.3 via
npm install -g openclaw - Restart gateway (
openclaw gateway restart) - Observe: "cron: started" log with correct
nextWakeAtMs - Wait past
nextWakeAtMs— timer never fires, no jobs execute - Restart again —
recomputeNextRunsadvancesnextRunAtMsto next future slot, prior windows silently skipped
Evidence
Six gateway startups on 2026-02-06, each logging cron: started with correct nextWakeAtMs. Zero cron executions between any pair of startups:
| Startup Time (CT) | nextWakeAtMs target |
Timer fired? |
|---|---|---|
| 10:32 (old code CHghbhEZ) | 10:45 | No — restarted at 11:05 |
| 11:05 (new code CqYA0Lb3) | 11:15 | No — restarted at 11:18 |
| 11:18 | 11:30 | No — 83 min silence, process alive |
| 12:41 (SIGUSR1) | 12:45 | No — config reload at 12:42 |
| 12:42 (config reload) | 12:45 | No — 3+ hours silence, process alive |
lastRunAtMsfor all cron jobs is frozen at pre-update timestamps (10:00 and 10:30 CT)- Process is alive during gaps — handles Discord messages, WebSocket reconnects
- Both SIGUSR1 hot-restarts and
launchctl loadcold starts exhibit the behavior
Affected Code Path
In gateway-cli-CqYA0Lb3.js, the armTimer() function:
function armTimer(state) {
if (state.timer) clearTimeout(state.timer);
state.timer = null;
if (!state.deps.cronEnabled) return;
const nextAt = nextWakeAtMs(state);
if (!nextAt) return;
const delay = Math.max(nextAt - state.deps.nowMs(), 0);
const clampedDelay = Math.min(delay, MAX_TIMEOUT_MS);
state.timer = setTimeout(() => {
onTimer(state).catch((err) => {
state.deps.log.error({ err: String(err) }, "cron: timer tick failed");
});
}, clampedDelay);
state.timer.unref?.();
}Timer is armed with correct delay, but the callback never executes. No "cron: timer tick failed" errors in logs either. The start() → recomputeNextRuns() → persist() → armTimer() sequence completes and logs correctly — it's only the subsequent setTimeout callback that silently fails.
Workaround
None found yet. Hard restart (gateway stop + launchctl load) re-arms the timer but it still doesn't fire. Testing a fresh cold start now.
Impact
All cron-scheduled jobs stop executing after update. For agents relying on cron heartbeats (e.g., every 15 minutes), this means complete loss of autonomous operation with no errors or warnings.