refactor: remove plugin dependency cleanup leftovers

This commit is contained in:
Peter Steinberger
2026-05-01 21:55:27 +01:00
parent 33e527d1fc
commit 112dedd093
21 changed files with 55 additions and 645 deletions

View File

@@ -18,7 +18,7 @@ import memoryPlugin, {
normalizeRecallQuery,
shouldCapture,
} from "./index.js";
import { createLanceDbRuntimeLoader, type LanceDbRuntimeLogger } from "./lancedb-runtime.js";
import { createLanceDbRuntimeLoader } from "./lancedb-runtime.js";
import { installTmpDirHarness } from "./test-helpers.js";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "test-key";
@@ -38,22 +38,7 @@ type MemoryPluginTestConfig = {
storageOptions?: Record<string, string>;
};
const TEST_RUNTIME_MANIFEST = {
name: "openclaw-memory-lancedb-runtime",
private: true as const,
type: "module" as const,
dependencies: {
"@lancedb/lancedb": "^0.27.1",
},
};
type LanceDbModule = typeof import("@lancedb/lancedb");
type RuntimeManifest = {
name: string;
private: true;
type: "module";
dependencies: Record<string, string>;
};
function createMockModule(): LanceDbModule {
return {
@@ -67,40 +52,19 @@ function invokeEmbeddingCreate(mock: ReturnType<typeof vi.fn>, body: unknown) {
function createRuntimeLoader(
overrides: {
env?: NodeJS.ProcessEnv;
importBundled?: () => Promise<LanceDbModule>;
importResolved?: (resolvedPath: string) => Promise<LanceDbModule>;
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
resolveRuntimeEntry?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
}) => string | null;
installRuntime?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
env: NodeJS.ProcessEnv;
logger?: LanceDbRuntimeLogger;
}) => Promise<string>;
} = {},
) {
return createLanceDbRuntimeLoader({
env: overrides.env ?? ({} as NodeJS.ProcessEnv),
platform: overrides.platform,
arch: overrides.arch,
resolveStateDir: () => "/tmp/openclaw-state",
runtimeManifest: TEST_RUNTIME_MANIFEST,
importBundled:
overrides.importBundled ??
(async () => {
throw new Error("Cannot find package '@lancedb/lancedb'");
}),
importResolved: overrides.importResolved ?? (async () => createMockModule()),
resolveRuntimeEntry: overrides.resolveRuntimeEntry ?? (() => null),
installRuntime:
overrides.installRuntime ??
(async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`),
});
}
@@ -2261,131 +2225,47 @@ describe("lancedb runtime loader", () => {
test("uses the bundled module when it is already available", async () => {
const bundledModule = createMockModule();
const importBundled = vi.fn(async () => bundledModule);
const importResolved = vi.fn(async () => createMockModule());
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(async () => "/tmp/openclaw-state/plugin-runtimes/lancedb.js");
const loader = createRuntimeLoader({
importBundled,
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(bundledModule);
expect(resolveRuntimeEntry).not.toHaveBeenCalled();
expect(installRuntime).not.toHaveBeenCalled();
expect(importResolved).not.toHaveBeenCalled();
});
test("reuses an existing user runtime install before attempting a reinstall", async () => {
const runtimeModule = createMockModule();
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(
() => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const installRuntime = vi.fn(
async () => "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/runtime-entry.js",
);
const loader = createRuntimeLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(resolveRuntimeEntry).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
}),
);
expect(installRuntime).not.toHaveBeenCalled();
});
test("installs LanceDB into user state when the bundled runtime is unavailable", async () => {
const runtimeModule = createMockModule();
const logger: LanceDbRuntimeLogger = {
warn: vi.fn(),
info: vi.fn(),
};
const importResolved = vi.fn(async () => runtimeModule);
const resolveRuntimeEntry = vi.fn(() => null);
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
importResolved,
resolveRuntimeEntry,
installRuntime,
});
await expect(loader.load(logger)).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledWith(
expect.objectContaining({
runtimeDir: "/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
manifest: TEST_RUNTIME_MANIFEST,
}),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
"installing runtime deps under /tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb",
),
);
});
test("fails fast in nix mode instead of attempting auto-install", async () => {
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
env: { OPENCLAW_NIX_MODE: "1" } as NodeJS.ProcessEnv,
installRuntime,
});
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: failed to load LanceDB and Nix mode disables auto-install.",
);
expect(installRuntime).not.toHaveBeenCalled();
expect(importBundled).toHaveBeenCalledTimes(1);
});
test("fails clearly on Intel macOS instead of attempting an unsupported native install", async () => {
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
platform: "darwin",
arch: "x64",
installRuntime,
});
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: LanceDB runtime is unavailable on darwin-x64.",
);
expect(installRuntime).not.toHaveBeenCalled();
});
test("clears the cached failure so later calls can retry the install", async () => {
test("fails fast when package dependencies are missing", async () => {
const loader = createRuntimeLoader();
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: bundled @lancedb/lancedb dependency is unavailable.",
);
});
test("clears the cached failure so later calls can retry the package import", async () => {
const runtimeModule = createMockModule();
const installRuntime = vi
const importBundled = vi
.fn()
.mockRejectedValueOnce(new Error("network down"))
.mockResolvedValueOnce(
"/tmp/openclaw-state/plugin-runtimes/memory-lancedb/lancedb/node_modules/@lancedb/lancedb/index.js",
);
const importResolved = vi.fn(async () => runtimeModule);
.mockResolvedValueOnce(runtimeModule);
const loader = createRuntimeLoader({
installRuntime,
importResolved,
importBundled,
});
await expect(loader.load()).rejects.toThrow("network down");
await expect(loader.load()).resolves.toBe(runtimeModule);
expect(installRuntime).toHaveBeenCalledTimes(2);
expect(importBundled).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,87 +0,0 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveLanceDbDependencySpec } from "./lancedb-runtime.js";
function mapReader(
entries: ReadonlyArray<[string, { dependencies?: Record<string, string> } | null]>,
): (manifestPath: string) => { dependencies?: Record<string, string> } | 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"',
);
});
});

View File

@@ -1,11 +1,3 @@
import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { resolveStateDir } from "./api.js";
type LanceDbModule = typeof import("@lancedb/lancedb");
export type LanceDbRuntimeLogger = {
@@ -13,211 +5,18 @@ export type LanceDbRuntimeLogger = {
warn?: (message: string) => void;
};
type RuntimeManifest = {
name: string;
private: true;
type: "module";
dependencies: Record<string, string>;
};
type PackageJsonWithDependencies = {
dependencies?: Record<string, string>;
};
type ReadPackageJson = (manifestPath: string) => PackageJsonWithDependencies | null;
type LanceDbRuntimeLoaderDeps = {
env: NodeJS.ProcessEnv;
platform: NodeJS.Platform;
arch: NodeJS.Architecture;
resolveStateDir: (env?: NodeJS.ProcessEnv, homedir?: () => string) => string;
runtimeManifest: RuntimeManifest;
importBundled: () => Promise<LanceDbModule>;
importResolved: (resolvedPath: string) => Promise<LanceDbModule>;
resolveRuntimeEntry: (params: { runtimeDir: string; manifest: RuntimeManifest }) => string | null;
installRuntime: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
env: NodeJS.ProcessEnv;
logger?: LanceDbRuntimeLogger;
}) => Promise<string>;
};
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<string>();
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,
type: "module",
dependencies: {
"@lancedb/lancedb": lanceDbSpec,
},
};
})();
function resolveRuntimeDir(stateDir: string): string {
return path.join(stateDir, "plugin-runtimes", "memory-lancedb", "lancedb");
}
function readRuntimeManifest(filePath: string): RuntimeManifest | null {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as RuntimeManifest;
} catch {
return null;
}
}
function manifestsMatch(actual: RuntimeManifest | null, expected: RuntimeManifest): boolean {
if (!actual) {
return false;
}
return JSON.stringify(actual) === JSON.stringify(expected);
}
function defaultResolveRuntimeEntry(params: {
runtimeDir: string;
manifest: RuntimeManifest;
}): string | null {
const runtimePackagePath = path.join(params.runtimeDir, "package.json");
if (!manifestsMatch(readRuntimeManifest(runtimePackagePath), params.manifest)) {
return null;
}
try {
const runtimeRequire = createRequire(runtimePackagePath);
return runtimeRequire.resolve("@lancedb/lancedb");
} catch {
return null;
}
}
function collectSpawnOutput(params: {
command: string;
args: string[];
cwd: string;
env: NodeJS.ProcessEnv;
}): Promise<{ code: number | null; stdout: string; stderr: string; error?: Error }> {
return new Promise((resolve) => {
const child = spawn(params.command, params.args, {
cwd: params.cwd,
env: params.env,
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer | string) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer | string) => {
stderr += chunk.toString();
});
child.on("error", (error) => {
resolve({ code: null, stdout, stderr, error });
});
child.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
async function defaultInstallRuntime(params: {
runtimeDir: string;
manifest: RuntimeManifest;
env: NodeJS.ProcessEnv;
logger?: LanceDbRuntimeLogger;
}): Promise<string> {
const runtimePackagePath = path.join(params.runtimeDir, "package.json");
const currentManifest = readRuntimeManifest(runtimePackagePath);
if (!manifestsMatch(currentManifest, params.manifest)) {
await fs.promises.rm(path.join(params.runtimeDir, "node_modules"), {
recursive: true,
force: true,
});
await fs.promises.rm(path.join(params.runtimeDir, "package-lock.json"), { force: true });
}
await fs.promises.mkdir(params.runtimeDir, { recursive: true });
await fs.promises.writeFile(
runtimePackagePath,
`${JSON.stringify(params.manifest, null, 2)}\n`,
"utf8",
);
const install = await collectSpawnOutput({
command: "npm",
args: ["install", "--omit=dev", "--silent", "--ignore-scripts", "--package-lock=false"],
cwd: params.runtimeDir,
env: params.env,
});
if (install.error) {
const spawnError = install.error as NodeJS.ErrnoException;
throw new Error(
spawnError.code === "ENOENT"
? "npm is required to install the LanceDB runtime but was not found on PATH"
: install.error.message,
);
}
if ((install.code ?? 0) !== 0) {
const detail = install.stderr.trim() || install.stdout.trim();
throw new Error(detail || `npm exited with code ${install.code ?? "unknown"}`);
}
const resolved = defaultResolveRuntimeEntry({
runtimeDir: params.runtimeDir,
manifest: params.manifest,
});
if (!resolved) {
throw new Error("installed LanceDB runtime is missing the @lancedb/lancedb entry");
}
params.logger?.info?.(`memory-lancedb: installed LanceDB runtime under ${params.runtimeDir}`);
return resolved;
}
function defaultImportResolved(resolvedPath: string): Promise<LanceDbModule> {
return import(pathToFileURL(resolvedPath).href);
}
function buildLoadFailureMessage(prefix: string, error: unknown): string {
return `memory-lancedb: ${prefix}. ${String(error)}`;
function buildLoadFailureMessage(error: unknown): string {
return [
"memory-lancedb: bundled @lancedb/lancedb dependency is unavailable.",
"Install or repair the memory-lancedb plugin package dependencies, then restart OpenClaw.",
String(error),
].join(" ");
}
function isUnsupportedNativePlatform(params: {
@@ -239,87 +38,31 @@ function buildUnsupportedNativePlatformMessage(params: {
}
export function createLanceDbRuntimeLoader(overrides: Partial<LanceDbRuntimeLoaderDeps> = {}): {
load: (logger?: LanceDbRuntimeLogger) => Promise<LanceDbModule>;
load: (_logger?: LanceDbRuntimeLogger) => Promise<LanceDbModule>;
} {
const deps: LanceDbRuntimeLoaderDeps = {
env: overrides.env ?? process.env,
platform: overrides.platform ?? process.platform,
arch: overrides.arch ?? process.arch,
resolveStateDir: overrides.resolveStateDir ?? resolveStateDir,
runtimeManifest: overrides.runtimeManifest ?? MEMORY_LANCEDB_RUNTIME_MANIFEST,
importBundled: overrides.importBundled ?? (() => import("@lancedb/lancedb")),
importResolved: overrides.importResolved ?? defaultImportResolved,
resolveRuntimeEntry: overrides.resolveRuntimeEntry ?? defaultResolveRuntimeEntry,
installRuntime: overrides.installRuntime ?? defaultInstallRuntime,
};
let loadPromise: Promise<LanceDbModule> | null = null;
return {
async load(logger?: LanceDbRuntimeLogger): Promise<LanceDbModule> {
async load(_logger?: LanceDbRuntimeLogger): Promise<LanceDbModule> {
if (!loadPromise) {
loadPromise = (async () => {
try {
return await deps.importBundled();
} catch (bundledError) {
if (isUnsupportedNativePlatform({ platform: deps.platform, arch: deps.arch })) {
throw new Error(
buildUnsupportedNativePlatformMessage({
platform: deps.platform,
arch: deps.arch,
}),
{ cause: bundledError },
);
}
const runtimeDir = resolveRuntimeDir(
deps.resolveStateDir(deps.env, () =>
deps.env.HOME?.trim() ? deps.env.HOME : os.homedir(),
),
);
const existingRuntime = deps.resolveRuntimeEntry({
runtimeDir,
manifest: deps.runtimeManifest,
});
if (existingRuntime) {
try {
return await deps.importResolved(existingRuntime);
} catch {
// Reinstall below when the cached runtime is incomplete or stale.
}
}
if (deps.env.OPENCLAW_NIX_MODE === "1") {
throw new Error(
buildLoadFailureMessage(
"failed to load LanceDB and Nix mode disables auto-install",
bundledError,
),
{ cause: bundledError },
);
}
logger?.warn?.(
`memory-lancedb: bundled LanceDB runtime unavailable (${String(bundledError)}); installing runtime deps under ${runtimeDir}`,
);
const installedEntry = await deps.installRuntime({
runtimeDir,
manifest: deps.runtimeManifest,
env: deps.env,
logger,
});
try {
return await deps.importResolved(installedEntry);
} catch (runtimeError) {
throw new Error(
buildLoadFailureMessage(
"failed to load LanceDB after installing runtime deps",
runtimeError,
),
{ cause: runtimeError },
);
}
}
})().catch((error) => {
loadPromise = deps.importBundled().catch((error) => {
loadPromise = null;
throw error;
if (isUnsupportedNativePlatform({ platform: deps.platform, arch: deps.arch })) {
throw new Error(
buildUnsupportedNativePlatformMessage({
platform: deps.platform,
arch: deps.arch,
}),
{ cause: error },
);
}
throw new Error(buildLoadFailureMessage(error), { cause: error });
});
}
return await loadPromise;