openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
396 lines (395 loc) • 19 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { c as isRecord, p as resolveUserPath } from "./utils-CCC-BEJH.js";
import { i as normalizeProviderId, n as findNormalizedProviderValue } from "./provider-id-Dq06Bcx6.js";
import { n as defaultSlotIdForKey } from "./slots-kpL659LX.js";
import { s as normalizePluginsConfig } from "./config-state-CikNNz7T.js";
import "./agent-scope-MrLta7Pq.js";
import { a as resolveAgentDir, c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js";
import { t as getProviderEnvVars } from "./provider-env-vars-Clp1DREb.js";
import { t as hasAnyAuthProfileStoreSource } from "./source-check-DlvZuh4h.js";
import "./auth-profiles-84rzaGag.js";
import { t as resolveEnvApiKey } from "./model-auth-env-O5hF4UJr.js";
import { d as resolveApiKeyForProvider, h as resolveUsableCustomProviderApiKey } from "./model-auth-DVfmdW0b.js";
import { i as resolveActiveMemoryBackendConfig, r as getActiveMemorySearchManager } from "./memory-runtime-DaQfwMi_.js";
import { t as resolveMemorySearchConfig } from "./memory-search-CTXTHmj4.js";
import { t as note } from "./note-BRSJp0UF.js";
import { c as repairShortTermPromotionArtifacts, n as auditDreamingArtifacts, r as auditShortTermPromotionArtifacts, s as repairDreamingArtifacts } from "./memory-core-engine-runtime-B_lbnEkT.js";
import { t as hasConfiguredMemorySecretInput } from "./secret-input-CVx0lyPz.js";
import { r as resolveQmdBinaryUnavailableReason, t as checkQmdBinaryAvailability } from "./engine-qmd-BXdBKAnj.js";
import "./legacy-config-record-shared-BGMSeWmL.js";
import { c as noteWorkspaceMemoryHealth, o as maybeRepairWorkspaceMemoryHealth } from "./doctor-workspace-Bb0DQcvv.js";
import fs from "node:fs";
//#region src/commands/doctor-memory-search.ts
const BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA = [
{
providerId: "github-copilot",
authProviderId: "github-copilot",
transport: "remote",
autoSelectPriority: 15
},
{
providerId: "openai",
authProviderId: "openai",
transport: "remote",
autoSelectPriority: 20
},
{
providerId: "gemini",
authProviderId: "google",
transport: "remote",
autoSelectPriority: 30
},
{
providerId: "voyage",
authProviderId: "voyage",
transport: "remote",
autoSelectPriority: 40
},
{
providerId: "mistral",
authProviderId: "mistral",
transport: "remote",
autoSelectPriority: 50
},
{
providerId: "bedrock",
authProviderId: "amazon-bedrock",
transport: "remote",
autoSelectPriority: 60
}
];
const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai";
const OPENAI_COMPATIBLE_MEMORY_EMBEDDING_PROVIDER = "openai-compatible";
const OPENAI_COMPATIBLE_MODEL_APIS = new Set(["openai-completions", "openai-responses"]);
function resolveMemoryEmbeddingProviderDoctorMetadata(providerId) {
const metadata = BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.find((candidate) => candidate.providerId === providerId) ?? null;
if (!metadata) return null;
return {
...metadata,
envVars: getProviderEnvVars(metadata.authProviderId)
};
}
function listAutoSelectMemoryEmbeddingProviderDoctorMetadata() {
return BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.filter((provider) => typeof provider.autoSelectPriority === "number").toSorted((a, b) => (a.autoSelectPriority ?? 0) - (b.autoSelectPriority ?? 0)).map((provider) => ({
providerId: provider.providerId,
authProviderId: provider.authProviderId,
transport: provider.transport,
autoSelectPriority: provider.autoSelectPriority,
envVars: getProviderEnvVars(provider.authProviderId)
}));
}
function resolveSuggestedRemoteMemoryProvider() {
return listAutoSelectMemoryEmbeddingProviderDoctorMetadata().find((provider) => provider.transport === "remote")?.providerId;
}
function isOpenAICompatibleMemoryProvider(providerId, cfg) {
const normalizedProviderId = normalizeProviderId(providerId);
if (normalizedProviderId === OPENAI_COMPATIBLE_MEMORY_EMBEDDING_PROVIDER) return true;
if (BUNDLED_MEMORY_EMBEDDING_PROVIDER_DOCTOR_METADATA.some((provider) => provider.providerId === normalizedProviderId)) return false;
const providerConfig = findNormalizedProviderValue(cfg.models?.providers, providerId);
if (!providerConfig) return false;
const api = normalizeProviderId(providerConfig.api ?? "");
if (api === OPENAI_COMPATIBLE_MEMORY_EMBEDDING_PROVIDER || OPENAI_COMPATIBLE_MODEL_APIS.has(api)) return true;
return !api && Boolean(normalizeOptionalString(providerConfig.baseUrl));
}
function resolveOpenAICompatibleMemoryBaseUrl(providerId, cfg, remoteBaseUrl) {
return normalizeOptionalString(remoteBaseUrl) ?? normalizeOptionalString(findNormalizedProviderValue(cfg.models?.providers, providerId)?.baseUrl);
}
function isKeyOptionalMemoryProvider(providerId, cfg) {
return providerId === "local" || providerId === "ollama" || providerId === "lmstudio" || isOpenAICompatibleMemoryProvider(providerId, cfg);
}
async function resolveRuntimeMemoryAuditContext(cfg) {
const manager = (await getActiveMemorySearchManager({
cfg,
agentId: resolveDefaultAgentId(cfg),
purpose: "status"
})).manager;
if (!manager) return null;
try {
const status = manager.status();
const customQmd = isRecord(status.custom) && isRecord(status.custom.qmd) ? status.custom.qmd : null;
return {
workspaceDir: status.workspaceDir?.trim(),
backend: status.backend,
dbPath: status.dbPath,
qmdCollections: typeof customQmd?.collections === "number" ? customQmd.collections : void 0
};
} finally {
await manager.close?.().catch(() => void 0);
}
}
function buildMemoryRecallIssueNote(audit) {
if (audit.issues.length === 0) return null;
const issueLines = audit.issues.map((issue) => `- ${issue.message}`);
const guidance = audit.issues.some((issue) => issue.fixable) ? `Fix: ${formatCliCommand("openclaw doctor --fix")} or ${formatCliCommand("openclaw memory status --fix")}` : `Verify: ${formatCliCommand("openclaw memory status --deep")}`;
return [
"Memory recall artifacts need attention:",
...issueLines,
`Recall store: ${audit.storePath}`,
guidance
].join("\n");
}
function buildDreamingArtifactIssueNote(audit) {
if (audit.issues.length === 0) return null;
const issueLines = audit.issues.map((issue) => `- ${issue.message}`);
const hasFixableIssue = audit.issues.some((issue) => issue.fixable);
return [
"Dreaming artifacts need attention:",
...issueLines,
`Dream corpus: ${audit.sessionCorpusDir}`,
hasFixableIssue ? `Fix: ${formatCliCommand("openclaw doctor --fix")} or ${formatCliCommand("openclaw memory status --fix")}` : `Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n");
}
async function noteMemoryRecallHealth(cfg) {
try {
const context = await resolveRuntimeMemoryAuditContext(cfg);
const workspaceDir = context?.workspaceDir?.trim();
if (!workspaceDir) return;
const message = buildMemoryRecallIssueNote(await auditShortTermPromotionArtifacts({
workspaceDir,
qmd: context?.backend === "qmd" ? {
dbPath: context.dbPath,
collections: context.qmdCollections
} : void 0
}));
if (message) note(message, "Memory search");
const dreamingMessage = buildDreamingArtifactIssueNote(await auditDreamingArtifacts({ workspaceDir }));
if (dreamingMessage) note(dreamingMessage, "Memory search");
} catch (err) {
note(`Memory recall audit could not be completed: ${formatErrorMessage(err)}`, "Memory search");
}
}
async function maybeRepairMemoryRecallHealth(params) {
await maybeRepairWorkspaceMemoryHealth(params);
try {
const context = await resolveRuntimeMemoryAuditContext(params.cfg);
const workspaceDir = context?.workspaceDir?.trim();
if (!workspaceDir) return;
if ((await auditShortTermPromotionArtifacts({
workspaceDir,
qmd: context?.backend === "qmd" ? {
dbPath: context.dbPath,
collections: context.qmdCollections
} : void 0
})).issues.some((issue) => issue.fixable)) {
if (await params.prompter.confirmRuntimeRepair({
message: "Normalize memory recall artifacts and remove stale promotion locks?",
initialValue: true
})) {
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
if (repair.changed) {
const removedOverflowEntries = repair.removedOverflowEntries ?? 0;
const details = [repair.removedInvalidEntries > 0 ? `-${repair.removedInvalidEntries} invalid entries` : null, removedOverflowEntries > 0 ? `-${removedOverflowEntries} overflow entries` : null].filter(Boolean).join(", ");
note([
"Memory recall artifacts repaired:",
repair.rewroteStore ? `- rewrote recall store${details ? ` (${details})` : ""}` : null,
repair.removedStaleLock ? "- removed stale promotion lock" : null,
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].filter(Boolean).join("\n"), "Doctor changes");
}
}
}
if (!(await auditDreamingArtifacts({ workspaceDir })).issues.some((issue) => issue.fixable)) return;
if (!await params.prompter.confirmRuntimeRepair({
message: "Archive contaminated dreaming artifacts and reset derived dream corpus state?",
initialValue: true
})) return;
const dreamingRepair = await repairDreamingArtifacts({ workspaceDir });
if (!dreamingRepair.changed) return;
note([
"Dreaming artifacts repaired:",
dreamingRepair.archivedSessionCorpus ? "- archived session corpus" : null,
dreamingRepair.archivedSessionIngestion ? "- archived session-ingestion state" : null,
dreamingRepair.archivedDreamsDiary ? "- archived dream diary" : null,
dreamingRepair.archiveDir ? `- archive dir: ${dreamingRepair.archiveDir}` : null,
...dreamingRepair.warnings.map((warning) => `- warning: ${warning}`),
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].filter(Boolean).join("\n"), "Doctor changes");
} catch (err) {
note(`Memory artifact repair could not be completed: ${formatErrorMessage(err)}`, "Memory search");
}
}
function hasActiveAlternateMemoryPluginSlot(cfg) {
const plugins = normalizePluginsConfig(cfg.plugins);
if (!plugins.enabled) return false;
const memorySlot = plugins.slots.memory;
if (typeof memorySlot !== "string" || memorySlot.length === 0) return false;
if (memorySlot === defaultSlotIdForKey("memory")) return false;
if (plugins.deny.includes(memorySlot)) return false;
if (!Object.hasOwn(plugins.entries, memorySlot)) return false;
const entry = plugins.entries[memorySlot];
if (!entry || entry.enabled === false) return false;
return entry.enabled === true || entry.config !== void 0;
}
/**
* Check whether memory search has a usable embedding provider.
* Runs as part of `openclaw doctor` — config-only checks where possible;
* may spawn a short-lived probe process when `memory.backend=qmd` to verify
* the configured `qmd` binary is available.
*/
async function noteMemorySearchHealth(cfg, opts) {
await noteWorkspaceMemoryHealth(cfg);
const agentId = resolveDefaultAgentId(cfg);
const agentDir = resolveAgentDir(cfg, agentId);
const resolved = resolveMemorySearchConfig(cfg, agentId);
const hasRemoteApiKey = hasConfiguredMemorySecretInput(resolved?.remote?.apiKey);
if (!resolved) {
note("Memory search is explicitly disabled (enabled: false).", "Memory search");
return;
}
const provider = resolved.provider === "auto" ? DEFAULT_MEMORY_EMBEDDING_PROVIDER : resolved.provider;
const backendConfig = resolveActiveMemoryBackendConfig({
cfg,
agentId
});
if (!backendConfig) {
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) return;
if (hasActiveAlternateMemoryPluginSlot(cfg)) return;
note("No active memory plugin is registered for the current config.", "Memory search");
return;
}
if (backendConfig.backend === "qmd") {
const qmdCheck = await checkQmdBinaryAvailability({
command: backendConfig.qmd?.command ?? "qmd",
env: process.env,
cwd: resolveAgentWorkspaceDir(cfg, agentId)
});
if (!qmdCheck.available) {
const workspaceProbeFailed = resolveQmdBinaryUnavailableReason(qmdCheck) === "workspace-cwd";
const probeError = qmdCheck.error.trim();
note([
workspaceProbeFailed ? "QMD memory backend is configured, but the agent workspace directory could not be used for the QMD startup probe." : `QMD memory backend is configured, but the qmd binary could not be started (${backendConfig.qmd?.command ?? "qmd"}).`,
probeError ? `Probe error: ${probeError}` : null,
"",
"Fix (pick one):",
workspaceProbeFailed ? "- Create the missing workspace directory or update the agent workspace path to an existing directory." : "- Install the supported QMD package: npm install -g @tobilu/qmd (or bun install -g @tobilu/qmd)",
workspaceProbeFailed ? "- Verify the resolved workspace path for the affected agent before retrying." : `- Set an explicit binary path: ${formatCliCommand("openclaw config set memory.qmd.command /absolute/path/to/qmd")}`,
`- Or switch back to builtin memory: ${formatCliCommand("openclaw config set memory.backend builtin")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].filter(Boolean).join("\n"), "Memory search");
}
return;
}
if (provider === "local") {
const suggestedRemoteProvider = resolveSuggestedRemoteMemoryProvider();
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) return;
const hasExplicitLocalModel = hasLocalEmbeddings(resolved.local);
const detail = opts?.gatewayMemoryProbe?.error?.trim();
note([
hasExplicitLocalModel ? "Memory search provider is set to \"local\" and a local model path is configured, but local embeddings are not confirmed ready." : "Memory search provider is set to \"local\", but local embeddings are not confirmed ready.",
detail ? `Gateway probe: ${detail}` : null,
"",
"Fix (pick one):",
`- Install the llama.cpp provider plugin: ${formatCliCommand("openclaw plugins install @openclaw/llama-cpp-provider")}`,
`- Set a local GGUF model path in config`,
suggestedRemoteProvider ? `- Switch to a remote provider: ${formatCliCommand(`openclaw config set agents.defaults.memorySearch.provider ${suggestedRemoteProvider}`)}` : `- Switch to a remote embedding provider in config`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].filter(Boolean).join("\n"), "Memory search");
return;
}
if (isOpenAICompatibleMemoryProvider(provider, cfg) && !resolveOpenAICompatibleMemoryBaseUrl(provider, cfg, resolved.remote?.baseUrl)) {
note([
`Memory search provider is set to "${provider}" but no OpenAI-compatible embeddings endpoint was configured.`,
"Set agents.defaults.memorySearch.remote.baseUrl to the /v1 endpoint for your embeddings server.",
"",
"Fix:",
`- ${formatCliCommand("openclaw config set agents.defaults.memorySearch.remote.baseUrl http://127.0.0.1:1234/v1")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
return;
}
if (isOpenAICompatibleMemoryProvider(provider, cfg) && !normalizeOptionalString(resolved.model)) {
note([
`Memory search provider is set to "${provider}" but no OpenAI-compatible embedding model was configured.`,
"Set agents.defaults.memorySearch.model to the embedding model id your server expects.",
"",
"Fix:",
`- ${formatCliCommand("openclaw config set agents.defaults.memorySearch.model text-embedding-bge-m3")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
return;
}
if (isKeyOptionalMemoryProvider(provider, cfg)) {
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) return;
if (opts?.gatewayMemoryProbe?.skipped) return;
const gatewayProbeWarning = buildGatewayProbeWarning(opts?.gatewayMemoryProbe);
note([
gatewayProbeWarning ? `Memory search provider "${provider}" is configured, but the gateway reports embeddings are not ready.` : `Memory search provider "${provider}" is configured, but the gateway could not confirm embeddings are ready.`,
gatewayProbeWarning,
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].filter(Boolean).join("\n"), "Memory search");
return;
}
if (hasRemoteApiKey || await hasApiKeyForProvider(provider, cfg, agentDir)) return;
if (opts?.gatewayMemoryProbe?.checked && opts.gatewayMemoryProbe.ready) {
note([
`Memory search provider is set to "${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 = resolvePrimaryMemoryProviderEnvVar(provider);
note([
`Memory search provider is set to "${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`,
`- Configure credentials: ${formatCliCommand("openclaw configure --section model")}`,
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
"",
`Verify: ${formatCliCommand("openclaw memory status --deep")}`
].join("\n"), "Memory search");
}
/**
* Check whether local embeddings are available.
*
*/
function hasLocalEmbeddings(local) {
const modelPath = normalizeOptionalString(local.modelPath);
if (!modelPath) return false;
if (/^(hf:|https?:)/i.test(modelPath)) return true;
const resolved = resolveUserPath(modelPath);
try {
return fs.statSync(resolved).isFile();
} catch {
return false;
}
}
async function hasApiKeyForProvider(provider, cfg, agentDir) {
const authProviderId = resolveMemoryEmbeddingProviderDoctorMetadata(provider)?.authProviderId ?? provider;
if (resolveEnvApiKey(authProviderId) || resolveUsableCustomProviderApiKey({
cfg,
provider: authProviderId
})) return true;
if (authProviderId !== "amazon-bedrock" && !hasAnyAuthProfileStoreSource(agentDir)) return false;
try {
await resolveApiKeyForProvider({
provider: authProviderId,
cfg,
agentDir
});
return true;
} catch {
return false;
}
}
function resolvePrimaryMemoryProviderEnvVar(provider) {
if (provider === "openai") return "OPENAI_API_KEY";
return resolveMemoryEmbeddingProviderDoctorMetadata(provider)?.envVars[0] ?? `${provider.toUpperCase()}_API_KEY`;
}
function buildGatewayProbeWarning(probe) {
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.";
}
//#endregion
export { maybeRepairMemoryRecallHealth, noteMemoryRecallHealth, noteMemorySearchHealth };