Skip to content

Discord gateway never connects: Carbon Client constructor doesn't await async GatewayPlugin.registerClient #56492

@seanliuxu

Description

@seanliuxu

Summary

Since the switch to @buape/carbon as the Discord client library (present in at least 2026.3.22+), the Discord gateway WebSocket frequently fails to connect on startup. The bot logs client initialized as ... ; awaiting gateway readiness but never reaches logged in to discord.

Root Cause

In @buape/carbon's Client constructor (node_modules/@buape/carbon/dist/src/classes/Client.js), plugins are registered synchronously:

for (const plugin of plugins) {
    plugin.registerClient?.(this);  // NOT awaited
    plugin.registerRoutes?.(this);
    this.plugins.push({ id: plugin.id, plugin });
}

However, SafeGatewayPlugin.registerClient (in openclaw's provider code) is async — it fetches gateway metadata via fetchDiscordGatewayInfoWithTimeout() before calling super.registerClient(client)this.connect(). Because the promise is fire-and-forget, the gateway connection starts asynchronously with no guaranteed completion before the rest of the startup flow proceeds.

In practice, the async registration often completes too late or its result is silently lost, leaving lifecycleGateway.isConnected === false permanently.

Reproduction

  1. Install openclaw 2026.3.23 or 2026.3.24
  2. Start the gateway with a valid Discord bot token
  3. Observe logs: client initialized as ... ; awaiting gateway readiness appears but logged in to discord never follows
  4. lsof confirms no outbound WebSocket connection to Discord gateway servers

Workaround

Added a wait loop in the provider after deployDiscordCommands that polls lifecycleGateway.isConnected || lifecycleGateway.isConnecting for up to 15 seconds, then triggers an explicit gateway.connect() if needed:

if (lifecycleGateway && !lifecycleGateway.isConnected) {
    for (let i = 0; i < 30; i++) {
        await new Promise(r => setTimeout(r, 500));
        if (lifecycleGateway.isConnected || lifecycleGateway.isConnecting) break;
    }
    if (!lifecycleGateway.isConnected && !lifecycleGateway.isConnecting) {
        if (!lifecycleGateway.gatewayInfo) {
            const resp = await fetch("https://discord.com/api/v10/gateway/bot",
                { headers: { Authorization: `Bot ${token}` } });
            lifecycleGateway.gatewayInfo = await resp.json();
        }
        lifecycleGateway.connect();
    }
}

Suggested Fix

The cleanest fix would be to ensure the gateway plugin's async registration completes before proceeding. Options:

  1. In openclaw's provider: await the gateway plugin's registerClient after constructing the Client, e.g. await client.getPlugin("gateway")?.registerClient?.(client) or store and await the promise.
  2. In @buape/carbon's Client: change the constructor to store plugin registration promises and expose an async init() method, or make registerClient synchronous by deferring the gateway info fetch.

Environment

  • openclaw 2026.3.23 / 2026.3.24
  • Node.js v25.8.1
  • macOS (Apple Silicon)
  • @buape/carbon (bundled in openclaw)

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already exists

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions