Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
});
});
28 changes: 27 additions & 1 deletion extensions/memory-core/src/memory/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading