From a7d2d9c6df002d718badc5500cf52f516d97d248 Mon Sep 17 00:00:00 2001 From: Mariano Belinky Date: Wed, 27 May 2026 15:40:42 +0200 Subject: [PATCH] fix: migrate legacy memory auto provider --- docs/gateway/doctor.md | 2 +- .../shared/legacy-config-migrate.test.ts | 69 ++++++++++++++++++ ...legacy-config-migrations.runtime.agents.ts | 70 +++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 43b5a80d74e..66e9b173b97 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -515,7 +515,7 @@ That stages grounded durable candidates into the short-term dreaming store while - **QMD backend**: probes whether the `qmd` binary is available and startable. If not, prints fix guidance including the npm package and a manual binary path option. - **Explicit local provider**: checks for a local model file or a recognized remote/downloadable model URL. If missing, suggests switching to a remote provider. - **Explicit remote provider** (`openai`, `voyage`, etc.): verifies an API key is present in the environment or auth store. Prints actionable fix hints if missing. - - **Legacy auto provider**: treats `memorySearch.provider: "auto"` as OpenAI and checks OpenAI readiness. + - **Legacy auto provider**: treats `memorySearch.provider: "auto"` as OpenAI, checks OpenAI readiness, and `doctor --fix` rewrites it to `provider: "openai"`. When a cached gateway probe result is available (gateway was healthy at the time of the check), doctor cross-references its result with the CLI-visible config and notes any discrepancy. Doctor does not start a fresh embedding ping on the default path; use the deep memory status command when you want a live provider check. diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index c6d77c422f2..172ba70c6da 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -62,6 +62,75 @@ describe("compatibility binding repair migrate", () => { }); }); +describe("legacy memory search config migrate", () => { + it("rewrites top-level legacy auto provider after moving memorySearch into agent defaults", () => { + const raw = { + memorySearch: { + provider: "auto", + model: "text-embedding-3-small", + }, + }; + + expect(findLegacyConfigIssues(raw).map((issue) => issue.path)).toEqual([ + "memorySearch", + "memorySearch.provider", + ]); + + const res = migrateLegacyConfigForTest(raw); + + expect(res.config?.agents?.defaults?.memorySearch).toEqual({ + provider: "openai", + model: "text-embedding-3-small", + }); + expect(res.config).not.toHaveProperty("memorySearch"); + expect(res.changes).toEqual([ + "Moved memorySearch → agents.defaults.memorySearch.", + 'Moved agents.defaults.memorySearch.provider from legacy "auto" to "openai".', + ]); + }); + + it("rewrites default and per-agent legacy auto memory providers", () => { + const raw = { + agents: { + defaults: { + memorySearch: { + provider: "auto", + }, + }, + list: [ + { + id: "local", + memorySearch: { + provider: " auto ", + }, + }, + { + id: "custom", + memorySearch: { + provider: "openai-compatible", + }, + }, + ], + }, + }; + + expect(findLegacyConfigIssues(raw).map((issue) => issue.path)).toEqual([ + "agents.defaults.memorySearch.provider", + "agents.list", + ]); + + const res = migrateLegacyConfigForTest(raw); + + expect(res.config?.agents?.defaults?.memorySearch?.provider).toBe("openai"); + expect(res.config?.agents?.list?.[0]?.memorySearch?.provider).toBe("openai"); + expect(res.config?.agents?.list?.[1]?.memorySearch?.provider).toBe("openai-compatible"); + expect(res.changes).toEqual([ + 'Moved agents.defaults.memorySearch.provider from legacy "auto" to "openai".', + 'Moved agents.list.0.memorySearch.provider from legacy "auto" to "openai".', + ]); + }); +}); + describe("legacy silent reply config migrate", () => { it("removes silent reply rewrite and direct-chat silent reply config", () => { const res = migrateLegacyConfigForTest({ diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts index 89eb568f86c..2976fef2163 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts @@ -45,6 +45,27 @@ const MEMORY_SEARCH_RULE: LegacyConfigRule = { 'top-level memorySearch was moved; use agents.defaults.memorySearch instead. Run "openclaw doctor --fix".', }; +const LEGACY_MEMORY_SEARCH_AUTO_PROVIDER_RULES: LegacyConfigRule[] = [ + { + path: ["memorySearch", "provider"], + message: + 'memorySearch.provider = "auto" is legacy; use "openai" explicitly. Run "openclaw doctor --fix".', + match: isLegacyMemorySearchAutoProvider, + }, + { + path: ["agents", "defaults", "memorySearch", "provider"], + message: + 'agents.defaults.memorySearch.provider = "auto" is legacy; use "openai" explicitly. Run "openclaw doctor --fix".', + match: isLegacyMemorySearchAutoProvider, + }, + { + path: ["agents", "list"], + message: + 'agents.list[].memorySearch.provider = "auto" is legacy; use "openai" explicitly. Run "openclaw doctor --fix".', + match: hasAgentListLegacyMemorySearchAutoProvider, + }, +]; + const HEARTBEAT_RULE: LegacyConfigRule = { path: ["heartbeat"], message: @@ -335,6 +356,31 @@ function migrateLegacyEmbeddedAgentKey( delete container.embeddedPi; } +function isLegacyMemorySearchAutoProvider(value: unknown): boolean { + return typeof value === "string" && value.trim().toLowerCase() === "auto"; +} + +function hasAgentListLegacyMemorySearchAutoProvider(value: unknown): boolean { + if (!Array.isArray(value)) { + return false; + } + return value.some((agent) => + isLegacyMemorySearchAutoProvider(getRecord(getRecord(agent)?.memorySearch)?.provider), + ); +} + +function rewriteLegacyMemorySearchAutoProvider( + memorySearch: Record | null, + pathLabel: string, + changes: string[], +): void { + if (!memorySearch || !isLegacyMemorySearchAutoProvider(memorySearch.provider)) { + return; + } + memorySearch.provider = "openai"; + changes.push(`Moved ${pathLabel}.provider from legacy "auto" to "openai".`); +} + function migrateLegacySandboxPerSession( sandbox: Record, pathLabel: string, @@ -1245,6 +1291,30 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_AGENTS: LegacyConfigMigrationSpec[ delete raw.memorySearch; }, }), + defineLegacyConfigMigration({ + id: "memorySearch.provider-auto->openai", + describe: 'Rewrite legacy memorySearch provider "auto" to "openai"', + legacyRules: LEGACY_MEMORY_SEARCH_AUTO_PROVIDER_RULES, + apply: (raw, changes) => { + const agents = getRecord(raw.agents); + rewriteLegacyMemorySearchAutoProvider( + getRecord(getRecord(agents?.defaults)?.memorySearch), + "agents.defaults.memorySearch", + changes, + ); + + if (!Array.isArray(agents?.list)) { + return; + } + for (const [index, agent] of agents.list.entries()) { + rewriteLegacyMemorySearchAutoProvider( + getRecord(getRecord(agent)?.memorySearch), + `agents.list.${index}.memorySearch`, + changes, + ); + } + }, + }), defineLegacyConfigMigration({ id: "heartbeat->agents.defaults.heartbeat", describe: "Move top-level heartbeat to agents.defaults.heartbeat/channels.defaults.heartbeat",