Bug Description
When the Discord WebSocket disconnects (close code 1006), the health-monitor detects stale-socket or disconnected and calls stopChannel → startChannel to restart the Discord provider. However, the Discord channel plugin does not implement stopAccount, so the old WebSocket connection is never properly closed.
Root Cause
In channel-health-monitor.ts, the restart flow calls:
if (status.running) await channelManager.stopChannel(channelId, accountId);
channelManager.resetRestartAttempts(channelId, accountId);
await channelManager.startChannel(channelId, accountId);
But stopChannel checks:
if (!plugin?.gateway?.stopAccount && store.aborts.size === 0 && store.tasks.size === 0) return;
Since the Discord plugin has no stopAccount, and if no active abort controllers or tasks exist, stopChannel returns immediately without closing anything. The abort signal alone does not trigger client.destroy() on the Discord.js Client.
Then startChannel creates a new Discord Client and calls login(), while the old Client's WebSocket connection is still alive in the same process. Discord sees two sessions with the same token and rejects the new IDENTIFY handshake. The gateway enters a death loop:
health-monitor: restarting (reason: stale-socket)
→ stopChannel (no-op)
→ startChannel (new Client, old WS still alive)
→ IDENTIFY rejected → gatewayConnected=false
→ health-monitor detects disconnected again
→ repeat every 5-10 minutes forever
Evidence
Observed on OpenClaw v2026.3.13, Node 25.8.1 (macOS arm64):
- 08:02 — WS connected successfully (
gatewayConnected=true)
- 08:33 — WS close code 1006, resume failed
- 10:07 → 13:34 — health-monitor restarts every ~10min, all fail with
gatewayConnected=false
- Even full process restarts (new PIDs via launchd) failed if Discord had not yet expired the old session
- Manual stop + 12-min wait → success (Discord session timeout)
Comparison: another instance on the same network recovered because a SIGUSR1-triggered full process restart killed all in-memory state (including the old Client), allowing a clean connection.
Expected Behavior
The Discord plugin should implement stopAccount that properly destroys the Discord.js Client (sends a WebSocket close frame), allowing Discord to immediately release the session. The new Client can then successfully IDENTIFY.
Suggested Fix
Add a stopAccount implementation to the Discord channel plugin that calls the equivalent of client.destroy() to send a proper WS close frame before the health-monitor creates a new Client.
Workaround
Full process restart (SIGUSR1 or openclaw gateway restart) clears all in-memory state and allows clean reconnection, but may require waiting for Discord to expire the old session (~1-5 minutes).
Environment
- OpenClaw: v2026.3.13
- Node.js: v25.8.1
- OS: macOS (arm64)
- discord-api-types: 0.38.42
Bug Description
When the Discord WebSocket disconnects (close code 1006), the health-monitor detects
stale-socketordisconnectedand callsstopChannel→startChannelto restart the Discord provider. However, the Discord channel plugin does not implementstopAccount, so the old WebSocket connection is never properly closed.Root Cause
In
channel-health-monitor.ts, the restart flow calls:But
stopChannelchecks:Since the Discord plugin has no
stopAccount, and if no active abort controllers or tasks exist,stopChannelreturns immediately without closing anything. The abort signal alone does not triggerclient.destroy()on the Discord.js Client.Then
startChannelcreates a new Discord Client and callslogin(), while the old Client's WebSocket connection is still alive in the same process. Discord sees two sessions with the same token and rejects the new IDENTIFY handshake. The gateway enters a death loop:Evidence
Observed on OpenClaw v2026.3.13, Node 25.8.1 (macOS arm64):
gatewayConnected=true)gatewayConnected=falseComparison: another instance on the same network recovered because a SIGUSR1-triggered full process restart killed all in-memory state (including the old Client), allowing a clean connection.
Expected Behavior
The Discord plugin should implement
stopAccountthat properly destroys the Discord.js Client (sends a WebSocket close frame), allowing Discord to immediately release the session. The new Client can then successfully IDENTIFY.Suggested Fix
Add a
stopAccountimplementation to the Discord channel plugin that calls the equivalent ofclient.destroy()to send a proper WS close frame before the health-monitor creates a new Client.Workaround
Full process restart (
SIGUSR1oropenclaw gateway restart) clears all in-memory state and allows clean reconnection, but may require waiting for Discord to expire the old session (~1-5 minutes).Environment