fix(doctor): treat gateway memory probe timeout as inconclusive (#72618)

This commit is contained in:
Vincent Koc
2026-04-26 22:40:26 -07:00
committed by GitHub
parent 45bdfb5f72
commit 6bbb1b79e1
4 changed files with 87 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Exec/node: skip approval-plan preparation for full-trust `host=node` runs so interpreter and script commands no longer fail with `SYSTEM_RUN_DENIED: approval cannot safely bind` when effective policy is `security=full` and `ask=off`. Fixes #48457 and duplicate #69251. Thanks @ajtran303, @jaserNo1, @Blakeshannon, @lesliefag, and @AvIsBeastMC.
- Exec/node: synthesize a local approval plan when a paired node advertises `system.run` without `system.run.prepare`, unblocking approval-required `host=node` exec on current macOS companion nodes while preserving remote prepare for node hosts that support it. Fixes #37591 and duplicate #66839; carries forward #69725. Thanks @soloclz.
- Memory/QMD: prefer QMD's `--mask` collection pattern flag so root memory indexing stays scoped to `MEMORY.md` instead of widening to every markdown file in the workspace. Thanks @codex.
- Memory/doctor: treat the specific `gateway timeout after ...` gateway memory probe result as inconclusive instead of reporting embeddings not ready, while preserving warnings for explicit failures. Fixes #44426; carries forward #46576 with the Greptile review feedback applied. Thanks Cengiz (@ghost).
- Gateway/memory: defer QMD startup for implicit non-default agents and scope memory runtime loading to the selected memory slot so Gateway boot and first memory recall avoid broad plugin runtime fanout. Thanks @vincentkoc.
- Gateway/startup: keep core request handlers, setup wizard, and channel runtime helpers off the boot path until the first matching request, wizard run, or channel start, reducing no-plugin Gateway ready RSS and avoidable startup imports. Thanks @vincentkoc.
- CLI/Gateway: use a parse-only config snapshot for plain `gateway status` reads and reuse same-path service config context so status no longer spends tens of seconds in full config validation before printing. Thanks @vincentkoc.

View File

@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const callGateway = vi.hoisted(() => vi.fn());
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails: vi.fn(() => ({
message: "Gateway target: ws://127.0.0.1:18789",
})),
callGateway,
}));
vi.mock("./health.js", () => ({
healthCommand: vi.fn(),
}));
import { probeGatewayMemoryStatus } from "./doctor-gateway-health.js";
describe("probeGatewayMemoryStatus", () => {
const cfg = {} as OpenClawConfig;
beforeEach(() => {
callGateway.mockReset();
});
it("treats outer gateway timeouts as inconclusive", async () => {
callGateway.mockRejectedValue(
new Error("gateway timeout after 8000ms\nGateway target: ws://127.0.0.1:18789"),
);
await expect(probeGatewayMemoryStatus({ cfg })).resolves.toEqual({
checked: false,
ready: false,
error: expect.stringContaining("gateway memory probe timed out"),
});
});
it("keeps gateway request timeouts as explicit failures", async () => {
callGateway.mockRejectedValue(new Error("gateway request timeout for doctor.memory.status"));
await expect(probeGatewayMemoryStatus({ cfg })).resolves.toEqual({
checked: true,
ready: false,
error: "gateway memory probe unavailable: gateway request timeout for doctor.memory.status",
});
});
it("keeps non-timeout gateway errors as explicit failures", async () => {
callGateway.mockRejectedValue(new Error("gateway closed (1006): no close reason"));
await expect(probeGatewayMemoryStatus({ cfg })).resolves.toEqual({
checked: true,
ready: false,
error: "gateway memory probe unavailable: gateway closed (1006): no close reason",
});
});
});

View File

@@ -14,6 +14,10 @@ export type GatewayMemoryProbe = {
error?: string;
};
function isGatewayCallTimeout(message: string): boolean {
return /^gateway timeout after \d+ms(?:\n|$)/.test(message);
}
export async function checkGatewayHealth(params: {
runtime: RuntimeEnv;
cfg: OpenClawConfig;
@@ -84,6 +88,13 @@ export async function probeGatewayMemoryStatus(params: {
};
} catch (err) {
const message = formatErrorMessage(err);
if (isGatewayCallTimeout(message)) {
return {
checked: false,
ready: false,
error: `gateway memory probe timed out: ${message}`,
};
}
return {
checked: true,
ready: false,

View File

@@ -228,6 +228,24 @@ describe("noteMemorySearchHealth", () => {
expect(note).not.toHaveBeenCalled();
});
it("does not treat an inconclusive gateway timeout as local embeddings not ready", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "local",
local: {},
remote: {},
});
await noteMemorySearchHealth(cfg, {
gatewayMemoryProbe: {
checked: false,
ready: false,
error: "gateway memory probe timed out: gateway timeout after 8000ms",
},
});
expect(note).not.toHaveBeenCalled();
});
it("does not warn when local provider has an explicit hf: modelPath", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "local",