fix: bound configured acp binding readiness

This commit is contained in:
Peter Steinberger
2026-04-26 02:36:38 +01:00
parent 29463b9c47
commit 8ed52c1463
3 changed files with 77 additions and 3 deletions

View File

@@ -75,6 +75,9 @@ Docs: https://docs.openclaw.ai
- ACP: wait for the configured runtime backend to become healthy before startup
identity reconciliation, avoiding transient acpx warnings during Gateway boot.
Fixes #40566.
- Channels/ACP bindings: time out configured binding readiness checks instead of
letting Discord preflight hang forever when an ACP target never settles. Fixes
#68776.
- Control UI: hide the chat loading skeleton during background history reloads
when existing messages or active stream content are already visible, avoiding
reload flashes on high-latency local gateways. Fixes #71844. Thanks

View File

@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
__testing,
registerSessionBindingAdapter,
@@ -6,7 +6,14 @@ import {
type SessionBindingRecord,
} from "../../infra/outbound/session-binding-service.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import { resolveRuntimeConversationBindingRoute } from "./binding-routing.js";
import {
ensureConfiguredBindingRouteReady,
resolveRuntimeConversationBindingRoute,
} from "./binding-routing.js";
import {
registerStatefulBindingTargetDriver,
unregisterStatefulBindingTargetDriver,
} from "./stateful-target-drivers.js";
function createRoute(): ResolvedAgentRoute {
return {
@@ -112,3 +119,35 @@ describe("runtime conversation binding route", () => {
expect(result.route).toBe(route);
});
});
describe("ensureConfiguredBindingRouteReady", () => {
afterEach(() => {
vi.useRealTimers();
unregisterStatefulBindingTargetDriver("slow");
});
it("returns a bounded failure when target readiness never settles", async () => {
vi.useFakeTimers();
registerStatefulBindingTargetDriver({
id: "slow",
ensureReady: async () => await new Promise<never>(() => {}),
ensureSession: async () => ({
ok: false,
sessionKey: "agent:slow:binding",
error: "not used",
}),
});
const resultPromise = ensureConfiguredBindingRouteReady({
cfg: {} as never,
bindingResolution: { statefulTarget: { driverId: "slow" } } as never,
});
await vi.advanceTimersByTimeAsync(30_000);
await expect(resultPromise).resolves.toEqual({
ok: false,
error: "Configured binding route ready check timed out",
});
});
});

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { logVerbose } from "../../globals.js";
import {
getSessionBindingService,
type ConversationRef,
@@ -11,6 +12,8 @@ import { resolveConfiguredBinding } from "./binding-registry.js";
import { ensureConfiguredBindingTargetReady } from "./binding-targets.js";
import type { ConfiguredBindingResolution } from "./binding-types.js";
const CONFIGURED_BINDING_ROUTE_READY_TIMEOUT_MS = 30_000;
export type ConfiguredBindingRouteResult = {
bindingResolution: ConfiguredBindingResolution | null;
route: ResolvedAgentRoute;
@@ -152,5 +155,34 @@ export async function ensureConfiguredBindingRouteReady(params: {
cfg: OpenClawConfig;
bindingResolution: ConfiguredBindingResolution | null;
}): Promise<{ ok: true } | { ok: false; error: string }> {
return await ensureConfiguredBindingTargetReady(params);
const readyPromise = ensureConfiguredBindingTargetReady(params);
let timer: ReturnType<typeof setTimeout> | undefined;
const timeoutToken = Symbol("configured-binding-route-ready-timeout");
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
timer = setTimeout(() => resolve(timeoutToken), CONFIGURED_BINDING_ROUTE_READY_TIMEOUT_MS);
timer.unref?.();
});
try {
const result = await Promise.race([readyPromise, timeoutPromise]);
if (result !== timeoutToken) {
return result;
}
logVerbose(
`configured binding route ready check timed out after ${
CONFIGURED_BINDING_ROUTE_READY_TIMEOUT_MS / 1_000
}s`,
);
readyPromise.then(
(lateResult) =>
logVerbose(
`configured binding route ready check settled after timeout (ok=${lateResult.ok})`,
),
(err) =>
logVerbose(`configured binding route ready check rejected after timeout: ${String(err)}`),
);
return { ok: false, error: "Configured binding route ready check timed out" };
} finally {
clearTimeout(timer);
}
}