fix(memory): avoid recursive provider discovery during register (#61402)

* fix(memory): avoid recursive provider discovery during register

* test(memory): remove resetModules from provider adapter regression

* fix: avoid recursive provider discovery during register (#61402) (thanks @ngutman)
This commit is contained in:
Nimrod Gutman
2026-04-05 18:55:58 +03:00
committed by GitHub
parent b169b2c977
commit 0047048179
5 changed files with 78 additions and 4 deletions

View File

@@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai
- Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly `reasoning:stream`, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc.
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.
- Memory: keep `memory-core` builtin embedding registration on the already-registered path so selecting `memory-core` no longer recurses through plugin discovery and crashes during startup. (#61402) Thanks @ngutman.
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
- Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.

View File

@@ -0,0 +1,63 @@
import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBuiltInMemoryEmbeddingProviders } from "./provider-adapters.js";
const mocks = vi.hoisted(() => ({
listRegisteredMemoryEmbeddingProviderAdapters: vi.fn<() => MemoryEmbeddingProviderAdapter[]>(
() => [],
),
listMemoryEmbeddingProviders: vi.fn(() => {
throw new Error("fallback capability loading should stay cold during memory-core register");
}),
}));
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-embeddings", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/memory-core-host-engine-embeddings")
>("openclaw/plugin-sdk/memory-core-host-engine-embeddings");
return {
...actual,
listRegisteredMemoryEmbeddingProviderAdapters:
mocks.listRegisteredMemoryEmbeddingProviderAdapters,
listMemoryEmbeddingProviders: mocks.listMemoryEmbeddingProviders,
};
});
beforeEach(() => {
mocks.listRegisteredMemoryEmbeddingProviderAdapters.mockReset();
mocks.listRegisteredMemoryEmbeddingProviderAdapters.mockReturnValue([]);
mocks.listMemoryEmbeddingProviders.mockClear();
});
describe("registerBuiltInMemoryEmbeddingProviders", () => {
it("uses only already-registered providers when avoiding duplicates", () => {
const ids: string[] = [];
registerBuiltInMemoryEmbeddingProviders({
registerMemoryEmbeddingProvider(adapter) {
ids.push(adapter.id);
},
});
expect(ids).toEqual(["local", "openai", "gemini", "voyage", "mistral"]);
expect(mocks.listRegisteredMemoryEmbeddingProviderAdapters).toHaveBeenCalledTimes(1);
expect(mocks.listMemoryEmbeddingProviders).not.toHaveBeenCalled();
});
it("skips builtin adapters that are already registered in the current load", () => {
mocks.listRegisteredMemoryEmbeddingProviderAdapters.mockReturnValue([
{ id: "local", create: vi.fn() } as MemoryEmbeddingProviderAdapter,
{ id: "gemini", create: vi.fn() } as MemoryEmbeddingProviderAdapter,
]);
const ids: string[] = [];
registerBuiltInMemoryEmbeddingProviders({
registerMemoryEmbeddingProvider(adapter) {
ids.push(adapter.id);
},
});
expect(ids).toEqual(["openai", "voyage", "mistral"]);
expect(mocks.listMemoryEmbeddingProviders).not.toHaveBeenCalled();
});
});

View File

@@ -13,7 +13,7 @@ import {
createOpenAiEmbeddingProvider,
createVoyageEmbeddingProvider,
hasNonTextEmbeddingParts,
listMemoryEmbeddingProviders,
listRegisteredMemoryEmbeddingProviderAdapters,
runGeminiEmbeddingBatches,
runOpenAiEmbeddingBatches,
runVoyageEmbeddingBatches,
@@ -334,7 +334,12 @@ export function getBuiltinMemoryEmbeddingProviderAdapter(
export function registerBuiltInMemoryEmbeddingProviders(register: {
registerMemoryEmbeddingProvider: (adapter: MemoryEmbeddingProviderAdapter) => void;
}): void {
const existingIds = new Set(listMemoryEmbeddingProviders().map((adapter) => adapter.id));
// Only inspect providers already registered in the current load. Falling back
// to capability discovery here can recursively trigger plugin loading while
// memory-core itself is still registering.
const existingIds = new Set(
listRegisteredMemoryEmbeddingProviderAdapters().map((adapter) => adapter.id),
);
for (const adapter of builtinMemoryEmbeddingProviderAdapters) {
if (existingIds.has(adapter.id)) {
continue;

View File

@@ -3,6 +3,7 @@
export {
getMemoryEmbeddingProvider,
listMemoryEmbeddingProviders,
listRegisteredMemoryEmbeddingProviderAdapters,
} from "../plugins/memory-embedding-provider-runtime.js";
export type {
MemoryEmbeddingBatchChunk,

View File

@@ -6,12 +6,16 @@ import {
type MemoryEmbeddingProviderAdapter,
} from "./memory-embedding-providers.js";
export function listRegisteredMemoryEmbeddingProviderAdapters(): MemoryEmbeddingProviderAdapter[] {
return listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter);
}
export function listMemoryEmbeddingProviders(
cfg?: OpenClawConfig,
): MemoryEmbeddingProviderAdapter[] {
const registered = listRegisteredMemoryEmbeddingProviders();
const registered = listRegisteredMemoryEmbeddingProviderAdapters();
if (registered.length > 0) {
return registered.map((entry) => entry.adapter);
return registered;
}
return resolvePluginCapabilityProviders({
key: "memoryEmbeddingProviders",