diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f561502dd..96e168f54e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/doctor-gateway-health.test.ts b/src/commands/doctor-gateway-health.test.ts new file mode 100644 index 00000000000..3056a8932ab --- /dev/null +++ b/src/commands/doctor-gateway-health.test.ts @@ -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", + }); + }); +}); diff --git a/src/commands/doctor-gateway-health.ts b/src/commands/doctor-gateway-health.ts index 38468a94aea..8380635fe77 100644 --- a/src/commands/doctor-gateway-health.ts +++ b/src/commands/doctor-gateway-health.ts @@ -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, diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index 8d480b70f84..ddee6c267c5 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -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",