diff --git a/src/memory/search-manager.test.ts b/src/memory/search-manager.test.ts index 38f576cebf..991dbe619c 100644 --- a/src/memory/search-manager.test.ts +++ b/src/memory/search-manager.test.ts @@ -114,4 +114,35 @@ describe("getMemorySearchManager caching", () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2); }); + + it("does not evict a newer cached wrapper when closing an older failed wrapper", async () => { + const retryAgentId = "retry-agent-close"; + const cfg = { + memory: { backend: "qmd", qmd: {} }, + agents: { list: [{ id: retryAgentId, default: true, workspace: "/tmp/workspace" }] }, + } as const; + + mockPrimary.search.mockRejectedValueOnce(new Error("qmd query failed")); + + const first = await getMemorySearchManager({ cfg, agentId: retryAgentId }); + expect(first.manager).toBeTruthy(); + if (!first.manager) { + throw new Error("manager missing"); + } + await first.manager.search("hello"); + + const second = await getMemorySearchManager({ cfg, agentId: retryAgentId }); + expect(second.manager).toBeTruthy(); + if (!second.manager) { + throw new Error("manager missing"); + } + expect(second.manager).not.toBe(first.manager); + + await first.manager.close?.(); + + const third = await getMemorySearchManager({ cfg, agentId: retryAgentId }); + expect(third.manager).toBe(second.manager); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(QmdMemoryManager.create).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/memory/search-manager.ts b/src/memory/search-manager.ts index e3b4aaf8a3..aead341764 100644 --- a/src/memory/search-manager.ts +++ b/src/memory/search-manager.ts @@ -68,6 +68,7 @@ class FallbackMemoryManager implements MemorySearchManager { private fallback: MemorySearchManager | null = null; private primaryFailed = false; private lastError?: string; + private cacheEvicted = false; constructor( private readonly deps: { @@ -90,7 +91,7 @@ class FallbackMemoryManager implements MemorySearchManager { log.warn(`qmd memory failed; switching to builtin index: ${this.lastError}`); await this.deps.primary.close?.().catch(() => {}); // Evict the failed wrapper so the next request can retry QMD with a fresh manager. - this.onClose?.(); + this.evictCacheEntry(); } } const fallback = await this.ensureFallback(); @@ -175,7 +176,7 @@ class FallbackMemoryManager implements MemorySearchManager { async close() { await this.deps.primary.close?.(); await this.fallback?.close?.(); - this.onClose?.(); + this.evictCacheEntry(); } private async ensureFallback(): Promise { @@ -190,6 +191,14 @@ class FallbackMemoryManager implements MemorySearchManager { this.fallback = fallback; return this.fallback; } + + private evictCacheEntry(): void { + if (this.cacheEvicted) { + return; + } + this.cacheEvicted = true; + this.onClose?.(); + } } function buildQmdCacheKey(agentId: string, config: ResolvedQmdConfig): string {