fix: harden startup readiness and discord replies

(cherry picked from commit 3956672106b3387d42427a485a9ca01e77f3b78f)
This commit is contained in:
Satoshi
2026-05-04 14:42:16 +01:00
committed by Peter Steinberger
parent 7e229f0d3d
commit e259938e96
12 changed files with 381 additions and 49 deletions

View File

@@ -118,7 +118,11 @@ export async function withOperatorApprovalsGatewayClient<T>(
clientOptions: { preauthHandshakeTimeoutMs: params.config.gateway?.handshakeTimeoutMs },
});
if (!readiness.ready) {
throw new Error("gateway event loop readiness timeout");
throw new Error(
readiness.aborted
? "gateway approval client start aborted before readiness"
: "gateway readiness unavailable before approval client start",
);
}
await ready;
return await run(gatewayClient);

View File

@@ -2,6 +2,7 @@ export const STARTUP_UNAVAILABLE_GATEWAY_METHODS = [
"agent.wait",
"chat.history",
"models.list",
"sessions.list",
"sessions.abort",
"sessions.create",
"sessions.send",

View File

@@ -232,6 +232,55 @@ describe("startChannelApprovalHandlerBootstrap", () => {
await cleanup();
});
it("defers retryable gateway readiness startup failures without terminal error logs", async () => {
vi.useFakeTimers();
const channelRuntime = createRuntimeChannel();
const readinessError = new Error("gateway event loop readiness timeout");
const start = vi.fn().mockRejectedValueOnce(readinessError).mockResolvedValueOnce(undefined);
const stop = vi.fn().mockResolvedValue(undefined);
const logger = {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
child: vi.fn(),
isEnabled: vi.fn().mockReturnValue(true),
isVerboseEnabled: vi.fn().mockReturnValue(false),
verbose: vi.fn(),
};
createChannelApprovalHandlerFromCapability
.mockResolvedValueOnce({ start, stop })
.mockResolvedValueOnce({ start, stop });
const cleanup = await startTestBootstrap({ channelRuntime, logger });
registerApprovalContext(channelRuntime);
await flushTransitions();
expect(start).toHaveBeenCalledTimes(1);
await flushTransitions();
expect(logger.error).not.toHaveBeenCalledWith(
expect.stringContaining("failed to start native approval handler"),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining("native approval handler deferred until gateway readiness recovers"),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining("gateway readiness unavailable before approval handler start"),
);
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("gateway event loop readiness timeout"),
);
await vi.advanceTimersByTimeAsync(1_000);
await flushTransitions();
expect(createChannelApprovalHandlerFromCapability).toHaveBeenCalledTimes(2);
expect(start).toHaveBeenCalledTimes(2);
await cleanup();
});
it("does not retry terminal native approval startup failures", async () => {
vi.useFakeTimers();
const channelRuntime = createRuntimeChannel();

View File

@@ -17,6 +17,28 @@ import { isExecApprovalChannelRuntimeTerminalStartError } from "./exec-approval-
type ApprovalBootstrapHandler = ChannelApprovalHandler;
const APPROVAL_HANDLER_BOOTSTRAP_RETRY_MS = 1_000;
function isRetryableApprovalBootstrapStartError(error: unknown): boolean {
const message = String(error);
return (
message.includes("gateway readiness unavailable before approval client start") ||
message.includes("gateway approval client start aborted before readiness") ||
message.includes("gateway readiness unavailable before exec approval runtime start") ||
message.includes("gateway approval runtime start aborted before readiness") ||
message.includes("gateway event loop readiness timeout") ||
message.includes("gateway starting") ||
message.includes("code=1013") ||
message.includes("close code 1013")
);
}
function formatRetryableApprovalBootstrapStartError(error: unknown): string {
const message = String(error);
if (message.includes("gateway event loop readiness timeout")) {
return "gateway readiness unavailable before approval handler start";
}
return message;
}
export async function startChannelApprovalHandlerBootstrap(params: {
plugin: Pick<ChannelPlugin, "id" | "meta" | "approvalCapability">;
cfg: OpenClawConfig;
@@ -122,6 +144,13 @@ export async function startChannelApprovalHandlerBootstrap(params: {
logger.error(`native approval handler disabled: ${String(error)}`);
return;
}
if (isRetryableApprovalBootstrapStartError(error)) {
logger.warn(
`native approval handler deferred until gateway readiness recovers: ${formatRetryableApprovalBootstrapStartError(error)}`,
);
scheduleRetryForContext(context, generation);
return;
}
logger.error(`failed to start native approval handler: ${String(error)}`);
scheduleRetryForContext(context, generation);
}

View File

@@ -291,7 +291,9 @@ describe("createExecApprovalChannelRuntime", () => {
finalizeResolved: async () => undefined,
});
await expect(runtime.start()).rejects.toThrow("gateway event loop readiness timeout");
await expect(runtime.start()).rejects.toThrow(
"gateway readiness unavailable before exec approval runtime start",
);
expect(mockGatewayClientStarts).not.toHaveBeenCalled();
expect(mockGatewayClientStops).toHaveBeenCalledTimes(1);

View File

@@ -365,7 +365,11 @@ export function createExecApprovalChannelRuntime<
},
});
if (!readiness.ready) {
throw new Error("gateway event loop readiness timeout");
throw new Error(
readiness.aborted
? "gateway approval runtime start aborted before readiness"
: "gateway readiness unavailable before exec approval runtime start",
);
}
await ready;
if (stopClientIfInactive(client)) {