mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:07:39 +00:00
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:
committed by
GitHub
parent
bf373eeb43
commit
8d69251475
@@ -1,11 +1,18 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import type { DoctorMemoryStatusPayload } from "../gateway/server-methods/doctor.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
|
||||
export type GatewayMemoryProbe = {
|
||||
checked: boolean;
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function checkGatewayHealth(params: {
|
||||
runtime: RuntimeEnv;
|
||||
cfg: OpenClawConfig;
|
||||
@@ -56,3 +63,30 @@ export async function checkGatewayHealth(params: {
|
||||
|
||||
return { healthOk };
|
||||
}
|
||||
|
||||
export async function probeGatewayMemoryStatus(params: {
|
||||
cfg: OpenClawConfig;
|
||||
timeoutMs?: number;
|
||||
}): Promise<GatewayMemoryProbe> {
|
||||
const timeoutMs =
|
||||
typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 8_000;
|
||||
try {
|
||||
const payload = await callGateway<DoctorMemoryStatusPayload>({
|
||||
method: "doctor.memory.status",
|
||||
timeoutMs,
|
||||
config: params.cfg,
|
||||
});
|
||||
return {
|
||||
checked: true,
|
||||
ready: payload.embedding.ok,
|
||||
error: payload.embedding.error,
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
checked: true,
|
||||
ready: false,
|
||||
error: `gateway memory probe unavailable: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("noteMemorySearchHealth", () => {
|
||||
remote: { apiKey: "from-config" },
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg);
|
||||
await noteMemorySearchHealth(cfg, {});
|
||||
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
expect(resolveApiKeyForProvider).not.toHaveBeenCalled();
|
||||
@@ -53,9 +53,10 @@ describe("noteMemorySearchHealth", () => {
|
||||
note.mockClear();
|
||||
resolveDefaultAgentId.mockClear();
|
||||
resolveAgentDir.mockClear();
|
||||
resolveMemorySearchConfig.mockClear();
|
||||
resolveApiKeyForProvider.mockClear();
|
||||
resolveMemoryBackendConfig.mockClear();
|
||||
resolveMemorySearchConfig.mockReset();
|
||||
resolveApiKeyForProvider.mockReset();
|
||||
resolveApiKeyForProvider.mockRejectedValue(new Error("missing key"));
|
||||
resolveMemoryBackendConfig.mockReset();
|
||||
resolveMemoryBackendConfig.mockReturnValue({ backend: "builtin", citations: "auto" });
|
||||
});
|
||||
|
||||
@@ -70,7 +71,7 @@ describe("noteMemorySearchHealth", () => {
|
||||
remote: {},
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg);
|
||||
await noteMemorySearchHealth(cfg, {});
|
||||
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -95,7 +96,7 @@ describe("noteMemorySearchHealth", () => {
|
||||
mode: "api-key",
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg);
|
||||
await noteMemorySearchHealth(cfg, {});
|
||||
|
||||
expect(resolveApiKeyForProvider).toHaveBeenCalledWith({
|
||||
provider: "google",
|
||||
@@ -126,6 +127,42 @@ describe("noteMemorySearchHealth", () => {
|
||||
});
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("notes when gateway probe reports embeddings ready and CLI API key is missing", async () => {
|
||||
resolveMemorySearchConfig.mockReturnValue({
|
||||
provider: "gemini",
|
||||
local: {},
|
||||
remote: {},
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg, {
|
||||
gatewayMemoryProbe: { checked: true, ready: true },
|
||||
});
|
||||
|
||||
const message = note.mock.calls[0]?.[0] as string;
|
||||
expect(message).toContain("reports memory embeddings are ready");
|
||||
});
|
||||
|
||||
it("uses configure hint when gateway probe is unavailable and API key is missing", async () => {
|
||||
resolveMemorySearchConfig.mockReturnValue({
|
||||
provider: "gemini",
|
||||
local: {},
|
||||
remote: {},
|
||||
});
|
||||
|
||||
await noteMemorySearchHealth(cfg, {
|
||||
gatewayMemoryProbe: {
|
||||
checked: true,
|
||||
ready: false,
|
||||
error: "gateway memory probe unavailable: timeout",
|
||||
},
|
||||
});
|
||||
|
||||
const message = note.mock.calls[0]?.[0] as string;
|
||||
expect(message).toContain("Gateway memory probe for default agent is not ready");
|
||||
expect(message).toContain("openclaw configure");
|
||||
expect(message).not.toContain("auth add");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectLegacyWorkspaceDirs", () => {
|
||||
|
||||
@@ -12,7 +12,16 @@ import { resolveUserPath } from "../utils.js";
|
||||
* Check whether memory search has a usable embedding provider.
|
||||
* Runs as part of `openclaw doctor` — config-only, no network calls.
|
||||
*/
|
||||
export async function noteMemorySearchHealth(cfg: OpenClawConfig): Promise<void> {
|
||||
export async function noteMemorySearchHealth(
|
||||
cfg: OpenClawConfig,
|
||||
opts?: {
|
||||
gatewayMemoryProbe?: {
|
||||
checked: boolean;
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
};
|
||||
},
|
||||
): Promise<void> {
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
const resolved = resolveMemorySearchConfig(cfg, agentId);
|
||||
@@ -54,15 +63,28 @@ export async function noteMemorySearchHealth(cfg: OpenClawConfig): Promise<void>
|
||||
if (hasRemoteApiKey || (await hasApiKeyForProvider(resolved.provider, cfg, agentDir))) {
|
||||
return;
|
||||
}
|
||||
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) {
|
||||
note(
|
||||
[
|
||||
`Memory search provider is set to "${resolved.provider}" but the API key was not found in the CLI environment.`,
|
||||
"The running gateway reports memory embeddings are ready for the default agent.",
|
||||
`Verify: ${formatCliCommand("openclaw memory status --deep")}`,
|
||||
].join("\n"),
|
||||
"Memory search",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const gatewayProbeWarning = buildGatewayProbeWarning(opts?.gatewayMemoryProbe);
|
||||
const envVar = providerEnvVar(resolved.provider);
|
||||
note(
|
||||
[
|
||||
`Memory search provider is set to "${resolved.provider}" but no API key was found.`,
|
||||
`Semantic recall will not work without a valid API key.`,
|
||||
gatewayProbeWarning ? gatewayProbeWarning : null,
|
||||
"",
|
||||
"Fix (pick one):",
|
||||
`- Set ${envVar} in your environment`,
|
||||
`- Add credentials: ${formatCliCommand(`openclaw auth add --provider ${resolved.provider}`)}`,
|
||||
`- Configure credentials: ${formatCliCommand("openclaw configure")}`,
|
||||
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
|
||||
"",
|
||||
`Verify: ${formatCliCommand("openclaw memory status --deep")}`,
|
||||
@@ -82,14 +104,28 @@ export async function noteMemorySearchHealth(cfg: OpenClawConfig): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) {
|
||||
note(
|
||||
[
|
||||
'Memory search provider is set to "auto" but the API key was not found in the CLI environment.',
|
||||
"The running gateway reports memory embeddings are ready for the default agent.",
|
||||
`Verify: ${formatCliCommand("openclaw memory status --deep")}`,
|
||||
].join("\n"),
|
||||
"Memory search",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const gatewayProbeWarning = buildGatewayProbeWarning(opts?.gatewayMemoryProbe);
|
||||
|
||||
note(
|
||||
[
|
||||
"Memory search is enabled but no embedding provider is configured.",
|
||||
"Semantic recall will not work without an embedding provider.",
|
||||
gatewayProbeWarning ? gatewayProbeWarning : null,
|
||||
"",
|
||||
"Fix (pick one):",
|
||||
"- Set OPENAI_API_KEY, GEMINI_API_KEY, VOYAGE_API_KEY, or MISTRAL_API_KEY in your environment",
|
||||
`- Add credentials: ${formatCliCommand("openclaw auth add --provider openai")}`,
|
||||
`- Configure credentials: ${formatCliCommand("openclaw configure")}`,
|
||||
`- For local embeddings: configure agents.defaults.memorySearch.provider and local model path`,
|
||||
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
|
||||
"",
|
||||
@@ -145,3 +181,21 @@ function providerEnvVar(provider: string): string {
|
||||
return `${provider.toUpperCase()}_API_KEY`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildGatewayProbeWarning(
|
||||
probe:
|
||||
| {
|
||||
checked: boolean;
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
}
|
||||
| undefined,
|
||||
): string | null {
|
||||
if (!probe?.checked || probe.ready) {
|
||||
return null;
|
||||
}
|
||||
const detail = probe.error?.trim();
|
||||
return detail
|
||||
? `Gateway memory probe for default agent is not ready: ${detail}`
|
||||
: "Gateway memory probe for default agent is not ready.";
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ vi.mock("./doctor-gateway-daemon-flow.js", () => ({
|
||||
|
||||
vi.mock("./doctor-gateway-health.js", () => ({
|
||||
checkGatewayHealth: vi.fn().mockResolvedValue({ healthOk: false }),
|
||||
probeGatewayMemoryStatus: vi.fn().mockResolvedValue({ checked: false, ready: false }),
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-memory-search.js", () => ({
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { doctorShellCompletion } from "./doctor-completion.js";
|
||||
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
|
||||
import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js";
|
||||
import { checkGatewayHealth } from "./doctor-gateway-health.js";
|
||||
import { checkGatewayHealth, probeGatewayMemoryStatus } from "./doctor-gateway-health.js";
|
||||
import {
|
||||
maybeRepairGatewayServiceConfig,
|
||||
maybeScanExtraGatewayServices,
|
||||
@@ -264,7 +264,6 @@ export async function doctorCommand(
|
||||
}
|
||||
|
||||
noteWorkspaceStatus(cfg);
|
||||
await noteMemorySearchHealth(cfg);
|
||||
|
||||
// Check and fix shell completion
|
||||
await doctorShellCompletion(runtime, prompter, {
|
||||
@@ -276,6 +275,13 @@ export async function doctorCommand(
|
||||
cfg,
|
||||
timeoutMs: options.nonInteractive === true ? 3000 : 10_000,
|
||||
});
|
||||
const gatewayMemoryProbe = healthOk
|
||||
? await probeGatewayMemoryStatus({
|
||||
cfg,
|
||||
timeoutMs: options.nonInteractive === true ? 3000 : 10_000,
|
||||
})
|
||||
: { checked: false, ready: false };
|
||||
await noteMemorySearchHealth(cfg, { gatewayMemoryProbe });
|
||||
await maybeRepairGatewayDaemon({
|
||||
cfg,
|
||||
runtime,
|
||||
|
||||
Reference in New Issue
Block a user