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 <noreply@anthropic.com>
This commit is contained in:
Mitch McAlister
2026-03-03 00:28:05 +00:00
committed by Peter Steinberger
parent 586f057c24
commit d9119f0791
2 changed files with 36 additions and 0 deletions

View File

@@ -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<string, unknown>]) => 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 {

View File

@@ -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)) {