mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 22:22:54 +00:00
Memory: surface explicit memory_search unavailable status
This commit is contained in:
@@ -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/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.
|
- 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/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.
|
- 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: 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.
|
- Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow.
|
||||||
|
|||||||
@@ -170,7 +170,10 @@ describe("memory tools", () => {
|
|||||||
expect(result.details).toEqual({
|
expect(result.details).toEqual({
|
||||||
results: [],
|
results: [],
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
unavailable: true,
|
||||||
error: "openai embeddings failed: 429 insufficient_quota",
|
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.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
84
src/agents/tools/memory-tool.test.ts
Normal file
84
src/agents/tools/memory-tool.test.ts
Normal 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.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,7 +50,7 @@ export function createMemorySearchTool(options: {
|
|||||||
label: "Memory Search",
|
label: "Memory Search",
|
||||||
name: "memory_search",
|
name: "memory_search",
|
||||||
description:
|
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,
|
parameters: MemorySearchSchema,
|
||||||
execute: async (_toolCallId, params) => {
|
execute: async (_toolCallId, params) => {
|
||||||
const query = readStringParam(params, "query", { required: true });
|
const query = readStringParam(params, "query", { required: true });
|
||||||
@@ -61,7 +61,7 @@ export function createMemorySearchTool(options: {
|
|||||||
agentId,
|
agentId,
|
||||||
});
|
});
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return jsonResult({ results: [], disabled: true, error });
|
return jsonResult(buildMemorySearchUnavailableResult(error));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const citationsMode = resolveMemoryCitationsMode(cfg);
|
const citationsMode = resolveMemoryCitationsMode(cfg);
|
||||||
@@ -92,7 +92,7 @@ export function createMemorySearchTool(options: {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(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;
|
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: {
|
function shouldIncludeCitations(params: {
|
||||||
mode: MemoryCitationsMode;
|
mode: MemoryCitationsMode;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user