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>
This commit is contained in:
Patrick Erichsen
2026-04-23 21:21:04 -07:00
committed by GitHub
parent 84fc00afda
commit 4d7c4b3298
4 changed files with 73 additions and 6 deletions

View File

@@ -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<PropertyKey, unknown>)[cacheKey] = {};
const cache = resolveSingletonManagedCache<TestEntry>(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<PropertyKey, unknown>)[cacheKey];
});
it("deduplicates concurrent creation for the same cache key", async () => {
const cache = createTestCache();
cachesForCleanup.push(cache);

View File

@@ -10,10 +10,24 @@ export type ManagedCache<T> = {
};
export function resolveSingletonManagedCache<T>(cacheKey: symbol): ManagedCache<T> {
return resolveGlobalSingleton<ManagedCache<T>>(cacheKey, () => ({
const resolved = resolveGlobalSingleton<unknown>(cacheKey, () => ({
cache: new Map<string, T>(),
pending: new Map<string, Promise<T>>(),
}));
if (
typeof resolved === "object" &&
resolved !== null &&
(resolved as Partial<ManagedCache<T>>).cache instanceof Map &&
(resolved as Partial<ManagedCache<T>>).pending instanceof Map
) {
return resolved as ManagedCache<T>;
}
const repaired: ManagedCache<T> = {
cache: new Map<string, T>(),
pending: new Map<string, Promise<T>>(),
};
(globalThis as Record<PropertyKey, unknown>)[cacheKey] = repaired;
return repaired;
}
export async function getOrCreateManagedCacheEntry<T>(params: {

View File

@@ -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<PropertyKey, unknown>)[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<PropertyKey, unknown>)[cacheKey];
}
});
it("reuses the same QMD manager instance for repeated calls", async () => {
const cfg = createQmdCfg("main");

View File

@@ -42,15 +42,30 @@ type MemorySearchManagerCacheStore = {
pendingQmdManagerCreates: Map<string, PendingQmdManagerCreate>;
};
function createMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore {
return {
qmdManagerCache: new Map<string, CachedQmdManagerEntry>(),
pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(),
};
}
function getMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore {
// Keep caches reachable across `vi.resetModules()` so later cleanup can close older instances.
return resolveGlobalSingleton<MemorySearchManagerCacheStore>(
const resolved = resolveGlobalSingleton<unknown>(
MEMORY_SEARCH_MANAGER_CACHE_KEY,
() => ({
qmdManagerCache: new Map<string, CachedQmdManagerEntry>(),
pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(),
}),
createMemorySearchManagerCacheStore,
);
if (
typeof resolved === "object" &&
resolved !== null &&
(resolved as Partial<MemorySearchManagerCacheStore>).qmdManagerCache instanceof Map &&
(resolved as Partial<MemorySearchManagerCacheStore>).pendingQmdManagerCreates instanceof Map
) {
return resolved as MemorySearchManagerCacheStore;
}
const repaired = createMemorySearchManagerCacheStore();
(globalThis as Record<PropertyKey, unknown>)[MEMORY_SEARCH_MANAGER_CACHE_KEY] = repaired;
return repaired;
}
const log = createSubsystemLogger("memory");