Memory: surface explicit memory_search unavailable status

This commit is contained in:
Vignesh Natarajan
2026-02-20 20:30:52 -08:00
parent 1cc2263578
commit 93c2f20a23
4 changed files with 110 additions and 3 deletions

View File

@@ -170,7 +170,10 @@ describe("memory tools", () => {
expect(result.details).toEqual({
results: [],
disabled: true,
unavailable: true,
error: "openai embeddings failed: 429 insufficient_quota",
warning: "Memory search is unavailable because the embedding provider quota is exhausted.",
action: "Top up or switch embedding provider, then retry memory_search.",
});
});

View File

@@ -0,0 +1,84 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
type SearchImpl = () => Promise<unknown[]>;
let searchImpl: SearchImpl = async () => [];
const stubManager = {
search: vi.fn(async () => await searchImpl()),
readFile: vi.fn(),
status: () => ({
backend: "builtin" as const,
files: 1,
chunks: 1,
dirty: false,
workspaceDir: "/workspace",
dbPath: "/workspace/.memory/index.sqlite",
provider: "builtin",
model: "builtin",
requestedProvider: "builtin",
sources: ["memory" as const],
sourceCounts: [{ source: "memory" as const, files: 1, chunks: 1 }],
}),
sync: vi.fn(),
probeVectorAvailability: vi.fn(async () => true),
close: vi.fn(),
};
vi.mock("../../memory/index.js", () => ({
getMemorySearchManager: async () => ({ manager: stubManager }),
}));
import { createMemorySearchTool } from "./memory-tool.js";
describe("memory_search unavailable payloads", () => {
beforeEach(() => {
searchImpl = async () => [];
vi.clearAllMocks();
});
it("returns explicit unavailable metadata for quota failures", async () => {
searchImpl = async () => {
throw new Error("openai embeddings failed: 429 insufficient_quota");
};
const tool = createMemorySearchTool({
config: { agents: { list: [{ id: "main", default: true }] } },
});
if (!tool) {
throw new Error("tool missing");
}
const result = await tool.execute("quota", { query: "hello" });
expect(result.details).toEqual({
results: [],
disabled: true,
unavailable: true,
error: "openai embeddings failed: 429 insufficient_quota",
warning: "Memory search is unavailable because the embedding provider quota is exhausted.",
action: "Top up or switch embedding provider, then retry memory_search.",
});
});
it("returns explicit unavailable metadata for non-quota failures", async () => {
searchImpl = async () => {
throw new Error("embedding provider timeout");
};
const tool = createMemorySearchTool({
config: { agents: { list: [{ id: "main", default: true }] } },
});
if (!tool) {
throw new Error("tool missing");
}
const result = await tool.execute("generic", { query: "hello" });
expect(result.details).toEqual({
results: [],
disabled: true,
unavailable: true,
error: "embedding provider timeout",
warning: "Memory search is unavailable due to an embedding/provider error.",
action: "Check embedding provider configuration and retry memory_search.",
});
});
});

View File

@@ -50,7 +50,7 @@ export function createMemorySearchTool(options: {
label: "Memory Search",
name: "memory_search",
description:
"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines.",
"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.",
parameters: MemorySearchSchema,
execute: async (_toolCallId, params) => {
const query = readStringParam(params, "query", { required: true });
@@ -61,7 +61,7 @@ export function createMemorySearchTool(options: {
agentId,
});
if (!manager) {
return jsonResult({ results: [], disabled: true, error });
return jsonResult(buildMemorySearchUnavailableResult(error));
}
try {
const citationsMode = resolveMemoryCitationsMode(cfg);
@@ -92,7 +92,7 @@ export function createMemorySearchTool(options: {
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return jsonResult({ results: [], disabled: true, error: message });
return jsonResult(buildMemorySearchUnavailableResult(message));
}
},
};
@@ -192,6 +192,25 @@ function clampResultsByInjectedChars(
return clamped;
}
function buildMemorySearchUnavailableResult(error: string | undefined) {
const reason = (error ?? "memory search unavailable").trim() || "memory search unavailable";
const isQuotaError = /insufficient_quota|quota|429/.test(reason.toLowerCase());
const warning = isQuotaError
? "Memory search is unavailable because the embedding provider quota is exhausted."
: "Memory search is unavailable due to an embedding/provider error.";
const action = isQuotaError
? "Top up or switch embedding provider, then retry memory_search."
: "Check embedding provider configuration and retry memory_search.";
return {
results: [],
disabled: true,
unavailable: true,
error: reason,
warning,
action,
};
}
function shouldIncludeCitations(params: {
mode: MemoryCitationsMode;
sessionKey?: string;