diff --git a/src/config/schema-base.ts b/src/config/schema-base.ts index eb3bdb87622..1aa1d72a641 100644 --- a/src/config/schema-base.ts +++ b/src/config/schema-base.ts @@ -19,6 +19,8 @@ export type BaseConfigSchemaResponse = { generatedAt: string; }; +type BaseConfigSchemaStablePayload = Omit; + function cloneSchema(value: T): T { if (typeof structuredClone === "function") { return structuredClone(value); @@ -54,19 +56,42 @@ function stripChannelSchema(schema: ConfigSchema): ConfigSchema { return next; } -export function computeBaseConfigSchemaResponse(params?: { - generatedAt?: string; -}): BaseConfigSchemaResponse { +let baseConfigSchemaStablePayload: BaseConfigSchemaStablePayload | null = null; + +function computeBaseConfigSchemaStablePayload(): BaseConfigSchemaStablePayload { + if (baseConfigSchemaStablePayload) { + return { + schema: cloneSchema(baseConfigSchemaStablePayload.schema), + uiHints: cloneSchema(baseConfigSchemaStablePayload.uiHints), + version: baseConfigSchemaStablePayload.version, + }; + } const schema = OpenClawSchema.toJSONSchema({ target: "draft-07", unrepresentable: "any", }); schema.title = "OpenClawConfig"; - const hints = applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints())); - return { + const stablePayload = { schema: stripChannelSchema(schema), - uiHints: hints, + uiHints: applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints())), version: VERSION, + } satisfies BaseConfigSchemaStablePayload; + baseConfigSchemaStablePayload = stablePayload; + return { + schema: cloneSchema(stablePayload.schema), + uiHints: cloneSchema(stablePayload.uiHints), + version: stablePayload.version, + }; +} + +export function computeBaseConfigSchemaResponse(params?: { + generatedAt?: string; +}): BaseConfigSchemaResponse { + const stablePayload = computeBaseConfigSchemaStablePayload(); + return { + schema: stablePayload.schema, + uiHints: stablePayload.uiHints, + version: stablePayload.version, generatedAt: params?.generatedAt ?? new Date().toISOString(), }; } diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index 950346bd3e4..1b5b939aea4 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -957,6 +957,7 @@ describe("memory index", () => { const result = await getMemorySearchManager({ cfg, agentId: "main" }); const manager = requireManager(result); + await manager.probeEmbeddingAvailability(); expect( providerCalls.some( diff --git a/src/memory/manager.get-concurrency.test.ts b/src/memory/manager.get-concurrency.test.ts index 5f2fa417f7d..1e8a09d0e06 100644 --- a/src/memory/manager.get-concurrency.test.ts +++ b/src/memory/manager.get-concurrency.test.ts @@ -93,17 +93,15 @@ describe("memory manager cache hydration", () => { expect(managers).toHaveLength(12); expect(new Set(managers).size).toBe(1); - expect(hoisted.providerCreateCalls).toBe(1); + expect(hoisted.providerCreateCalls).toBe(0); await managers[0].close(); }); - it("drains in-flight manager creation during global teardown", async () => { + it("evicts cached managers during global teardown", async () => { const indexPath = path.join(workspaceDir, "index.sqlite"); const cfg = createMemoryConcurrencyConfig(indexPath); - hoisted.providerDelayMs = 100; - const pendingResult = RawMemoryIndexManager.get({ cfg, agentId: "main" }); await closeAllMemoryIndexManagers(); const firstManager = await pendingResult; @@ -113,7 +111,7 @@ describe("memory manager cache hydration", () => { expect(firstManager).toBeTruthy(); expect(secondManager).toBeTruthy(); expect(Object.is(secondManager, firstManager)).toBe(false); - expect(hoisted.providerCreateCalls).toBe(2); + expect(hoisted.providerCreateCalls).toBe(0); await secondManager?.close?.(); }); diff --git a/src/memory/manager.ts b/src/memory/manager.ts index 5d14c7b968c..fe8622f6ae3 100644 --- a/src/memory/manager.ts +++ b/src/memory/manager.ts @@ -193,11 +193,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem return pending; } const createPromise = (async () => { - const providerResult = await MemoryIndexManager.loadProviderResult({ - cfg, - agentId, - settings, - }); const refreshed = INDEX_CACHE.get(key); if (refreshed) { return refreshed; @@ -208,7 +203,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem agentId, workspaceDir, settings, - providerResult, purpose: params.purpose, }); INDEX_CACHE.set(key, manager); @@ -334,17 +328,21 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem sessionKey?: string; }, ): Promise { - await this.ensureProviderInitialized(); + const cleaned = query.trim(); + if (!cleaned) { + return []; + } void this.warmSession(opts?.sessionKey); if (this.settings.sync.onSearch && (this.dirty || this.sessionsDirty)) { void this.sync({ reason: "search" }).catch((err) => { log.warn(`memory sync failed (search): ${String(err)}`); }); } - const cleaned = query.trim(); - if (!cleaned) { + const hasIndexedContent = this.hasIndexedContent(); + if (!hasIndexedContent) { return []; } + await this.ensureProviderInitialized(); const minScore = opts?.minScore ?? this.settings.query.minScore; const maxResults = opts?.maxResults ?? this.settings.query.maxResults; const hybrid = this.settings.query.hybrid; @@ -437,6 +435,32 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem .slice(0, maxResults); } + private hasIndexedContent(): boolean { + const chunks = + ( + this.db.prepare(`SELECT COUNT(*) as c FROM chunks`).get() as + | { + c: number; + } + | undefined + )?.c ?? 0; + if (chunks > 0) { + return true; + } + if (!this.fts.enabled || !this.fts.available) { + return false; + } + const ftsRows = + ( + this.db.prepare(`SELECT COUNT(*) as c FROM ${FTS_TABLE}`).get() as + | { + c: number; + } + | undefined + )?.c ?? 0; + return ftsRows > 0; + } + private async searchVector( queryVec: number[], limit: number,