Skip to content

Cron scheduler timer never fires after gateway startup (v2026.2.3) #10702

@troyhoffman-oss

Description

@troyhoffman-oss

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

  1. Have cron jobs configured (tested with */15 7-23 * * * and 0 7-21 * * *)
  2. Update OpenClaw to 2026.2.3 via npm install -g openclaw
  3. Restart gateway (openclaw gateway restart)
  4. Observe: "cron: started" log with correct nextWakeAtMs
  5. Wait past nextWakeAtMs — timer never fires, no jobs execute
  6. Restart again — recomputeNextRuns advances nextRunAtMs to 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
  • lastRunAtMs for 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 load cold 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions