diff --git a/extensions/memory-core/src/memory/manager-search-orchestration.test.ts b/extensions/memory-core/src/memory/manager-search-orchestration.test.ts index e56e02ef0bbf..c77d5d1747c3 100644 --- a/extensions/memory-core/src/memory/manager-search-orchestration.test.ts +++ b/extensions/memory-core/src/memory/manager-search-orchestration.test.ts @@ -143,6 +143,58 @@ describe("memory index", () => { expect(queryCalls).toBe(3); }); + it("keeps a healthy local provider active when the caller cancels search", async () => { + const cfg = createCfg({ + hybrid: { enabled: true, vectorWeight: 0.5, textWeight: 0.5 }, + }); + const manager = await getPersistentManager(cfg); + await manager.sync({ reason: "test" }); + + const close = vi.fn(async () => {}); + let queryCalls = 0; + const fields = manager as unknown as { + provider: { + id: string; + model: string; + embedQuery: (text: string) => Promise; + embedBatch: (texts: string[]) => Promise; + close: () => Promise; + }; + providerKey: string; + providerLifecycle: { mode: "active"; providerId: string }; + computeProviderKey: () => string; + }; + fields.provider = { + id: "local", + model: "mock-embed", + embedQuery: async () => { + queryCalls += 1; + return [1, 0, 0, 0]; + }, + embedBatch: async (texts) => texts.map(() => [1, 0, 0, 0]), + close, + }; + fields.providerLifecycle = { mode: "active", providerId: "local" }; + fields.providerKey = fields.computeProviderKey(); + await manager.sync({ reason: "test", force: true }); + + const abortReason = new Error("memory search was cancelled"); + await expect( + manager.search("alpha", { signal: AbortSignal.abort(abortReason) }), + ).rejects.toMatchObject({ cause: abortReason }); + + expect(manager.status()).toMatchObject({ + provider: "local", + custom: { + providerState: { mode: "active", providerId: "local" }, + providerUnavailableReason: undefined, + }, + }); + await expect(manager.search("alpha")).resolves.not.toStrictEqual([]); + expect(queryCalls).toBe(1); + expect(close).not.toHaveBeenCalled(); + }); + it("supplements thin strict FTS results for conversational queries", async () => { const cases = [ { diff --git a/extensions/memory-core/src/memory/manager-search-orchestration.ts b/extensions/memory-core/src/memory/manager-search-orchestration.ts index 8a74712978d2..10ee52b2ceab 100644 --- a/extensions/memory-core/src/memory/manager-search-orchestration.ts +++ b/extensions/memory-core/src/memory/manager-search-orchestration.ts @@ -285,12 +285,12 @@ export abstract class MemorySearchOrchestration extends MemoryKeywordRetrieval { ); } catch (err) { releaseSemanticProvider(); - this.markLocalEmbeddingProviderDegraded(err); - // An aborted caller already stopped waiting; skip fallback-provider - // activation so the abandoned search stops instead of re-embedding. + // An aborted caller already stopped waiting; keep the provider generation + // healthy and skip fallback activation instead of poisoning later searches. if (opts?.signal?.aborted) { throw err; } + this.markLocalEmbeddingProviderDegraded(err); const message = formatErrorMessage(err); const activatedFallback = this.shouldFallbackOnError(err) ? await this.activateFallbackProvider(message).catch((fallbackErr: unknown) => { @@ -331,7 +331,9 @@ export abstract class MemorySearchOrchestration extends MemoryKeywordRetrieval { ); } catch (fallbackErr) { releaseFallbackProvider(); - this.markLocalEmbeddingProviderDegraded(fallbackErr); + if (!opts?.signal?.aborted) { + this.markLocalEmbeddingProviderDegraded(fallbackErr); + } throw fallbackErr; } finally { releaseFallbackProvider();