From e5afb4f84cf26c345642c35348cdf2c38eedcfd0 Mon Sep 17 00:00:00 2001 From: fred-doctorina Date: Wed, 9 Sep 2026 19:42:01 +0200 Subject: [PATCH 1/2] memory-core: make context-service seam timeout configurable (default 2500ms) --- extensions/memory-core/src/memory/manager.ts | 28 +++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/extensions/memory-core/src/memory/manager.ts b/extensions/memory-core/src/memory/manager.ts index 857188debc6f7..e42f5db33b482 100644 --- a/extensions/memory-core/src/memory/manager.ts +++ b/extensions/memory-core/src/memory/manager.ts @@ -220,6 +220,32 @@ async function closeMemoryIndexManagersForScope(params: { } } +/** + * Resolve the context-service seam abort timeout (milliseconds). + * + * The seam ({@link MemoryIndexManager.__fredContextServiceSearch}) is bounded by + * an abort timeout so a slow or cold context-service call can never stall a + * turn. The prior hard-coded 800ms was too tight for a cold embedding + * round-trip (query-embedding warm-up measured ~700-760ms), so a legitimate + * cold call was aborted and silently fell through to the dead native path + * (reported as provider:none / backend:builtin / 0 hits). The default is raised + * to 2500ms and made configurable via FRED_CONTEXT_SERVICE_TIMEOUT_MS. The + * value is clamped to [100, 30000]ms; missing or invalid input uses the default. + */ +export function resolveContextServiceTimeoutMs(raw: string | undefined): number { + const DEFAULT_MS = 2500; + const MIN_MS = 100; + const MAX_MS = 30000; + if (raw === undefined || raw.trim() === "") { + return DEFAULT_MS; + } + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + return DEFAULT_MS; + } + return Math.min(MAX_MS, Math.max(MIN_MS, Math.round(parsed))); +} + export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager { private readonly cacheKey: string; private readonly purpose: MemoryIndexManagerPurpose; @@ -628,7 +654,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query, k, mode: "hybrid" }), - signal: AbortSignal.timeout(800), + signal: AbortSignal.timeout(resolveContextServiceTimeoutMs(process.env.FRED_CONTEXT_SERVICE_TIMEOUT_MS)), }); if (!res || !res.ok) { return undefined; From be3320498855e3f3e6201448e9512abfe6379ae4 Mon Sep 17 00:00:00 2001 From: fred-doctorina Date: Wed, 9 Sep 2026 19:42:01 +0200 Subject: [PATCH 2/2] memory-core: test context-service timeout resolver (default/clamp/invalid) --- .../manager.context-service-timeout.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 extensions/memory-core/src/memory/manager.context-service-timeout.test.ts diff --git a/extensions/memory-core/src/memory/manager.context-service-timeout.test.ts b/extensions/memory-core/src/memory/manager.context-service-timeout.test.ts new file mode 100644 index 0000000000000..316c330c7fd59 --- /dev/null +++ b/extensions/memory-core/src/memory/manager.context-service-timeout.test.ts @@ -0,0 +1,33 @@ +// Unit coverage for the context-service seam abort-timeout resolver. +// +// The seam abort budget was hard-coded at 800ms, which is below a cold +// query-embedding round-trip (~700-760ms), so a legitimate cold context-service +// call was aborted and silently fell through to the dead native path. The +// budget is now configurable via FRED_CONTEXT_SERVICE_TIMEOUT_MS with a 2500ms +// default and [100, 30000]ms clamp. These tests pin that contract. +import { describe, expect, it } from "vitest"; +import { resolveContextServiceTimeoutMs } from "./manager.js"; + +describe("resolveContextServiceTimeoutMs", () => { + it("defaults to 2500ms when unset or blank", () => { + expect(resolveContextServiceTimeoutMs(undefined)).toBe(2500); + expect(resolveContextServiceTimeoutMs("")).toBe(2500); + expect(resolveContextServiceTimeoutMs(" ")).toBe(2500); + }); + + it("parses a valid positive integer", () => { + expect(resolveContextServiceTimeoutMs("1500")).toBe(1500); + expect(resolveContextServiceTimeoutMs("2500")).toBe(2500); + }); + + it("clamps to [100, 30000]", () => { + expect(resolveContextServiceTimeoutMs("5")).toBe(100); + expect(resolveContextServiceTimeoutMs("99999")).toBe(30000); + }); + + it("falls back to the default on invalid input", () => { + expect(resolveContextServiceTimeoutMs("abc")).toBe(2500); + expect(resolveContextServiceTimeoutMs("-100")).toBe(2500); + expect(resolveContextServiceTimeoutMs("0")).toBe(2500); + }); +});