mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:50:46 +00:00
fix: suppress discord reconnect exhaustion during shutdown
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user