Summary
WhatsApp Web connection enters a self-reinforcing loop: the health monitor restarts the provider after 30 min of no events, but each new connection gets a 440 session conflict from WhatsApp and immediately stops. This repeats indefinitely (51+ times in a single day), causing all outbound delivery to fail while appearing "connected" in the logs.
Environment
- OpenClaw version: 2026.3.13 (61d171a) — also reproduced on 2026.3.12
- OS: Linux 6.8.0-100-generic (x64), Ubuntu, systemd VPS deployment
- Node: v22.22.0
- Baileys version: @whiskeysockets/baileys 7.0.0-rc.9
- WhatsApp type: Business account, single linked device (only OpenClaw)
Timeline
- March 13, 16:26 UTC: Last successful WhatsApp message (inbound + auto-reply)
- March 14-15: Zero actual inbound/outbound messages. Loop running silently all weekend. Gateway restarted on Mar 15 at 10:36 + 14:02 UTC — did not help.
- March 16, all day: 25 health-monitor-triggered restarts (51 total stale-socket events counting both WhatsApp + Slack). Delivery failures on all crons.
Root Cause Analysis (code-level)
Step 1: 440 causes permanent monitoring shutdown
In channel-web-DXX6S0Of.js:
function isNonRetryableWebCloseStatus(statusCode) {
return statusCode === 440; // connectionReplaced in baileys
}
// ...
if (isNonRetryableWebCloseStatus(statusCode)) {
runtime.error(`WhatsApp Web connection closed (status ${statusCode}: session conflict). Stopping web monitoring.`);
await closeListener();
break; // exits reconnect loop permanently
}
When WhatsApp sends a 440 (baileys DisconnectReason.connectionReplaced), OpenClaw calls closeListener() and breaks out of the monitoring loop entirely. The channel enters a stopped state with running=false, connected=false, lastEventAt=Date.now().
Step 2: Health monitor sees "stale" and restarts
In gateway-cli-CuZs0RlJ.js:
const DEFAULT_CHECK_INTERVAL_MS = 5 * 60_000; // 5 min
// staleEventThresholdMs defaults to 18e5 = 1,800,000ms = 30 min
// Health evaluation:
if (policy.now - snapshot.lastEventAt > policy.staleEventThresholdMs) {
return { healthy: false, reason: "stale-socket" };
}
// On unhealthy: stops channel, then starts it again
if (status.running) await channelManager.stopChannel(channelId, accountId);
channelManager.resetRestartAttempts(channelId, accountId);
await channelManager.startChannel(channelId, accountId);
After the 440 shutdown, lastEventAt is set to Date.now(). So 30 minutes later, the health monitor fires, sees no events, and calls startChannel() again. The new connection gets another 440. Loop repeats every ~35 minutes (30 min threshold + 5 min check interval).
Step 3: Why 440 keeps happening
The 440 (connectionReplaced) means WhatsApp believes another session has replaced this one. Possible causes:
- The health monitor's
stopChannel() does not fully close the Baileys WebSocket before startChannel() creates a new one — WhatsApp server may still consider the old session active
- There is a race between
closeListener() (from the 440 handler) and startChannel() (from health monitor) — if the health monitor fires while the old connection is still technically registered server-side, the new connection triggers another 440
- A stale session entry on WhatsApp's servers persists between reconnects
Supporting evidence
Both WhatsApp AND Slack restart at the exact same millisecond every cycle, suggesting a global sweep:
Mar 16 00:32:28 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
Mar 16 00:32:28 [health-monitor] [slack:default] health-monitor: restarting (reason: stale-socket)
(Slack does not get 440 and reconnects fine — this just shows the health monitor fires simultaneously for all channels.)
The 428 case: At 11:00 UTC on Mar 16, got a 428 (Precondition Required) — this IS retryable and reconnected in 2.25s. But within 30 min of no inbound messages, health monitor triggered again, new connection got 440, loop resumed.
Connection appears "healthy" to health monitor after 440 shutdown because lastEventAt=Date.now() is set on shutdown, resetting the 30-min clock. The health monitor cannot distinguish "stopped cleanly after 440" from "stale-socket".
What we tried (all failed)
npm update -g openclaw (2026.3.12 to 2026.3.13) — no change
openclaw channels login --channel whatsapp — auto-reconnects without QR, gets 440 again within minutes
- Verified only 1 linked device on phone (WhatsApp Settings > Linked Devices)
- User manually removed + re-linked device from phone settings
- Multiple gateway restarts via systemd
Proposed fixes
Option A (quick fix): After a 440, set a longer backoff (e.g. 5-10 min) before the health monitor is allowed to restart, to give WhatsApp's server time to clean up the old session.
Option B (proper fix): On 440, mark the channel as "conflict-stopped" (distinct from "stale") and do NOT automatically restart it. Require explicit user action to re-establish. This prevents the infinite self-restart loop.
Option C (Baileys level): Investigate whether closeListener() fully tears down the WebSocket before returning. If the close is async/fire-and-forget, the old session may still be live on WhatsApp's end when the new connection is made.
Log samples
# Exact 35-min cycle:
Mar 16 00:32:28 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
Mar 16 01:07:28 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
Mar 16 01:42:28 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
... (22 more times through the day)
# The 440 that starts each cycle:
Mar 16 16:56:40 [whatsapp] Web connection closed (status 440: session conflict). Resolve conflicting WhatsApp Web sessions, then relink. Stopping web monitoring.
# Brief working window — 428 is retryable, reconnects fine:
Mar 16 11:00:07 [whatsapp] Web connection closed (status 428). Retry 1/12 in 2.25s
Mar 16 11:00:11 [whatsapp] Listening for personal WhatsApp inbound messages.
# 32 min later — no actual events received, health monitor fires:
Mar 16 11:32:28 [health-monitor] [whatsapp:default] health-monitor: restarting (reason: stale-socket)
# New connection immediately gets 440 -> stops -> cycle continues
Impact
- All cron announce delivery (meeting prep, executive digest, reminders) silently fails
- Sub-agents cannot send WhatsApp messages
- Outbound from main agent intermittent (works only in brief windows post-restart before first 440)
- No user-visible alert when the loop starts — crons silently drop messages for days
- Issue undetectable until user manually notices missing digests
Summary
WhatsApp Web connection enters a self-reinforcing loop: the health monitor restarts the provider after 30 min of no events, but each new connection gets a 440 session conflict from WhatsApp and immediately stops. This repeats indefinitely (51+ times in a single day), causing all outbound delivery to fail while appearing "connected" in the logs.
Environment
Timeline
Root Cause Analysis (code-level)
Step 1: 440 causes permanent monitoring shutdown
In
channel-web-DXX6S0Of.js:When WhatsApp sends a 440 (baileys
DisconnectReason.connectionReplaced), OpenClaw callscloseListener()and breaks out of the monitoring loop entirely. The channel enters a stopped state withrunning=false,connected=false,lastEventAt=Date.now().Step 2: Health monitor sees "stale" and restarts
In
gateway-cli-CuZs0RlJ.js:After the 440 shutdown,
lastEventAtis set toDate.now(). So 30 minutes later, the health monitor fires, sees no events, and callsstartChannel()again. The new connection gets another 440. Loop repeats every ~35 minutes (30 min threshold + 5 min check interval).Step 3: Why 440 keeps happening
The 440 (
connectionReplaced) means WhatsApp believes another session has replaced this one. Possible causes:stopChannel()does not fully close the Baileys WebSocket beforestartChannel()creates a new one — WhatsApp server may still consider the old session activecloseListener()(from the 440 handler) andstartChannel()(from health monitor) — if the health monitor fires while the old connection is still technically registered server-side, the new connection triggers another 440Supporting evidence
Both WhatsApp AND Slack restart at the exact same millisecond every cycle, suggesting a global sweep:
(Slack does not get 440 and reconnects fine — this just shows the health monitor fires simultaneously for all channels.)
The 428 case: At 11:00 UTC on Mar 16, got a 428 (Precondition Required) — this IS retryable and reconnected in 2.25s. But within 30 min of no inbound messages, health monitor triggered again, new connection got 440, loop resumed.
Connection appears "healthy" to health monitor after 440 shutdown because
lastEventAt=Date.now()is set on shutdown, resetting the 30-min clock. The health monitor cannot distinguish "stopped cleanly after 440" from "stale-socket".What we tried (all failed)
npm update -g openclaw(2026.3.12 to 2026.3.13) — no changeopenclaw channels login --channel whatsapp— auto-reconnects without QR, gets 440 again within minutesProposed fixes
Option A (quick fix): After a 440, set a longer backoff (e.g. 5-10 min) before the health monitor is allowed to restart, to give WhatsApp's server time to clean up the old session.
Option B (proper fix): On 440, mark the channel as "conflict-stopped" (distinct from "stale") and do NOT automatically restart it. Require explicit user action to re-establish. This prevents the infinite self-restart loop.
Option C (Baileys level): Investigate whether
closeListener()fully tears down the WebSocket before returning. If the close is async/fire-and-forget, the old session may still be live on WhatsApp's end when the new connection is made.Log samples
Impact