From 4d7c4b3298920c0e6e2c5cc8bd7a1546e8122abe Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 23 Apr 2026 21:21:04 -0700 Subject: [PATCH] fix(memory-core): harden singleton cache recovery (#70925) * fix memory cache singleton hardening * refactor(memory-core): simplify singleton cache repair Co-authored-by: BirdSong <88885494+fire-mirror@users.noreply.github.com> --------- Co-authored-by: BirdSong <88885494+fire-mirror@users.noreply.github.com> --- .../src/memory/manager-cache.test.ts | 19 ++++++++++++++ .../memory-core/src/memory/manager-cache.ts | 16 +++++++++++- .../src/memory/search-manager.test.ts | 19 ++++++++++++++ .../memory-core/src/memory/search-manager.ts | 25 +++++++++++++++---- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/extensions/memory-core/src/memory/manager-cache.test.ts b/extensions/memory-core/src/memory/manager-cache.test.ts index 435b4e9b366..af28895712d 100644 --- a/extensions/memory-core/src/memory/manager-cache.test.ts +++ b/extensions/memory-core/src/memory/manager-cache.test.ts @@ -46,6 +46,25 @@ describe("manager cache", () => { ); }); + it("repairs an invalid singleton cache shape", async () => { + const cacheKey = Symbol("openclaw.manager-cache.corrupt-test"); + (globalThis as Record)[cacheKey] = {}; + + const cache = resolveSingletonManagedCache(cacheKey); + cachesForCleanup.push(cache); + const entry = await getOrCreateManagedCacheEntry({ + cache: cache.cache, + pending: cache.pending, + key: "same", + create: async () => createEntry("repaired"), + }); + + expect(entry.id).toBe("repaired"); + expect(cache.cache).toBeInstanceOf(Map); + expect(cache.pending).toBeInstanceOf(Map); + delete (globalThis as Record)[cacheKey]; + }); + it("deduplicates concurrent creation for the same cache key", async () => { const cache = createTestCache(); cachesForCleanup.push(cache); diff --git a/extensions/memory-core/src/memory/manager-cache.ts b/extensions/memory-core/src/memory/manager-cache.ts index ed0675b461f..07f76531f2d 100644 --- a/extensions/memory-core/src/memory/manager-cache.ts +++ b/extensions/memory-core/src/memory/manager-cache.ts @@ -10,10 +10,24 @@ export type ManagedCache = { }; export function resolveSingletonManagedCache(cacheKey: symbol): ManagedCache { - return resolveGlobalSingleton>(cacheKey, () => ({ + const resolved = resolveGlobalSingleton(cacheKey, () => ({ cache: new Map(), pending: new Map>(), })); + if ( + typeof resolved === "object" && + resolved !== null && + (resolved as Partial>).cache instanceof Map && + (resolved as Partial>).pending instanceof Map + ) { + return resolved as ManagedCache; + } + const repaired: ManagedCache = { + cache: new Map(), + pending: new Map>(), + }; + (globalThis as Record)[cacheKey] = repaired; + return repaired; } export async function getOrCreateManagedCacheEntry(params: { diff --git a/extensions/memory-core/src/memory/search-manager.test.ts b/extensions/memory-core/src/memory/search-manager.test.ts index 034cce1a3f2..d71c87de932 100644 --- a/extensions/memory-core/src/memory/search-manager.test.ts +++ b/extensions/memory-core/src/memory/search-manager.test.ts @@ -214,6 +214,25 @@ beforeEach(async () => { }); describe("getMemorySearchManager caching", () => { + it("repairs an invalid shared singleton cache shape before using qmd cache maps", async () => { + await closeAllMemorySearchManagers(); + vi.resetModules(); + const cacheKey = Symbol.for("openclaw.memorySearchManagerCache"); + (globalThis as Record)[cacheKey] = {}; + + const freshModule = await import("./search-manager.js"); + try { + const result = await freshModule.getMemorySearchManager({ + cfg: createQmdCfg("corrupt-cache-agent"), + agentId: "corrupt-cache-agent", + }); + requireManager(result); + } finally { + await freshModule.closeAllMemorySearchManagers(); + delete (globalThis as Record)[cacheKey]; + } + }); + it("reuses the same QMD manager instance for repeated calls", async () => { const cfg = createQmdCfg("main"); diff --git a/extensions/memory-core/src/memory/search-manager.ts b/extensions/memory-core/src/memory/search-manager.ts index 365d7c847dd..a5ae613e329 100644 --- a/extensions/memory-core/src/memory/search-manager.ts +++ b/extensions/memory-core/src/memory/search-manager.ts @@ -42,15 +42,30 @@ type MemorySearchManagerCacheStore = { pendingQmdManagerCreates: Map; }; +function createMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore { + return { + qmdManagerCache: new Map(), + pendingQmdManagerCreates: new Map(), + }; +} + function getMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore { // Keep caches reachable across `vi.resetModules()` so later cleanup can close older instances. - return resolveGlobalSingleton( + const resolved = resolveGlobalSingleton( MEMORY_SEARCH_MANAGER_CACHE_KEY, - () => ({ - qmdManagerCache: new Map(), - pendingQmdManagerCreates: new Map(), - }), + createMemorySearchManagerCacheStore, ); + if ( + typeof resolved === "object" && + resolved !== null && + (resolved as Partial).qmdManagerCache instanceof Map && + (resolved as Partial).pendingQmdManagerCreates instanceof Map + ) { + return resolved as MemorySearchManagerCacheStore; + } + const repaired = createMemorySearchManagerCacheStore(); + (globalThis as Record)[MEMORY_SEARCH_MANAGER_CACHE_KEY] = repaired; + return repaired; } const log = createSubsystemLogger("memory");