Skip to content

Commit df80fe7

Browse files
fix(memory): gracefully degrade when embedding provider is unregistered
When a configured embedding provider (e.g. 'openai') has no registered adapter—typically because the provider plugin is not installed— ensureProviderInitialized() previously let the 'Unknown memory embedding provider' error propagate, crashing the CLI on . Now the error is caught and the manager gracefully degrades to FTS-only mode, setting providerUnavailableReason so that still works and shows the real problem instead of a stack trace. Refs #91778
1 parent bf89552 commit df80fe7

2 files changed

Lines changed: 121 additions & 1 deletion

File tree

extensions/memory-core/src/memory/manager.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,22 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
471471
// Clear the cached rejected promise so subsequent calls can retry
472472
// initialization instead of being permanently stuck with a stale failure.
473473
this.providerInitPromise = null;
474-
throw err;
474+
const message = formatErrorMessage(err);
475+
const isUnknownProvider = message.startsWith("Unknown memory embedding provider:");
476+
if (isUnknownProvider) {
477+
// Gracefully degrade: the configured embedding provider plugin is not
478+
// installed or not registered. Set FTS-only mode so the CLI and
479+
// memory status still work instead of crashing.
480+
this.applyProviderResult({
481+
provider: null,
482+
requestedProvider: this.requestedProvider,
483+
providerUnavailableReason: message,
484+
});
485+
this.providerKey = this.computeProviderKey();
486+
this.batch = this.resolveBatchConfig();
487+
} else {
488+
throw err;
489+
}
475490
} finally {
476491
if (this.providerInitialized) {
477492
this.providerInitPromise = null;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import fs from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
5+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
6+
import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js";
7+
import type { MemoryIndexManager } from "./manager.js";
8+
import "./test-runtime-mocks.js";
9+
10+
const createEmbeddingProviderMock = vi.hoisted(() =>
11+
vi.fn(async () => {
12+
throw new Error("Unknown memory embedding provider: nonexistent-provider");
13+
}),
14+
);
15+
16+
vi.mock("./embeddings.js", () => ({
17+
createEmbeddingProvider: createEmbeddingProviderMock,
18+
resolveEmbeddingProviderAdapterId: () => undefined,
19+
resolveEmbeddingProviderAdapterTransport: () => "remote" as const,
20+
resolveEmbeddingProviderFallbackModel: () => "fts-only",
21+
}));
22+
23+
describe("memory manager unknown provider graceful degradation", () => {
24+
let fixtureRoot = "";
25+
let caseId = 0;
26+
let workspaceDir = "";
27+
let indexPath = "";
28+
let manager: MemoryIndexManager | null = null;
29+
30+
beforeAll(async () => {
31+
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mem-unknown-provider-"));
32+
});
33+
34+
beforeEach(async () => {
35+
createEmbeddingProviderMock.mockClear();
36+
workspaceDir = path.join(fixtureRoot, `case-${caseId++}`);
37+
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
38+
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Test memory note.\n\nAlpha topic.");
39+
indexPath = path.join(workspaceDir, "index.sqlite");
40+
});
41+
42+
afterEach(async () => {
43+
if (manager) {
44+
await manager.close();
45+
manager = null;
46+
}
47+
await closeAllMemorySearchManagers();
48+
});
49+
50+
afterAll(async () => {
51+
await closeAllMemorySearchManagers();
52+
if (fixtureRoot) {
53+
await fs.rm(fixtureRoot, { recursive: true, force: true });
54+
}
55+
});
56+
57+
async function createManager(
58+
params: { provider?: string; vectorEnabled?: boolean } = {},
59+
): Promise<MemoryIndexManager> {
60+
const store =
61+
params.vectorEnabled === undefined
62+
? { path: indexPath }
63+
: { path: indexPath, vector: { enabled: params.vectorEnabled } };
64+
const cfg = {
65+
memory: { backend: "builtin" },
66+
agents: {
67+
defaults: {
68+
workspace: workspaceDir,
69+
memorySearch: {
70+
provider: params.provider ?? "nonexistent-provider",
71+
model: "",
72+
store,
73+
cache: { enabled: false },
74+
sync: { watch: false, onSessionStart: false, onSearch: false },
75+
},
76+
},
77+
list: [{ id: "main", default: true }],
78+
},
79+
} as OpenClawConfig;
80+
const result = await getMemorySearchManager({ cfg, agentId: "main" });
81+
if (!result.manager) {
82+
throw new Error(result.error ?? "manager missing");
83+
}
84+
manager = result.manager as unknown as MemoryIndexManager;
85+
return manager;
86+
}
87+
88+
it("does not crash on probeEmbeddingAvailability when provider is unknown", async () => {
89+
const memoryManager = await createManager();
90+
91+
const probeResult = await memoryManager.probeEmbeddingAvailability();
92+
expect(probeResult.ok).toBe(false);
93+
expect(probeResult.error).toContain("Unknown memory embedding provider");
94+
});
95+
96+
it("reports fts-only lifecycle after probe when provider is unknown", async () => {
97+
const memoryManager = await createManager();
98+
99+
await memoryManager.probeEmbeddingAvailability();
100+
101+
const status = memoryManager.status();
102+
expect(status.custom?.providerState?.mode).toBe("fts-only");
103+
expect(status.custom?.providerState?.reason).toContain("Unknown memory embedding provider");
104+
});
105+
});

0 commit comments

Comments
 (0)