fix(memory): preserve provider on cancelled search (#124051)

This commit is contained in:
Peter Steinberger
2026-08-14 23:28:16 -07:00
committed by GitHub
parent 9050ff0e9e
commit 3f6bf166b6
2 changed files with 58 additions and 4 deletions
@@ -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<number[]>;
embedBatch: (texts: string[]) => Promise<number[][]>;
close: () => Promise<void>;
};
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 = [
{
@@ -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();