From d9119f079185d70205c8410af4ebe2103be9ab41 Mon Sep 17 00:00:00 2001 From: Mitch McAlister Date: Tue, 3 Mar 2026 00:28:05 +0000 Subject: [PATCH] fix(discord): push connected status when gateway is already connected at lifecycle start When the Discord gateway completes its READY handshake before `runDiscordGatewayLifecycle` registers its debug event listener, the initial "WebSocket connection opened" event is missed. This leaves `connected` as undefined in the channel runtime, causing the health monitor to treat the channel as "stuck" and restart it every check cycle. Check `gateway.isConnected` immediately after registering the debug listener and push the initial connected status if the gateway is already connected. Co-Authored-By: Claude Opus 4.6 --- .../monitor/provider.lifecycle.test.ts | 23 +++++++++++++++++++ src/discord/monitor/provider.lifecycle.ts | 13 +++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/discord/monitor/provider.lifecycle.test.ts b/src/discord/monitor/provider.lifecycle.test.ts index 961a8170d..278a6b257 100644 --- a/src/discord/monitor/provider.lifecycle.test.ts +++ b/src/discord/monitor/provider.lifecycle.test.ts @@ -77,6 +77,7 @@ describe("runDiscordGatewayLifecycle", () => { const runtimeError = vi.fn(); const runtimeExit = vi.fn(); const releaseEarlyGatewayErrorGuard = vi.fn(); + const statusSink = vi.fn(); const runtime: RuntimeEnv = { log: runtimeLog, error: runtimeError, @@ -89,6 +90,7 @@ describe("runDiscordGatewayLifecycle", () => { runtimeLog, runtimeError, releaseEarlyGatewayErrorGuard, + statusSink, lifecycleParams: { accountId: params?.accountId ?? "default", client: { @@ -102,6 +104,7 @@ describe("runDiscordGatewayLifecycle", () => { threadBindings: { stop: threadStop }, pendingGatewayErrors: params?.pendingGatewayErrors, releaseEarlyGatewayErrorGuard, + statusSink, }, }; }; @@ -203,6 +206,26 @@ describe("runDiscordGatewayLifecycle", () => { }); }); + it("pushes connected status when gateway is already connected at lifecycle start", async () => { + const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js"); + const { emitter, gateway } = createGatewayHarness(); + gateway.isConnected = true; + getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter); + + const { lifecycleParams, statusSink } = createLifecycleHarness({ gateway }); + await expect(runDiscordGatewayLifecycle(lifecycleParams)).resolves.toBeUndefined(); + + const connectedCall = statusSink.mock.calls.find( + ([patch]: [Record]) => patch.connected === true, + ); + expect(connectedCall).toBeDefined(); + expect(connectedCall![0]).toMatchObject({ + connected: true, + lastDisconnect: null, + }); + expect(connectedCall![0].lastConnectedAt).toBeTypeOf("number"); + }); + it("handles queued disallowed intents errors without waiting for gateway events", async () => { const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js"); const { diff --git a/src/discord/monitor/provider.lifecycle.ts b/src/discord/monitor/provider.lifecycle.ts index 4504f6d03..23e414840 100644 --- a/src/discord/monitor/provider.lifecycle.ts +++ b/src/discord/monitor/provider.lifecycle.ts @@ -244,6 +244,19 @@ export async function runDiscordGatewayLifecycle(params: { }; gatewayEmitter?.on("debug", onGatewayDebug); + // If the gateway is already connected when the lifecycle starts (the + // "WebSocket connection opened" debug event was emitted before we + // registered the listener above), push the initial connected status now. + if (gateway?.isConnected) { + const at = Date.now(); + pushStatus({ + connected: true, + lastEventAt: at, + lastConnectedAt: at, + lastDisconnect: null, + }); + } + let sawDisallowedIntents = false; const logGatewayError = (err: unknown) => { if (params.isDisallowedIntentsError(err)) {