diff --git a/CHANGELOG.md b/CHANGELOG.md index d833dc07c86..5635c38ac3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - Agents/subagents: bound automatic orphan recovery with persisted recovery attempts and a wedged-session tombstone, and teach task maintenance/doctor to reconcile those sessions so restart loops no longer require manual `sessions.json` surgery. Fixes #74864. Thanks @solosage1. - Plugins/runtime-deps: keep bundled provider policy config loading from staging plugin runtime dependencies, so config reads no longer fail on locked-down `/var/lib/openclaw/plugin-runtime-deps` directories. Fixes #74971. Thanks @eurojojo. +- Memory/runtime-deps: retain the native `node-llama-cpp` runtime only when local memory search is configured, so packaged installs can repair local embeddings without relying on unreachable global npm installs. Fixes #74777. Thanks @LLagoon3. - Gateway/startup: skip pre-bind web-fetch provider discovery for credential-free `tools.web.fetch` config, so Docker/Kubernetes gateways bind even when optional fetch limits are present. Fixes #74896. Thanks @KoykL. - Slack: require bot-authored room messages with `allowBots=true` to come from an explicitly channel-allowlisted bot or from a room where an explicit Slack owner is present, so broad bot relays cannot run unattended. Fixes #59284. Thanks @andrewhong-translucent. - Signal: derive `getAttachment` HTTP response caps from `channels.signal.mediaMaxMb` with base64 headroom, so inbound photos and videos no longer drop behind the 1 MiB RPC default. Fixes #73564. Thanks @heyhudson. diff --git a/docs/concepts/memory-search.md b/docs/concepts/memory-search.md index c3733b6a060..5584a870cb1 100644 --- a/docs/concepts/memory-search.md +++ b/docs/concepts/memory-search.md @@ -33,8 +33,9 @@ For multi-endpoint setups, `provider` can also be a custom `models.providers.` entry, such as `ollama-5080`, when that provider sets `api: "ollama"` or another embedding adapter owner. -For local embeddings with no API key, install the optional `node-llama-cpp` -runtime package next to OpenClaw and use `provider: "local"`. +For local embeddings with no API key, set `provider: "local"`. Packaged +installs retain the native `node-llama-cpp` runtime in OpenClaw's managed plugin +runtime-deps tree; run `openclaw doctor --fix` if that tree needs repair. Some OpenAI-compatible embedding endpoints require asymmetric labels such as `input_type: "query"` for searches and `input_type: "document"` or `"passage"` diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 9518474bd42..b651f01b6bc 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -284,7 +284,7 @@ For custom OpenAI-compatible endpoints or overriding provider defaults: | `local.modelCacheDir` | `string` | node-llama-cpp default | Cache dir for downloaded models | | `local.contextSize` | `number \| "auto"` | `4096` | Context window size for the embedding context. 4096 covers typical chunks (128–512 tokens) while bounding non-weight VRAM. Lower to 1024–2048 on constrained hosts. `"auto"` uses the model's trained maximum — not recommended for 8B+ models (Qwen3-Embedding-8B: 40 960 tokens → ~32 GB VRAM vs ~8.8 GB at 4096). | - Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded). Requires native build: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`. + Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded). Packaged installs repair the native `node-llama-cpp` runtime through managed plugin runtime deps when `provider: "local"` is configured. Source checkouts still require native build approval: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`. Use the standalone CLI to verify the same provider path the Gateway uses: diff --git a/extensions/memory-core/openclaw.plugin.json b/extensions/memory-core/openclaw.plugin.json index c7ef0f6cfb2..cf5ae6f92a3 100644 --- a/extensions/memory-core/openclaw.plugin.json +++ b/extensions/memory-core/openclaw.plugin.json @@ -7,6 +7,9 @@ "contracts": { "memoryEmbeddingProviders": ["local"] }, + "runtimeDependencies": { + "localMemoryEmbedding": ["node-llama-cpp@3.18.1"] + }, "commandAliases": [ { "name": "dreaming", diff --git a/extensions/memory-core/src/memory/provider-adapters.ts b/extensions/memory-core/src/memory/provider-adapters.ts index d7c62d8042a..e6db2921f02 100644 --- a/extensions/memory-core/src/memory/provider-adapters.ts +++ b/extensions/memory-core/src/memory/provider-adapters.ts @@ -59,7 +59,7 @@ function formatLocalSetupError(err: unknown): string { "To enable local embeddings:", "1) Use Node 24 (recommended for installs/updates; Node 22 LTS, currently 22.14+, remains supported)", missing - ? `2) Install optional local embedding runtime next to OpenClaw: npm i -g ${NODE_LLAMA_CPP_INSTALL_SPEC}` + ? `2) Run openclaw doctor --fix to repair managed plugin runtime deps for ${NODE_LLAMA_CPP_INSTALL_SPEC}` : null, `3) If you use pnpm: pnpm approve-builds (select ${NODE_LLAMA_CPP_RUNTIME_PACKAGE}), then pnpm rebuild ${NODE_LLAMA_CPP_RUNTIME_PACKAGE}`, ...listRemoteEmbeddingSetupHints(), diff --git a/src/plugins/bundled-runtime-deps-selection.ts b/src/plugins/bundled-runtime-deps-selection.ts index a72b376602e..e314e198e67 100644 --- a/src/plugins/bundled-runtime-deps-selection.ts +++ b/src/plugins/bundled-runtime-deps-selection.ts @@ -9,6 +9,7 @@ import { collectPackageRuntimeDeps, normalizeInstallableRuntimeDepName, parseInstallableRuntimeDep, + parseInstallableRuntimeDepSpec, type RuntimeDepEntry, } from "./bundled-runtime-deps-specs.js"; import { @@ -30,6 +31,7 @@ export type BundledPluginRuntimeDepsManifest = { enabledByDefault: boolean; id?: string; legacyPluginIds: string[]; + localMemoryEmbeddingRuntimeDeps: RuntimeDepEntry[]; modelSupport?: BundledPluginRuntimeDepsModelSupport; providers: string[]; }; @@ -110,6 +112,9 @@ function readBundledPluginRuntimeDepsManifest( const manifest = readRuntimeDepsJsonObject(path.join(pluginDir, "openclaw.plugin.json")); const channels = manifest?.channels; const legacyPluginIds = manifest?.legacyPluginIds; + const localMemoryEmbeddingRuntimeDeps = readBundledPluginLocalMemoryEmbeddingRuntimeDeps( + manifest?.runtimeDependencies, + ); const modelSupport = readBundledPluginRuntimeDepsModelSupport(manifest?.modelSupport); const providers = manifest?.providers; const runtimeDepsManifest = { @@ -123,6 +128,7 @@ function readBundledPluginRuntimeDepsManifest( (entry): entry is string => typeof entry === "string" && entry !== "", ) : [], + localMemoryEmbeddingRuntimeDeps, ...(modelSupport ? { modelSupport } : {}), providers: Array.isArray(providers) ? providers.filter((entry): entry is string => typeof entry === "string" && entry !== "") @@ -132,6 +138,24 @@ function readBundledPluginRuntimeDepsManifest( return runtimeDepsManifest; } +function readBundledPluginLocalMemoryEmbeddingRuntimeDeps(value: unknown): RuntimeDepEntry[] { + if (!isRecord(value)) { + return []; + } + const specs = value.localMemoryEmbedding; + if (!Array.isArray(specs)) { + return []; + } + return specs.map((spec) => { + if (typeof spec !== "string") { + throw new Error( + "openclaw.plugin.json runtimeDependencies.localMemoryEmbedding must contain strings", + ); + } + return { ...parseInstallableRuntimeDepSpec(spec), pluginIds: [] }; + }); +} + function readBundledPluginRuntimeDepsModelSupport( value: unknown, ): BundledPluginRuntimeDepsModelSupport | undefined { @@ -353,6 +377,30 @@ function collectConfiguredProviderIds(config: OpenClawConfig): Set { return collectConfiguredRuntimeDepsTargets(config).providerIds; } +function memorySearchConfigUsesProvider( + value: { enabled?: boolean; provider?: string } | undefined, + providerId: string, +): boolean { + return ( + value?.enabled !== false && normalizeOptionalLowercaseString(value?.provider) === providerId + ); +} + +function isMemoryEmbeddingProviderConfiguredForRuntimeDeps( + config: OpenClawConfig | undefined, + providerId: string, +): boolean { + if (!config) { + return false; + } + if (memorySearchConfigUsesProvider(config.agents?.defaults?.memorySearch, providerId)) { + return true; + } + return (config.agents?.list ?? []).some((agent) => + memorySearchConfigUsesProvider(agent.memorySearch, providerId), + ); +} + function matchesBundledRuntimeDepsModelSupport( manifest: BundledPluginRuntimeDepsManifest, modelId: string, @@ -665,20 +713,32 @@ export function collectBundledPluginRuntimeDeps(params: { continue; } includedPluginIds.add(pluginId); + const manifest = readBundledPluginRuntimeDepsManifest(pluginDir, manifestCache); const packageJson = readRuntimeDepsJsonObject(path.join(pluginDir, "package.json")); - if (!packageJson) { - continue; - } - for (const [name, rawVersion] of Object.entries(collectPackageRuntimeDeps(packageJson))) { - const dep = parseInstallableRuntimeDep(name, rawVersion); - if (!dep) { - continue; + if (packageJson) { + for (const [name, rawVersion] of Object.entries(collectPackageRuntimeDeps(packageJson))) { + const dep = parseInstallableRuntimeDep(name, rawVersion); + if (!dep) { + continue; + } + const byVersion = versionMap.get(dep.name) ?? new Map>(); + const pluginIds = byVersion.get(dep.version) ?? new Set(); + pluginIds.add(pluginId); + byVersion.set(dep.version, pluginIds); + versionMap.set(dep.name, byVersion); + } + } + if ( + manifest.localMemoryEmbeddingRuntimeDeps.length > 0 && + isMemoryEmbeddingProviderConfiguredForRuntimeDeps(params.config, "local") + ) { + for (const dep of manifest.localMemoryEmbeddingRuntimeDeps) { + const byVersion = versionMap.get(dep.name) ?? new Map>(); + const pluginIds = byVersion.get(dep.version) ?? new Set(); + pluginIds.add(pluginId); + byVersion.set(dep.version, pluginIds); + versionMap.set(dep.name, byVersion); } - const byVersion = versionMap.get(dep.name) ?? new Map>(); - const pluginIds = byVersion.get(dep.version) ?? new Set(); - pluginIds.add(pluginId); - byVersion.set(dep.version, pluginIds); - versionMap.set(dep.name, byVersion); } } diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 5735ee31a78..14f34c846e4 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -3421,6 +3421,78 @@ describe("ensureBundledPluginRuntimeDeps", () => { ]); }); + it("installs local memory embedding runtime deps only when local memory search is configured", () => { + const packageRoot = makeTempDir(); + const pluginRoot = writeBundledPluginPackage({ + packageRoot, + pluginId: "memory-core", + deps: { chokidar: "^5.0.0", typebox: "1.1.34" }, + runtimeDependencies: { + localMemoryEmbedding: ["node-llama-cpp@3.18.1"], + }, + }); + const calls: BundledRuntimeDepsInstallParams[] = []; + + const result = ensureBundledPluginRuntimeDeps({ + env: {}, + config: { + agents: { + defaults: { + memorySearch: { provider: "local" }, + }, + }, + }, + installDeps: (params) => { + calls.push(params); + }, + pluginId: "memory-core", + pluginRoot, + }); + + expect(result.installedSpecs).toEqual([ + "chokidar@^5.0.0", + "node-llama-cpp@3.18.1", + "typebox@1.1.34", + ]); + expect(calls[0]?.installSpecs).toEqual([ + "chokidar@^5.0.0", + "node-llama-cpp@3.18.1", + "typebox@1.1.34", + ]); + }); + + it("does not install local memory embedding runtime deps for remote memory search", () => { + const packageRoot = makeTempDir(); + const pluginRoot = writeBundledPluginPackage({ + packageRoot, + pluginId: "memory-core", + deps: { chokidar: "^5.0.0", typebox: "1.1.34" }, + runtimeDependencies: { + localMemoryEmbedding: ["node-llama-cpp@3.18.1"], + }, + }); + const calls: BundledRuntimeDepsInstallParams[] = []; + + const result = ensureBundledPluginRuntimeDeps({ + env: {}, + config: { + agents: { + defaults: { + memorySearch: { provider: "openai" }, + }, + }, + }, + installDeps: (params) => { + calls.push(params); + }, + pluginId: "memory-core", + pluginRoot, + }); + + expect(result.installedSpecs).toEqual(["chokidar@^5.0.0", "typebox@1.1.34"]); + expect(calls[0]?.installSpecs).toEqual(["chokidar@^5.0.0", "typebox@1.1.34"]); + }); + it("repairs external staged deps even when packaged plugin-local deps are present", () => { const packageRoot = makeTempDir(); const extensionsRoot = path.join(packageRoot, "dist", "extensions"); diff --git a/src/plugins/test-helpers/bundled-runtime-deps-fixtures.ts b/src/plugins/test-helpers/bundled-runtime-deps-fixtures.ts index 98f03ce196d..5c48a2836e8 100644 --- a/src/plugins/test-helpers/bundled-runtime-deps-fixtures.ts +++ b/src/plugins/test-helpers/bundled-runtime-deps-fixtures.ts @@ -48,6 +48,7 @@ export function writeBundledPluginRuntimeDepsPackage(params: { channels?: string[]; modelSupport?: { modelPatterns?: string[]; modelPrefixes?: string[] }; providers?: string[]; + runtimeDependencies?: Record; }): string { const pluginRoot = path.join(params.packageRoot, "dist", "extensions", params.pluginId); fs.mkdirSync(pluginRoot, { recursive: true }); @@ -63,6 +64,7 @@ export function writeBundledPluginRuntimeDepsPackage(params: { ...(params.channels ? { channels: params.channels } : {}), ...(params.modelSupport ? { modelSupport: params.modelSupport } : {}), ...(params.providers ? { providers: params.providers } : {}), + ...(params.runtimeDependencies ? { runtimeDependencies: params.runtimeDependencies } : {}), }), ); return pluginRoot;