diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index a5d95e3a..3a5e19d2 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -29,6 +29,7 @@ import { } from './provider-keys'; import { normalizePiAiModelCost, type PiAiModelCostRates } from '../shared/pi-ai-model-cost'; import { withConfigLock } from './config-mutex'; +import { ensureMemorySearchDisabledDefault, hasUserMemorySearchConfig } from './openclaw-memory-search'; import { PORTS } from './config'; import { getSetting } from './store'; import { @@ -2675,6 +2676,19 @@ export async function batchSyncConfigFields(token: string): Promise { console.log('[batch-sync] Seeded agents.defaults.compaction.mode=safeguard'); } + // ── Memory search default ── + // OpenClaw defaults to the openai embedding provider; without a key that + // yields doctor errors and a broken memory_search tool. Seed enabled=false + // only when the user has no memorySearch config anywhere AND no OpenAI key + // (i.e. the default embedding model is unusable). Existing user config is + // never modified. + if (!hasUserMemorySearchConfig(config) + && !(await getProviderApiKeyFromOpenClaw('openai')) + && ensureMemorySearchDisabledDefault(config)) { + modified = true; + console.log('[batch-sync] Seeded agents.defaults.memorySearch.enabled=false (no embedding provider configured)'); + } + // ── Custom provider contextWindow backfill ── const backfilledContextWindows = backfillCustomProviderModelContextWindows(config); if (backfilledContextWindows.length > 0) { diff --git a/electron/utils/openclaw-memory-search.ts b/electron/utils/openclaw-memory-search.ts new file mode 100644 index 00000000..793145c5 --- /dev/null +++ b/electron/utils/openclaw-memory-search.ts @@ -0,0 +1,46 @@ +/** + * Memory search default seeding for openclaw.json. + * + * OpenClaw enables semantic memory search by default with the `openai` + * embedding provider, so a user without an OpenAI key gets doctor errors and + * a broken memory_search tool. ClawX seeds `agents.defaults.memorySearch = + * { enabled: false }` at Gateway prelaunch — but only when the user has no + * memorySearch config anywhere (global defaults or per-agent overrides). + * Existing user config is never modified. + */ + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +/** + * True when the user manages memorySearch themselves: either + * `agents.defaults.memorySearch` or any `agents.list[].memorySearch` exists. + */ +export function hasUserMemorySearchConfig(config: Record): boolean { + const agents = isRecord(config.agents) ? config.agents : undefined; + if (!agents) return false; + + const defaults = isRecord(agents.defaults) ? agents.defaults : undefined; + if (defaults && defaults.memorySearch !== undefined) return true; + + const list = Array.isArray(agents.list) ? agents.list : []; + return list.some((entry) => isRecord(entry) && entry.memorySearch !== undefined); +} + +/** + * Seed `agents.defaults.memorySearch = { enabled: false }` when the user has + * no memorySearch config at all. Mutates `config` in place and returns true + * when a change was made. Never touches existing memorySearch objects. + */ +export function ensureMemorySearchDisabledDefault(config: Record): boolean { + if (hasUserMemorySearchConfig(config)) return false; + + const agents = (isRecord(config.agents) ? config.agents : {}) as Record; + const defaults = (isRecord(agents.defaults) ? agents.defaults : {}) as Record; + + defaults.memorySearch = { enabled: false }; + agents.defaults = defaults; + config.agents = agents; + return true; +} diff --git a/tests/unit/openclaw-memory-search.test.ts b/tests/unit/openclaw-memory-search.test.ts new file mode 100644 index 00000000..46fb04fa --- /dev/null +++ b/tests/unit/openclaw-memory-search.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { + ensureMemorySearchDisabledDefault, + hasUserMemorySearchConfig, +} from '@electron/utils/openclaw-memory-search'; + +describe('openclaw-memory-search', () => { + it('seeds memorySearch.enabled=false when no memorySearch config exists', () => { + const config: Record = { + agents: { defaults: { model: { primary: 'custom-customfc/gpt-5.5' } } }, + }; + expect(ensureMemorySearchDisabledDefault(config)).toBe(true); + expect(config).toEqual({ + agents: { + defaults: { + model: { primary: 'custom-customfc/gpt-5.5' }, + memorySearch: { enabled: false }, + }, + }, + }); + }); + + it('seeds on a completely empty config', () => { + const config: Record = {}; + expect(ensureMemorySearchDisabledDefault(config)).toBe(true); + expect(config).toEqual({ + agents: { defaults: { memorySearch: { enabled: false } } }, + }); + }); + + it('never touches existing defaults.memorySearch config', () => { + const config: Record = { + agents: { + defaults: { + memorySearch: { + provider: 'custom-customfc', + model: 'text-embedding-3-small', + fallback: 'none', + remote: { baseUrl: 'https://taolat.com/v1' }, + }, + }, + }, + }; + const before = JSON.parse(JSON.stringify(config)); + expect(ensureMemorySearchDisabledDefault(config)).toBe(false); + expect(config).toEqual(before); + }); + + it('never seeds when a per-agent memorySearch override exists', () => { + const config: Record = { + agents: { + defaults: { model: { primary: 'openai/gpt-4o' } }, + list: [ + { id: 'main' }, + { id: 'research', memorySearch: { provider: 'openai' } }, + ], + }, + }; + const before = JSON.parse(JSON.stringify(config)); + expect(ensureMemorySearchDisabledDefault(config)).toBe(false); + expect(config).toEqual(before); + }); + + it('treats explicit enabled=true as user config', () => { + const config: Record = { + agents: { defaults: { memorySearch: { enabled: true } } }, + }; + expect(hasUserMemorySearchConfig(config)).toBe(true); + expect(ensureMemorySearchDisabledDefault(config)).toBe(false); + expect((config.agents as { defaults: { memorySearch: { enabled: boolean } } }).defaults.memorySearch.enabled).toBe(true); + }); + + it('treats an empty memorySearch object as user config', () => { + const config: Record = { + agents: { defaults: { memorySearch: {} } }, + }; + expect(hasUserMemorySearchConfig(config)).toBe(true); + expect(ensureMemorySearchDisabledDefault(config)).toBe(false); + }); +});