diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bcea9d800..9fefd156876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai - Plugins/ClawHub: sanitize temporary archive filenames for scoped package names and slash-containing skill slugs so `openclaw plugins install @scope/name` no longer fails with `ENOENT` during archive download. (#56452) Thanks @soimy. - Telegram/polling: keep the watchdog from aborting long-running reply delivery by treating recent non-polling API activity as bounded liveness instead of a hard stall. (#56343) Thanks @openperf. - Memory/FTS: keep provider-less keyword hits visible at the default memory-search threshold, so FTS-only recall works without requiring `--min-score 0`. (#56473) Thanks @opriz. +- Memory/LanceDB: resolve runtime dependency manifest lookup from the bundled `extensions/memory-lancedb` path (including flattened dist chunks) so startup no longer fails with a missing `@lancedb/lancedb` dependency error. (#56623) Thanks @LUKSOAgent. ## 2026.3.28 diff --git a/extensions/memory-lancedb/lancedb-runtime.test.ts b/extensions/memory-lancedb/lancedb-runtime.test.ts new file mode 100644 index 00000000000..a4b819b7c52 --- /dev/null +++ b/extensions/memory-lancedb/lancedb-runtime.test.ts @@ -0,0 +1,87 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { resolveLanceDbDependencySpec } from "./lancedb-runtime.js"; + +function mapReader( + entries: ReadonlyArray<[string, { dependencies?: Record } | null]>, +): (manifestPath: string) => { dependencies?: Record } | null { + const byPath = new Map( + entries.map(([manifestPath, value]) => [path.normalize(manifestPath), value]), + ); + return (manifestPath: string) => byPath.get(path.normalize(manifestPath)) ?? null; +} + +describe("resolveLanceDbDependencySpec", () => { + it("reads dependency from source-layout sibling manifest", () => { + const modulePath = path.join("/repo/extensions/memory-lancedb", "lancedb-runtime.js"); + const packagePath = path.join("/repo/extensions/memory-lancedb", "package.json"); + const readPackageJson = mapReader([ + [ + packagePath, + { + dependencies: { "@lancedb/lancedb": "^0.27.1" }, + }, + ], + ]); + + expect(resolveLanceDbDependencySpec(modulePath, readPackageJson)).toBe("^0.27.1"); + }); + + it("falls back to dist/extensions memory-lancedb manifest for flattened bundles", () => { + const modulePath = path.join( + "/usr/lib/node_modules/openclaw/dist", + "lancedb-runtime-3m75WU-W.js", + ); + const distPackagePath = path.join("/usr/lib/node_modules/openclaw/dist", "package.json"); + const extensionPackagePath = path.join( + "/usr/lib/node_modules/openclaw/dist/extensions/memory-lancedb", + "package.json", + ); + const readPackageJson = mapReader([ + [distPackagePath, { dependencies: {} }], + [ + extensionPackagePath, + { + dependencies: { "@lancedb/lancedb": "^0.27.1" }, + }, + ], + ]); + + expect(resolveLanceDbDependencySpec(modulePath, readPackageJson)).toBe("^0.27.1"); + }); + + it("walks parent directories to support nested dist chunk paths", () => { + const modulePath = path.join( + "/usr/lib/node_modules/openclaw/dist/chunks/runtime", + "lancedb-runtime-3m75WU-W.js", + ); + const extensionPackagePath = path.join( + "/usr/lib/node_modules/openclaw/dist/extensions/memory-lancedb", + "package.json", + ); + const readPackageJson = mapReader([ + [ + extensionPackagePath, + { + dependencies: { "@lancedb/lancedb": "0.27.2" }, + }, + ], + ]); + + expect(resolveLanceDbDependencySpec(modulePath, readPackageJson)).toBe("0.27.2"); + }); + + it("throws when no candidate package manifest declares @lancedb/lancedb", () => { + const modulePath = path.join( + "/usr/lib/node_modules/openclaw/dist", + "lancedb-runtime-3m75WU-W.js", + ); + const readPackageJson = mapReader([ + [path.join("/usr/lib/node_modules/openclaw/dist", "package.json"), null], + ]); + + expect(() => resolveLanceDbDependencySpec(modulePath, readPackageJson)).toThrow( + 'memory-lancedb package.json is missing "@lancedb/lancedb"', + ); + }); +}); diff --git a/extensions/memory-lancedb/lancedb-runtime.ts b/extensions/memory-lancedb/lancedb-runtime.ts index d16b42d66c4..369936e213e 100644 --- a/extensions/memory-lancedb/lancedb-runtime.ts +++ b/extensions/memory-lancedb/lancedb-runtime.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { resolveStateDir } from "./api.js"; type LanceDbModule = typeof import("@lancedb/lancedb"); @@ -20,6 +20,12 @@ type RuntimeManifest = { dependencies: Record; }; +type PackageJsonWithDependencies = { + dependencies?: Record; +}; + +type ReadPackageJson = (manifestPath: string) => PackageJsonWithDependencies | null; + type LanceDbRuntimeLoaderDeps = { env: NodeJS.ProcessEnv; resolveStateDir: (env?: NodeJS.ProcessEnv, homedir?: () => string) => string; @@ -35,16 +41,47 @@ type LanceDbRuntimeLoaderDeps = { }) => Promise; }; -const MEMORY_LANCEDB_RUNTIME_MANIFEST: RuntimeManifest = (() => { - const packageJson = JSON.parse( - fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"), - ) as { - dependencies?: Record; - }; - const lanceDbSpec = packageJson.dependencies?.["@lancedb/lancedb"]; - if (!lanceDbSpec) { - throw new Error('memory-lancedb package.json is missing "@lancedb/lancedb"'); +function defaultReadPackageJson(manifestPath: string): PackageJsonWithDependencies | null { + try { + return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as PackageJsonWithDependencies; + } catch { + return null; } +} + +function buildMemoryLanceDbManifestCandidates(modulePath: string): string[] { + const moduleDir = path.dirname(modulePath); + const candidates = new Set(); + candidates.add(path.join(moduleDir, "package.json")); + + let cursor = moduleDir; + while (true) { + candidates.add(path.join(cursor, "extensions", "memory-lancedb", "package.json")); + const parent = path.dirname(cursor); + if (parent === cursor) { + break; + } + cursor = parent; + } + + return [...candidates]; +} + +export function resolveLanceDbDependencySpec( + modulePath: string, + readPackageJson: ReadPackageJson = defaultReadPackageJson, +): string { + for (const manifestPath of buildMemoryLanceDbManifestCandidates(modulePath)) { + const lanceDbSpec = readPackageJson(manifestPath)?.dependencies?.["@lancedb/lancedb"]; + if (lanceDbSpec) { + return lanceDbSpec; + } + } + throw new Error('memory-lancedb package.json is missing "@lancedb/lancedb"'); +} + +const MEMORY_LANCEDB_RUNTIME_MANIFEST: RuntimeManifest = (() => { + const lanceDbSpec = resolveLanceDbDependencySpec(fileURLToPath(import.meta.url)); return { name: "openclaw-memory-lancedb-runtime", private: true,