fix(discord): add stopAccount to close stale WS on health-monitor restart#51297
fix(discord): add stopAccount to close stale WS on health-monitor restart#51297ajitpratap0 wants to merge 2 commits into
Conversation
Greptile SummaryThis PR adds a Key changes:
Potential concern:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/discord/src/channel.ts
Line: 701
Comment:
**Missing error-event suppressor before `disconnect()`**
The analogous `onAbort` handler in `provider.lifecycle.ts` installs a one-time error listener on `gatewayEmitter` _before_ calling `gateway.disconnect()`:
```ts
gatewayEmitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();
```
This is a deliberate defensive measure: disconnecting an active WebSocket can emit an `"error"` event on the emitter. When the lifecycle is still running, `waitForDiscordGatewayStop`'s own error handler will pick that event up and run it through `shouldStopOnGatewayError` — which returns `true` for `"Max reconnect attempts"`, a message that may appear immediately after `maxAttempts` is set to `0`. If that path fires, the lifecycle exits via the error branch rather than cleanly.
To fully mirror `onAbort`, consider importing `getDiscordGatewayEmitter` (already available from `../monitor.gateway.js` as used in `provider.lifecycle.ts`) and suppressing the error event before disconnecting:
```ts
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
// inside stopAccount:
const emitter = getDiscordGatewayEmitter(gateway);
emitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "fix(discord): add st..." |
| try { | ||
| // Prevent reconnect attempts and forcibly close the WebSocket | ||
| gateway.options.reconnect = { maxAttempts: 0 }; | ||
| gateway.disconnect(); |
There was a problem hiding this comment.
Missing error-event suppressor before
disconnect()
The analogous onAbort handler in provider.lifecycle.ts installs a one-time error listener on gatewayEmitter before calling gateway.disconnect():
gatewayEmitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();This is a deliberate defensive measure: disconnecting an active WebSocket can emit an "error" event on the emitter. When the lifecycle is still running, waitForDiscordGatewayStop's own error handler will pick that event up and run it through shouldStopOnGatewayError — which returns true for "Max reconnect attempts", a message that may appear immediately after maxAttempts is set to 0. If that path fires, the lifecycle exits via the error branch rather than cleanly.
To fully mirror onAbort, consider importing getDiscordGatewayEmitter (already available from ../monitor.gateway.js as used in provider.lifecycle.ts) and suppressing the error event before disconnecting:
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
// inside stopAccount:
const emitter = getDiscordGatewayEmitter(gateway);
emitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/discord/src/channel.ts
Line: 701
Comment:
**Missing error-event suppressor before `disconnect()`**
The analogous `onAbort` handler in `provider.lifecycle.ts` installs a one-time error listener on `gatewayEmitter` _before_ calling `gateway.disconnect()`:
```ts
gatewayEmitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();
```
This is a deliberate defensive measure: disconnecting an active WebSocket can emit an `"error"` event on the emitter. When the lifecycle is still running, `waitForDiscordGatewayStop`'s own error handler will pick that event up and run it through `shouldStopOnGatewayError` — which returns `true` for `"Max reconnect attempts"`, a message that may appear immediately after `maxAttempts` is set to `0`. If that path fires, the lifecycle exits via the error branch rather than cleanly.
To fully mirror `onAbort`, consider importing `getDiscordGatewayEmitter` (already available from `../monitor.gateway.js` as used in `provider.lifecycle.ts`) and suppressing the error event before disconnecting:
```ts
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
// inside stopAccount:
const emitter = getDiscordGatewayEmitter(gateway);
emitter?.once("error", () => {});
gateway.options.reconnect = { maxAttempts: 0 };
gateway.disconnect();
```
How can I resolve this? If you propose a fix, please make it concise.31f50e3 to
1501d31
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1501d31421
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const accountId = ctx.accountId; | ||
| const gateway = getGateway(accountId); |
There was a problem hiding this comment.
Look up gateway with normalized account ID
stopAccount uses ctx.accountId for getGateway, but this context field is the raw config key from server-channels while Discord gateways are registered under the normalized account.accountId (see resolveDiscordAccount + startAccount). For accounts whose key is not already canonical (e.g., mixed case or punctuation), this lookup misses, so the stale gateway is never disconnected/unregistered and hot restarts can still leave duplicate WebSocket listeners running.
Useful? React with 👍 / 👎.
…tart The Discord channel plugin's gateway object lacked a stopAccount() implementation. When the health-monitor triggered a hot restart, the old WebSocket/GatewayPlugin remained connected and lingered until garbage collection, causing duplicate event processing and resource leaks. Add stopAccount() that: - Retrieves the registered GatewayPlugin for the account via getGateway() - Disables reconnect (maxAttempts: 0) to prevent re-connection after disconnect - Calls gateway.disconnect() to close the WebSocket immediately - Unregisters the gateway from the module-level registry via unregisterGateway() Fixes openclaw#50913
…count - `getDiscordGatewayEmitter` + error suppressor were already present from the original commit; no change needed for Issue 1. - Normalize `ctx.accountId` via `resolveDiscordAccount` before the `getGateway` lookup so non-canonical account keys resolve to the same ID under which the gateway was registered (matching `startAccount`). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1501d31 to
8bae765
Compare
|
Closing this as not reproducible on current Current What I checked:
Review notes: reviewed against 608c08fc54da. |
Summary
The Discord channel plugin's
gatewayobject was missing astopAccount()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
stopAccount()to thegatewayobject inextensions/discord/src/channel.tsGatewayPluginviagetGateway(accountId), setsreconnect.maxAttempts = 0to prevent reconnection, callsgateway.disconnect()to close the WebSocket, then callsunregisterGateway(accountId)to clean up the registryTesting
registerGateway/unregisterGatewaylifecyclestopAccountpath mirrors the existingonAborthandler inprovider.lifecycle.tswhich already uses this patternRelated
Fixes #50913