perf: lazy load memory embedding runtime

This commit is contained in:
Peter Steinberger
2026-05-08 05:39:09 +01:00
parent 8dcc2ff1d2
commit eabae023eb
12 changed files with 73 additions and 35 deletions

View File

@@ -802,7 +802,9 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
expect(codexPluginsManifestKeys).toEqual([...CODEX_PLUGINS_CONFIG_KEYS].toSorted());
expect(codexPluginsProperties.additionalProperties).toBe(false);
for (const key of CODEX_PLUGINS_CONFIG_KEYS) {
expect(manifest.uiHints[`codexPlugins.${key}`]).toBeTruthy();
expect(manifest.uiHints[`codexPlugins.${key}`]).toMatchObject({
label: expect.any(String),
});
}
const pluginEntryProperties = (
codexPluginsProperties.properties.plugins as {

View File

@@ -9,13 +9,8 @@
import { Buffer } from "node:buffer";
import { randomUUID } from "node:crypto";
import type * as LanceDB from "@lancedb/lancedb";
import OpenAI from "openai";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import {
getMemoryEmbeddingProvider,
type MemoryEmbeddingProvider,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/memory-host-core";
import type { MemoryEmbeddingProvider } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
import {
@@ -64,6 +59,40 @@ type AutoCaptureCursor = {
lastMessageFingerprint?: string;
};
type OpenAiEmbeddingClient = {
post<T>(
path: string,
options: { body: unknown; timeout?: number; maxRetries?: number },
): Promise<T>;
};
let openAiModulePromise: Promise<typeof import("openai")> | undefined;
function loadOpenAiModule(): Promise<typeof import("openai")> {
openAiModulePromise ??= import("openai");
return openAiModulePromise;
}
let memoryEmbeddingProviderModulePromise:
| Promise<typeof import("openclaw/plugin-sdk/memory-core-host-engine-embeddings")>
| undefined;
function loadMemoryEmbeddingProviderModule(): Promise<
typeof import("openclaw/plugin-sdk/memory-core-host-engine-embeddings")
> {
memoryEmbeddingProviderModulePromise ??=
import("openclaw/plugin-sdk/memory-core-host-engine-embeddings");
return memoryEmbeddingProviderModulePromise;
}
let memoryHostCoreModulePromise:
| Promise<typeof import("openclaw/plugin-sdk/memory-host-core")>
| undefined;
function loadMemoryHostCoreModule(): Promise<
typeof import("openclaw/plugin-sdk/memory-host-core")
> {
memoryHostCoreModulePromise ??= import("openclaw/plugin-sdk/memory-host-core");
return memoryHostCoreModulePromise;
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
@@ -314,7 +343,7 @@ type Embeddings = {
};
class OpenAiCompatibleEmbeddings implements Embeddings {
private client: OpenAI;
private clientPromise: Promise<OpenAiEmbeddingClient>;
constructor(
apiKey: string,
@@ -322,11 +351,13 @@ class OpenAiCompatibleEmbeddings implements Embeddings {
baseUrl?: string,
private dimensions?: number,
) {
this.client = new OpenAI({ apiKey, baseURL: baseUrl });
this.clientPromise = loadOpenAiModule().then(
({ default: OpenAI }) => new OpenAI({ apiKey, baseURL: baseUrl }) as OpenAiEmbeddingClient,
);
}
async embed(text: string, options?: { timeoutMs?: number }): Promise<number[]> {
const params: OpenAI.EmbeddingCreateParams = {
const params: Record<string, unknown> = {
model: this.model,
input: text,
};
@@ -338,7 +369,9 @@ class OpenAiCompatibleEmbeddings implements Embeddings {
// omitted, then decodes the response. Several compatible providers either
// reject encoding_format or always return float arrays, so use the generic
// transport and normalize the response ourselves.
const response = await this.client.post<EmbeddingCreateResponse>("/embeddings", {
const response = await (
await this.clientPromise
).post<EmbeddingCreateResponse>("/embeddings", {
body: params,
...(options?.timeoutMs ? { timeout: options.timeoutMs, maxRetries: 0 } : {}),
});
@@ -367,10 +400,12 @@ class ProviderAdapterEmbeddings implements Embeddings {
private async createProvider(): Promise<MemoryEmbeddingProvider> {
const cfg = (this.api.runtime.config?.current?.() ?? this.api.config) as OpenClawConfig;
const providerId = this.embedding.provider;
const { getMemoryEmbeddingProvider } = await loadMemoryEmbeddingProviderModule();
const adapter = getMemoryEmbeddingProvider(providerId, cfg);
if (!adapter) {
throw new Error(`Unknown memory embedding provider: ${providerId}`);
}
const { resolveDefaultAgentId } = await loadMemoryHostCoreModule();
const defaultAgentId = resolveDefaultAgentId(cfg);
const agentDir = this.api.runtime.agent.resolveAgentDir(cfg, defaultAgentId);
const remote =

View File

@@ -1463,14 +1463,14 @@ describe("slack prepareSlackMessage inbound contract", () => {
opts: { source: "message" },
});
expect(root).toBeTruthy();
expect(followUp).toBeTruthy();
assertPrepared(root, "root message");
assertPrepared(followUp, "follow-up message");
// Without the seeding fix, root would land on `agent:main:slack:channel:c0agg76cp1s`
// while followUp would land on `:thread:<rootTs>`, splitting the conversation
// across two sessions. Both must share one session key.
expect(root!.ctxPayload.SessionKey).toBe(expectedSessionKey);
expect(followUp!.ctxPayload.SessionKey).toBe(expectedSessionKey);
expect(new Set([root!.ctxPayload.SessionKey, followUp!.ctxPayload.SessionKey]).size).toBe(1);
expect(root.ctxPayload.SessionKey).toBe(expectedSessionKey);
expect(followUp.ctxPayload.SessionKey).toBe(expectedSessionKey);
expect(new Set([root.ctxPayload.SessionKey, followUp.ctxPayload.SessionKey]).size).toBe(1);
});
it("treats Slack user-group mentions as explicit mentions when the bot is a member", async () => {

View File

@@ -817,12 +817,6 @@ describe("resolveTelegramFetch", () => {
const seventhDispatcher = getDispatcherFromUndiciCall(7);
const eighthDispatcher = getDispatcherFromUndiciCall(8);
expect(firstDispatcher).toBeDefined();
expect(secondDispatcher).toBeDefined();
expect(sixthDispatcher).toBeDefined();
expect(seventhDispatcher).toBeDefined();
expect(eighthDispatcher).toBeDefined();
expect(firstDispatcher).not.toBe(secondDispatcher);
expect(secondDispatcher).toBe(sixthDispatcher);
expect(seventhDispatcher).toBe(firstDispatcher);

View File

@@ -171,8 +171,7 @@ describe("resolveMemoryBackendConfig", () => {
} as OpenClawConfig;
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
const custom = resolved.qmd?.collections.find((c) => c.name.startsWith("custom-notes"));
expect(custom).toBeDefined();
expect(custom?.path).toBe(path.resolve("/workspace/root", "notes"));
expect(custom).toMatchObject({ path: path.resolve("/workspace/root", "notes") });
});
it("scopes qmd collection names per agent", () => {

View File

@@ -44,7 +44,10 @@ describe("package withRemoteHttpResponse", () => {
...deps,
});
expect(deps.calls[0]).toBeDefined();
expect(deps.calls[0]).not.toHaveProperty("mode");
expect(deps.calls).toEqual([
expect.not.objectContaining({
mode: expect.any(String),
}),
]);
});
});

View File

@@ -124,7 +124,6 @@ describe("buildSessionEntry", () => {
// Content line 0 → JSONL line 4 (the first user message)
// Content line 1 → JSONL line 6 (the assistant message)
// Content line 2 → JSONL line 7 (the second user message)
expect(entry!.lineMap).toBeDefined();
expect(entry!.lineMap).toEqual([4, 6, 7]);
});

View File

@@ -578,8 +578,12 @@ liveGatewayDescribe("OpenClaw SDK live Gateway e2e", () => {
try {
await oc.connect();
await expect(oc.agents.list()).resolves.toBeDefined();
await expect(oc.models.status({ probe: false })).resolves.toBeDefined();
await expect(oc.agents.list()).resolves.toEqual(
expect.objectContaining({ agents: expect.any(Array) }),
);
await expect(oc.models.status({ probe: false })).resolves.toEqual(
expect.objectContaining({ providers: expect.any(Array) }),
);
const agent = await oc.agents.get(process.env.OPENCLAW_SDK_LIVE_AGENT_ID ?? "main");
const run = await agent.run({

View File

@@ -135,7 +135,6 @@ describe("acp translator stable lifecycle handlers", () => {
const result = await agent.initialize(createInitializeRequest());
const capabilities = result.agentCapabilities;
expect(capabilities).toBeDefined();
if (!capabilities) {
throw new Error("initialize response did not include agent capabilities");
}

View File

@@ -1091,10 +1091,12 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => {
]
>;
const runtimeContext = contextEngineCompactCalls[0]?.[0]?.runtimeContext;
expect(runtimeContext).toBeDefined();
if (!runtimeContext) {
throw new Error("expected compaction runtime context");
}
await expect(
runtimeContext?.llm?.complete?.({
runtimeContext.llm?.complete?.({
messages: [{ role: "user", content: "summarize" }],
agentId: "other-agent",
}),

View File

@@ -482,9 +482,10 @@ describe("config io write prepare", () => {
auth: { mode: "token" },
});
const channels = persisted.channels as Record<string, Record<string, unknown>> | undefined;
expect(channels?.imessage).toBeDefined();
expect(channels?.imessage).toMatchObject({
cliPath: "/usr/local/bin/imsg",
});
expect(channels?.imessage).not.toHaveProperty("runtimeOnlyDefault");
expect(channels?.imessage?.cliPath).toBe("/usr/local/bin/imsg");
});
it("does not reintroduce legacy nested dm.policy defaults in the persisted candidate", () => {

View File

@@ -25,7 +25,7 @@ describe("movePathWithCopyFallback", () => {
}),
).rejects.toThrow("Hardlinked source file is not allowed");
await expect(fs.stat(sourceFile)).resolves.toBeTruthy();
await expect(fs.readFile(sourceFile, "utf8")).resolves.toBe("hello");
await expect(fs.stat(targetDir)).rejects.toMatchObject({ code: "ENOENT" });
});
},