From 1a4313c2aa6c18d2f9c6bdc56845db6de03164dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 09:45:45 +0000 Subject: [PATCH] fix: avoid crash on memory embeddings errors (#1004) --- CHANGELOG.md | 1 + ...mory-tool.does-not-crash-on-errors.test.ts | 62 +++++++++++++++++++ src/agents/tools/memory-tool.ts | 46 ++++++++------ src/cli/memory-cli.ts | 26 ++++++-- src/macos/gateway-daemon.ts | 8 ++- src/macos/relay.ts | 5 +- 6 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b14aa5d53..8054c4a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.clawd.bot - Sessions: propagate deliveryContext into last-route updates to keep account/channel routing stable. (#1058) - Sessions: preserve overrides on `/new` reset. - Memory: prevent unhandled rejections when watch/interval sync fails. (#1076) — thanks @roshanasingh4. +- Memory: avoid gateway crash when embeddings return 429/insufficient_quota (disable tool + surface error). (#1004) - Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing. - Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields. - Build: allow `@lydell/node-pty` builds on supported platforms. diff --git a/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts b/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts new file mode 100644 index 000000000..638b833e0 --- /dev/null +++ b/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../memory/index.js", () => { + return { + getMemorySearchManager: async () => { + return { + manager: { + search: async () => { + throw new Error("openai embeddings failed: 429 insufficient_quota"); + }, + readFile: async () => { + throw new Error("path required"); + }, + status: () => ({ + files: 0, + chunks: 0, + dirty: true, + workspaceDir: "/tmp", + dbPath: "/tmp/index.sqlite", + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + }), + }, + }; + }, + }; +}); + +import { createMemoryGetTool, createMemorySearchTool } from "./memory-tool.js"; + +describe("memory tools", () => { + it("does not throw when memory_search fails (e.g. embeddings 429)", async () => { + const cfg = { agents: { list: [{ id: "main", default: true }] } }; + const tool = createMemorySearchTool({ config: cfg }); + expect(tool).not.toBeNull(); + if (!tool) throw new Error("tool missing"); + + const result = await tool.execute("call_1", { query: "hello" }); + expect(result.details).toEqual({ + results: [], + disabled: true, + error: "openai embeddings failed: 429 insufficient_quota", + }); + }); + + it("does not throw when memory_get fails", async () => { + const cfg = { agents: { list: [{ id: "main", default: true }] } }; + const tool = createMemoryGetTool({ config: cfg }); + expect(tool).not.toBeNull(); + if (!tool) throw new Error("tool missing"); + + const result = await tool.execute("call_2", { path: "memory/NOPE.md" }); + expect(result.details).toEqual({ + path: "memory/NOPE.md", + text: "", + disabled: true, + error: "path required", + }); + }); +}); + diff --git a/src/agents/tools/memory-tool.ts b/src/agents/tools/memory-tool.ts index a3fc80484..28ea5971f 100644 --- a/src/agents/tools/memory-tool.ts +++ b/src/agents/tools/memory-tool.ts @@ -47,18 +47,23 @@ export function createMemorySearchTool(options: { if (!manager) { return jsonResult({ results: [], disabled: true, error }); } - const results = await manager.search(query, { - maxResults, - minScore, - sessionKey: options.agentSessionKey, - }); - const status = manager.status(); - return jsonResult({ - results, - provider: status.provider, - model: status.model, - fallback: status.fallback, - }); + try { + const results = await manager.search(query, { + maxResults, + minScore, + sessionKey: options.agentSessionKey, + }); + const status = manager.status(); + return jsonResult({ + results, + provider: status.provider, + model: status.model, + fallback: status.fallback, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return jsonResult({ results: [], disabled: true, error: message }); + } }, }; } @@ -91,12 +96,17 @@ export function createMemoryGetTool(options: { if (!manager) { return jsonResult({ path: relPath, text: "", disabled: true, error }); } - const result = await manager.readFile({ - relPath, - from: from ?? undefined, - lines: lines ?? undefined, - }); - return jsonResult(result); + try { + const result = await manager.readFile({ + relPath, + from: from ?? undefined, + lines: lines ?? undefined, + }); + return jsonResult(result); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return jsonResult({ path: relPath, text: "", disabled: true, error: message }); + } }, }; } diff --git a/src/cli/memory-cli.ts b/src/cli/memory-cli.ts index 04cb32625..c90f47d18 100644 --- a/src/cli/memory-cli.ts +++ b/src/cli/memory-cli.ts @@ -75,8 +75,14 @@ export function registerMemoryCli(program: Command) { defaultRuntime.log(error ?? "Memory search disabled."); return; } - await manager.sync({ reason: "cli", force: opts.force }); - defaultRuntime.log("Memory index updated."); + try { + await manager.sync({ reason: "cli", force: opts.force }); + defaultRuntime.log("Memory index updated."); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + defaultRuntime.error(`Memory index failed: ${message}`); + process.exitCode = 1; + } }); memory @@ -105,10 +111,18 @@ export function registerMemoryCli(program: Command) { defaultRuntime.log(error ?? "Memory search disabled."); return; } - const results = await manager.search(query, { - maxResults: opts.maxResults, - minScore: opts.minScore, - }); + let results: Awaited>; + try { + results = await manager.search(query, { + maxResults: opts.maxResults, + minScore: opts.minScore, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + defaultRuntime.error(`Memory search failed: ${message}`); + process.exitCode = 1; + return; + } if (opts.json) { defaultRuntime.log(JSON.stringify({ results }, null, 2)); return; diff --git a/src/macos/gateway-daemon.ts b/src/macos/gateway-daemon.ts index 88b07a469..606c33954 100644 --- a/src/macos/gateway-daemon.ts +++ b/src/macos/gateway-daemon.ts @@ -178,4 +178,10 @@ async function main() { } } -void main(); +void main().catch((err) => { + console.error( + "[clawdbot] Gateway daemon failed:", + err instanceof Error ? (err.stack ?? err.message) : err, + ); + process.exit(1); +}); diff --git a/src/macos/relay.ts b/src/macos/relay.ts index a0d0d09fe..dcf7e998b 100644 --- a/src/macos/relay.ts +++ b/src/macos/relay.ts @@ -70,4 +70,7 @@ async function main() { await program.parseAsync(process.argv); } -void main(); +void main().catch((err) => { + console.error("[clawdbot] Relay failed:", err instanceof Error ? (err.stack ?? err.message) : err); + process.exit(1); +});