openclaw - ✅(Solved) Fix Discord channel plugin missing stopAccount — health-monitor hot restart leaves stale WS, enters death loop [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#50913Fetched 2026-04-08 01:06:38
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Participants
Timeline (top)
referenced ×4commented ×1cross-referenced ×1

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

Fix Action

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).

PR fix notes

PR #51297: fix(discord): add stopAccount to close stale WS on health-monitor restart

Description (problem / solution / changelog)

Summary

The Discord channel plugin's gateway object was missing a stopAccount() implementation. When the health-monitor triggered a hot restart, the old WebSocket (GatewayPlugin) remained connected, causing stale connections to linger and potentially process duplicate events.

Changes

  • Added stopAccount() to the gateway object in extensions/discord/src/channel.ts
  • On stop: retrieves the active GatewayPlugin via getGateway(accountId), sets reconnect.maxAttempts = 0 to prevent reconnection, calls gateway.disconnect() to close the WebSocket, then calls unregisterGateway(accountId) to clean up the registry

Testing

  • Existing tests cover registerGateway/unregisterGateway lifecycle
  • The stopAccount path mirrors the existing onAbort handler in provider.lifecycle.ts which already uses this pattern

Related

Fixes #50913

Changed files

  • extensions/discord/src/channel.ts (modified, +23/-0)

Code Example

if (status.running) await channelManager.stopChannel(channelId, accountId);
channelManager.resetRestartAttempts(channelId, accountId);
await channelManager.startChannel(channelId, accountId);

---

if (!plugin?.gateway?.stopAccount && store.aborts.size === 0 && store.tasks.size === 0) return;

---

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
RAW_BUFFERClick to expand / collapse

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

extent analysis

Fix Plan

To resolve the issue, we need to implement the stopAccount method in the Discord channel plugin. This method should properly destroy the Discord.js Client, sending a WebSocket close frame to release the session.

Step-by-Step Solution

  • Implement the stopAccount method in the Discord channel plugin:
plugin.gateway.stopAccount = async (accountId) => {
  // Get the Discord.js Client instance for the account
  const client = getClientInstance(accountId);
  
  // Destroy the client to send a WebSocket close frame
  if (client) {
    client.destroy();
  }
};
  • Update the stopChannel method to call stopAccount if implemented:
if (plugin?.gateway?.stopAccount) {
  await plugin.gateway.stopAccount(accountId);
}
  • Verify that the stopAccount method is called before creating a new Discord Client in the startChannel method.

Verification

To verify that the fix worked, monitor the health-monitor logs and Discord WebSocket connections. After implementing the stopAccount method, the health-monitor should be able to restart the Discord provider successfully without entering a death loop.

Extra Tips

  • Ensure that the stopAccount method is properly cleaning up any resources associated with the Discord.js Client.
  • Consider adding logging to verify that the stopAccount method is being called and that the client is being destroyed successfully.
  • If issues persist, verify that the Discord session timeout is not interfering with the new connection attempts.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix Discord channel plugin missing stopAccount — health-monitor hot restart leaves stale WS, enters death loop [1 pull requests, 1 comments, 2 participants]