Skip to content

Feature request: Make staleEventThresholdMs configurable (WhatsApp restart loop on low-traffic accounts) #51342

Description

@raistlin88

Summary

OpenClaw's WhatsApp channel health monitor restarts the WhatsApp Web socket every ~35 minutes on personal/low-traffic accounts. This creates a perpetual restart cycle that causes cron job delivery failures when jobs fire during the brief reconnection window. We observed 4,004 stale-socket restarts in 17 days and applied a config mitigation (channelHealthCheckMinutes: 360). We believe the root cause warrants a product fix: making staleEventThresholdMs configurable.

OpenClaw version: 2026.3.13
Platform: macOS (Darwin 25.2.0), local installation
WhatsApp: Multi-device, personal account
Active cron jobs: ~68 across 14 agents


The Problem

What we observed

Starting 2026-03-02, our gateway log showed a continuous pattern:

2026-03-18T15:39:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T16:14:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T16:49:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
2026-03-18T17:24:40 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
...

This pattern repeats 24/7, including overnight, weekends, and any quiet period.

Total stale-socket restarts: grep -c 'stale-socket' gateway.log4,004 over 17 days (~235/day).

Impact on cron jobs

We run ~68 active cron jobs across 14 agents (family reminders, daily digests, memory maintenance, weekly reviews). When a cron job fires during the brief window where WhatsApp is reconnecting, it fails with:

Error: No active WhatsApp Web listener (account: default).
Start the gateway, then link WhatsApp with: openclaw channels login --channel whatsapp --account default.

This caused 6 cron jobs to accumulate consecutive errors, including family reminders, morning digest delivery, and nightly memory retention jobs.

The error message is misleading — WhatsApp IS linked and the gateway IS running. The socket is simply in the middle of a health-monitor-triggered restart cycle.


Root Cause Analysis

How the health monitor works

We traced the behavior through the source code:

Layer 1: External health monitor (channel-health-policy.ts)

  • Runs every channelHealthCheckMinutes (default: 5 minutes)
  • For each connected channel, checks lastEventAt timestamp
  • If no event has been received for DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS (30 minutes, hardcoded at line 54), classifies the socket as "stale-socket"
  • Action: stops and restarts the channel

Layer 2: Internal watchdog (monitor.ts)

  • Independent from Layer 1
  • MESSAGE_TIMEOUT_MS = 30 * 60 * 1000 (30 minutes)
  • Checks every 60 seconds
  • If no message received for 30+ minutes, calls listener.signalClose() with status 499

Both layers use the same 30-minute threshold. Neither is configurable via openclaw.json.

Why it creates a restart loop on personal accounts

The logic assumes: "If no WhatsApp event arrives in 30 minutes, the socket is probably dead."

This assumption is valid for high-traffic business accounts but false for personal accounts where:

  • Overnight quiet periods (23:00–07:00) routinely have zero messages for 8+ hours
  • Weekend mornings may have 2-3 hour gaps between messages
  • Even during the day, a family account can easily go 30-60 minutes without a message

The result: Every 30 minutes of legitimate silence, the health monitor tears down a perfectly healthy WebSocket connection and restarts it. The restart takes a few seconds, during which the WhatsApp listener is unavailable. If a cron job fires in that window, it fails.

The restart cycle timeline

T+0:00  Health monitor checks → socket connected, lastEvent was 31 min ago → "stale-socket" → RESTART
T+0:05  Socket reconnects → WhatsApp Web connected → healthy
T+0:35  Health monitor checks → socket connected, lastEvent was 30 min ago → "stale-socket" → RESTART
T+0:40  Socket reconnects → healthy
T+1:10  Health monitor checks → stale again → RESTART
...repeat forever

With the default 5-minute check interval and 30-minute stale threshold, this produces a restart every ~35 minutes = ~41 restarts/day.


Source Code References

Component File Line Value
Stale threshold (hardcoded) src/gateway/channel-health-policy.ts 54 DEFAULT_CHANNEL_STALE_EVENT_THRESHOLD_MS = 30 * 60_000
Health check interval (configurable) src/gateway/server.impl.ts 758-765 gateway.channelHealthCheckMinutes (default 5)
Disable check (special case) src/gateway/server.impl.ts 759 channelHealthCheckMinutes === 0 → monitor disabled
Config schema src/config/zod-schema.ts 698 z.number().int().min(0).optional()
Internal watchdog timeout src/web/auto-reply/monitor.ts MESSAGE_TIMEOUT_MS = 30 * 60 * 1000
Internal watchdog check src/web/auto-reply/monitor.ts WATCHDOG_CHECK_MS = 60 * 1000

Current Mitigation

We set channelHealthCheckMinutes: 360 to reduce restarts from ~41/day to ~4/day:

"gateway": {
  "channelHealthCheckMinutes": 360
}
Option Effect Trade-off
0 (disable) No health monitoring Too risky — genuine dead sockets go undetected
60 (1 hour) ~24 restarts/day Reduces but doesn't solve
360 (6 hours) ~4 restarts/day Chosen — safety net preserved, quiet periods survivable

This is a workaround, not a fix. The check interval controls how often the monitor runs, not what it considers stale. Even at 360 minutes, a quiet channel is still classified as stale at each check.


Feature Request

Make staleEventThresholdMs configurable

Add a config key to control the stale-socket detection threshold:

"gateway": {
  "channelHealthCheckMinutes": 5,
  "channelStaleEventThresholdMinutes": 360
}

This would allow the health monitor to run frequently (catching genuine outages quickly) while tolerating long quiet periods on personal accounts.

Why the check interval alone is insufficient: With channelHealthCheckMinutes: 5 and a configurable stale threshold of 360 minutes, the monitor would check every 5 minutes but only restart if the socket has been truly silent for 6 hours. This is the ideal configuration — fast detection of real problems, zero false positives from normal quiet periods.

Suggested defaults

  • Current 30-minute default is reasonable for high-traffic business accounts
  • For personal accounts, 120-360 minutes would be appropriate
  • A documented recommendation in the configuration docs would help users choose

Additional consideration: internal watchdog

The internal watchdog in monitor.ts also uses a 30-minute timeout (MESSAGE_TIMEOUT_MS). If the external health monitor threshold becomes configurable, the internal watchdog should either:

  • Respect the same config value, or
  • Be independently configurable, or
  • Be documented as a separate recovery mechanism with its own fixed threshold

Evidence

Evidence Source Finding
4,004 stale-socket restarts grep -c 'stale-socket' gateway.log Continuous since 2026-03-02
~35-minute cycle grep 'stale-socket' gateway.log timestamps ~35min intervals
6 cron jobs in error state openclaw cron list "No active WhatsApp Web listener"
WhatsApp linked during errors openclaw doctor --non-interactive "WhatsApp: linked (auth age Xm)" confirmed while cron errors occurred
Config key valid src/config/zod-schema.ts:698 z.number().int().min(0).optional() — 360 passes
Threshold hardcoded src/gateway/channel-health-policy.ts:54 No config override available

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions