From 93c2f20a23168004b85dcf626600a7d72970786f Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Fri, 20 Feb 2026 20:30:52 -0800 Subject: [PATCH] Memory: surface explicit memory_search unavailable status --- CHANGELOG.md | 1 + src/agents/tools/memory-tool.e2e.test.ts | 3 + src/agents/tools/memory-tool.test.ts | 84 ++++++++++++++++++++++++ src/agents/tools/memory-tool.ts | 25 ++++++- 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/agents/tools/memory-tool.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b92acc08c8..97dd23edba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai - TUI/Heartbeat: suppress heartbeat ACK/prompt noise in chat streaming when `showOk` is disabled, while still preserving non-ACK heartbeat alerts in final output. (#20228) Thanks @bhalliburton. - TUI/History: cap chat-log component growth and prune stale render nodes/references so large default history loads no longer overflow render recursion with `RangeError: Maximum call stack size exceeded`. (#18068) Thanks @JaniJegoroff. - Memory/QMD: diversify mixed-source search ranking when both session and memory collections are present so session transcript hits no longer crowd out durable memory-file matches in top results. (#19913) Thanks @alextempr. +- Memory/Tools: return explicit `unavailable` warnings/actions from `memory_search` when embedding/provider failures occur (including quota exhaustion), so disabled memory does not look like an empty recall result. (#21894) Thanks @XBS9. - Auth/Onboarding: align OAuth profile-id config mapping with stored credential IDs for OpenAI Codex and Chutes flows, preventing `provider:default` mismatches when OAuth returns email-scoped credentials. (#12692) thanks @mudrii. - Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek. - Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow. diff --git a/src/agents/tools/memory-tool.e2e.test.ts b/src/agents/tools/memory-tool.e2e.test.ts index 3566ab98f90..08f9aa66a3c 100644 --- a/src/agents/tools/memory-tool.e2e.test.ts +++ b/src/agents/tools/memory-tool.e2e.test.ts @@ -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.", }); }); diff --git a/src/agents/tools/memory-tool.test.ts b/src/agents/tools/memory-tool.test.ts new file mode 100644 index 00000000000..08bb6775488 --- /dev/null +++ b/src/agents/tools/memory-tool.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +type SearchImpl = () => Promise; +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.", + }); + }); +}); diff --git a/src/agents/tools/memory-tool.ts b/src/agents/tools/memory-tool.ts index f2c169b7263..c0d595b21a2 100644 --- a/src/agents/tools/memory-tool.ts +++ b/src/agents/tools/memory-tool.ts @@ -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;