fix(doctor): use gateway health status for memory search key check (#22327)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 2f02ec9403
Co-authored-by: therk <901920+therk@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Ruslan Kharitonov
2026-02-23 14:07:16 -05:00
committed by GitHub
parent bf373eeb43
commit 8d69251475
11 changed files with 338 additions and 11 deletions

View File

@@ -43,6 +43,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
],
[READ_SCOPE]: [
"health",
"doctor.memory.status",
"logs.tail",
"channels.status",
"status",

View File

@@ -3,6 +3,7 @@ import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "./events.js";
const BASE_METHODS = [
"health",
"doctor.memory.status",
"logs.tail",
"channels.status",
"channels.logout",

View File

@@ -12,6 +12,7 @@ import { configHandlers } from "./server-methods/config.js";
import { connectHandlers } from "./server-methods/connect.js";
import { cronHandlers } from "./server-methods/cron.js";
import { deviceHandlers } from "./server-methods/devices.js";
import { doctorHandlers } from "./server-methods/doctor.js";
import { execApprovalsHandlers } from "./server-methods/exec-approvals.js";
import { healthHandlers } from "./server-methods/health.js";
import { logsHandlers } from "./server-methods/logs.js";
@@ -71,6 +72,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
...chatHandlers,
...cronHandlers,
...deviceHandlers,
...doctorHandlers,
...execApprovalsHandlers,
...webHandlers,
...modelsHandlers,

View File

@@ -0,0 +1,128 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
const loadConfig = vi.hoisted(() => vi.fn(() => ({}) as OpenClawConfig));
const resolveDefaultAgentId = vi.hoisted(() => vi.fn(() => "main"));
const getMemorySearchManager = vi.hoisted(() => vi.fn());
vi.mock("../../config/config.js", () => ({
loadConfig,
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId,
}));
vi.mock("../../memory/index.js", () => ({
getMemorySearchManager,
}));
import { doctorHandlers } from "./doctor.js";
describe("doctor.memory.status", () => {
beforeEach(() => {
loadConfig.mockClear();
resolveDefaultAgentId.mockClear();
getMemorySearchManager.mockReset();
});
it("returns gateway embedding probe status for the default agent", async () => {
const close = vi.fn().mockResolvedValue(undefined);
getMemorySearchManager.mockResolvedValue({
manager: {
status: () => ({ provider: "gemini" }),
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
close,
},
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
expect(getMemorySearchManager).toHaveBeenCalledWith({
cfg: expect.any(Object),
agentId: "main",
purpose: "status",
});
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
provider: "gemini",
embedding: { ok: true },
},
undefined,
);
expect(close).toHaveBeenCalled();
});
it("returns unavailable when memory manager is missing", async () => {
getMemorySearchManager.mockResolvedValue({
manager: null,
error: "memory search unavailable",
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
embedding: {
ok: false,
error: "memory search unavailable",
},
},
undefined,
);
});
it("returns probe failure when manager probe throws", async () => {
const close = vi.fn().mockResolvedValue(undefined);
getMemorySearchManager.mockResolvedValue({
manager: {
status: () => ({ provider: "openai" }),
probeEmbeddingAvailability: vi.fn().mockRejectedValue(new Error("timeout")),
close,
},
});
const respond = vi.fn();
await doctorHandlers["doctor.memory.status"]({
req: {} as never,
params: {} as never,
respond: respond as never,
context: {} as never,
client: null,
isWebchatConnect: () => false,
});
expect(respond).toHaveBeenCalledWith(
true,
{
agentId: "main",
embedding: {
ok: false,
error: "gateway memory probe failed: timeout",
},
},
undefined,
);
expect(close).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,62 @@
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { loadConfig } from "../../config/config.js";
import { getMemorySearchManager } from "../../memory/index.js";
import { formatError } from "../server-utils.js";
import type { GatewayRequestHandlers } from "./types.js";
export type DoctorMemoryStatusPayload = {
agentId: string;
provider?: string;
embedding: {
ok: boolean;
error?: string;
};
};
export const doctorHandlers: GatewayRequestHandlers = {
"doctor.memory.status": async ({ respond }) => {
const cfg = loadConfig();
const agentId = resolveDefaultAgentId(cfg);
const { manager, error } = await getMemorySearchManager({
cfg,
agentId,
purpose: "status",
});
if (!manager) {
const payload: DoctorMemoryStatusPayload = {
agentId,
embedding: {
ok: false,
error: error ?? "memory search unavailable",
},
};
respond(true, payload, undefined);
return;
}
try {
const status = manager.status();
let embedding = await manager.probeEmbeddingAvailability();
if (!embedding.ok && !embedding.error) {
embedding = { ok: false, error: "memory embeddings unavailable" };
}
const payload: DoctorMemoryStatusPayload = {
agentId,
provider: status.provider,
embedding,
};
respond(true, payload, undefined);
} catch (err) {
const payload: DoctorMemoryStatusPayload = {
agentId,
embedding: {
ok: false,
error: `gateway memory probe failed: ${formatError(err)}`,
},
};
respond(true, payload, undefined);
} finally {
await manager.close?.().catch(() => {});
}
},
};