fix: suppress discord reconnect exhaustion during shutdown

This commit is contained in:
Peter Steinberger
2026-05-02 12:43:17 +01:00
parent d90a08a447
commit 03df3539e9
3 changed files with 64 additions and 1 deletions

View File

@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
- Hooks/doctor: warn when `hooks.transformsDir` points outside the canonical hooks transform directory, so invalid workspace skill paths get a direct recovery hint before the Gateway crash-loops. Fixes #75853. Thanks @midobk.
- Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5.
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
- Discord: treat abort-time Carbon reconnect-exhausted events as expected shutdown during stale-socket restarts, so health-monitor restarts no longer reject the monitor lifecycle. Carries forward #58216; supersedes #73949. Thanks @Perttulands.
- Discord/native commands: return an explicit warning when slash command dispatch or direct plugin execution produces no visible reply instead of a success-style completion ack. Fixes #58986; supersedes #62057. Thanks @jb510.
- Discord: keep typing indicators alive during long tool runs and auto-compaction while keepalive ticks continue, so active sessions do not appear stalled before the final reply. Thanks @Squirbie.
- Discord: preserve multipart Content-Type headers for attachment uploads across REST fetch paths, so generated images and other media no longer fail delivery with `CONTENT_TYPE_INVALID`. Thanks @FunJim.

View File

@@ -175,12 +175,13 @@ describe("runDiscordGatewayLifecycle", () => {
threadStop: ReturnType<typeof vi.fn>;
waitCalls: number;
gatewaySupervisor: { detachLifecycle: ReturnType<typeof vi.fn> };
detachCalls?: number;
}) {
expect(waitForDiscordGatewayStopMock).toHaveBeenCalledTimes(params.waitCalls);
expect(unregisterGatewayMock).toHaveBeenCalledWith("default");
expect(stopGatewayLoggingMock).toHaveBeenCalledTimes(1);
expect(params.threadStop).toHaveBeenCalledTimes(1);
expect(params.gatewaySupervisor.detachLifecycle).toHaveBeenCalledTimes(1);
expect(params.gatewaySupervisor.detachLifecycle).toHaveBeenCalledTimes(params.detachCalls ?? 1);
}
it("resolves gateway READY timeouts from config, env, then defaults", () => {
@@ -509,6 +510,60 @@ describe("runDiscordGatewayLifecycle", () => {
});
});
it("treats abort-time live reconnect exhaustion as expected shutdown", async () => {
const abortController = new AbortController();
let liveGatewayHandler: ((event: DiscordGatewayEvent) => void) | undefined;
const { lifecycleParams, threadStop, runtimeLog, runtimeError, gatewaySupervisor } =
createLifecycleHarness();
lifecycleParams.abortSignal = abortController.signal;
gatewaySupervisor.attachLifecycle.mockImplementation(
(handler: (event: DiscordGatewayEvent) => void) => {
liveGatewayHandler = handler;
},
);
abortController.signal.addEventListener(
"abort",
() => {
if (!liveGatewayHandler) {
throw new Error("discord gateway lifecycle handler was not attached");
}
liveGatewayHandler(
createGatewayEvent(
"reconnect-exhausted",
"Max reconnect attempts (50) reached after close code 1005",
),
);
},
{ once: true },
);
waitForDiscordGatewayStopMock.mockImplementationOnce(async (waitParams) => {
const actual =
await vi.importActual<typeof import("../monitor.gateway.js")>("../monitor.gateway.js");
const waitPromise = actual.waitForDiscordGatewayStop(waitParams);
abortController.abort(new Error("shutdown"));
return await waitPromise;
});
await expect(runDiscordGatewayLifecycle(lifecycleParams)).resolves.toBeUndefined();
expect(gatewaySupervisor.attachLifecycle).toHaveBeenCalledTimes(1);
expect(runtimeLog).toHaveBeenCalledWith(
expect.stringContaining("treating reconnect-exhausted during expected shutdown as clean"),
);
expect(runtimeLog).toHaveBeenCalledWith(
expect.stringContaining("Max reconnect attempts (50) reached after close code 1005"),
);
expect(runtimeError).not.toHaveBeenCalledWith(
expect.stringContaining("discord gateway reconnect-exhausted"),
);
expectLifecycleCleanup({
threadStop,
waitCalls: 1,
gatewaySupervisor,
detachCalls: 2,
});
});
it("surfaces fatal startup gateway errors while waiting for READY", async () => {
vi.useFakeTimers();
try {

View File

@@ -457,6 +457,13 @@ export async function runDiscordGatewayLifecycle(params: {
let sawDisallowedIntents = false;
const handleGatewayEvent = (event: DiscordGatewayEvent): "continue" | "stop" => {
if (params.abortSignal?.aborted && event.type === "reconnect-exhausted") {
lifecycleStopping = true;
params.runtime.log?.(
`discord: treating reconnect-exhausted during expected shutdown as clean: ${event.message}`,
);
return "continue";
}
if (event.type === "disallowed-intents") {
lifecycleStopping = true;
sawDisallowedIntents = true;