Skip to content

Discord channel plugin missing stopAccount — health-monitor hot restart leaves stale WS, enters death loop #50913

Description

@doldrums2025

Bug Description

When the Discord WebSocket disconnects (close code 1006), the health-monitor detects stale-socket or disconnected and calls stopChannelstartChannel 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

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