fix(plugins): partition bootstrap bundled root caches

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 10:08:05 -04:00
parent c526eee3d9
commit f02b9f2cf1
5 changed files with 204 additions and 47 deletions

View File

@@ -1,5 +1,6 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { listBundledChannelPluginIds } from "./bundled-ids.js";
import { listBundledChannelPluginIdsForRoot } from "./bundled-ids.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
import {
getBundledChannelPlugin,
getBundledChannelSecrets,
@@ -16,7 +17,7 @@ type CachedBootstrapPlugins = {
missingIds: Set<string>;
};
let cachedBootstrapPlugins: CachedBootstrapPlugins | null = null;
const cachedBootstrapPluginsByRoot = new Map<string, CachedBootstrapPlugins>();
function mergePluginSection<T>(
runtimeValue: T | undefined,
@@ -63,18 +64,29 @@ function mergeBootstrapPlugin(
} as ChannelPlugin;
}
function buildBootstrapPlugins(): CachedBootstrapPlugins {
function buildBootstrapPlugins(
packageRoot: string,
env: NodeJS.ProcessEnv = process.env,
): CachedBootstrapPlugins {
return {
sortedIds: listBundledChannelPluginIds(),
sortedIds: listBundledChannelPluginIdsForRoot(packageRoot, env),
byId: new Map(),
secretsById: new Map(),
missingIds: new Set(),
};
}
function getBootstrapPlugins(): CachedBootstrapPlugins {
cachedBootstrapPlugins ??= buildBootstrapPlugins();
return cachedBootstrapPlugins;
function getBootstrapPlugins(
packageRoot = resolveBundledChannelPackageRoot(),
env: NodeJS.ProcessEnv = process.env,
): CachedBootstrapPlugins {
const cached = cachedBootstrapPluginsByRoot.get(packageRoot);
if (cached) {
return cached;
}
const created = buildBootstrapPlugins(packageRoot, env);
cachedBootstrapPluginsByRoot.set(packageRoot, created);
return created;
}
export function listBootstrapChannelPluginIds(): readonly string[] {
@@ -99,7 +111,7 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi
if (!resolvedId) {
return undefined;
}
const registry = getBootstrapPlugins();
const registry = getBootstrapPlugins(resolveBundledChannelPackageRoot());
const cached = registry.byId.get(resolvedId);
if (cached) {
return cached;
@@ -126,7 +138,7 @@ export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secret
if (!resolvedId) {
return undefined;
}
const registry = getBootstrapPlugins();
const registry = getBootstrapPlugins(resolveBundledChannelPackageRoot());
const cached = registry.secretsById.get(resolvedId);
if (cached) {
return cached;
@@ -142,5 +154,5 @@ export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secret
}
export function clearBootstrapChannelPluginCache(): void {
cachedBootstrapPlugins = null;
cachedBootstrapPluginsByRoot.clear();
}

View File

@@ -1,10 +1,23 @@
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
let bundledChannelPluginIds: string[] | null = null;
const bundledChannelPluginIdsByRoot = new Map<string, string[]>();
export function listBundledChannelPluginIds(): string[] {
bundledChannelPluginIds ??= listChannelCatalogEntries({ origin: "bundled" })
export function listBundledChannelPluginIdsForRoot(
packageRoot: string,
env: NodeJS.ProcessEnv = process.env,
): string[] {
const cached = bundledChannelPluginIdsByRoot.get(packageRoot);
if (cached) {
return [...cached];
}
const loaded = listChannelCatalogEntries({ origin: "bundled", env })
.map((entry) => entry.pluginId)
.toSorted((left, right) => left.localeCompare(right));
return [...bundledChannelPluginIds];
bundledChannelPluginIdsByRoot.set(packageRoot, loaded);
return [...loaded];
}
export function listBundledChannelPluginIds(): string[] {
return listBundledChannelPluginIdsForRoot(resolveBundledChannelPackageRoot());
}

View File

@@ -0,0 +1,129 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../../test/helpers/import-fresh.ts";
const tempDirs: string[] = [];
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
function makeBundledRoot(prefix: string): { root: string; pluginsDir: string } {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(root);
const pluginsDir = path.join(root, "dist", "extensions");
fs.mkdirSync(pluginsDir, { recursive: true });
return { root, pluginsDir };
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
vi.resetModules();
vi.doUnmock("../../plugins/channel-catalog-registry.js");
vi.doUnmock("./bundled.js");
vi.doUnmock("./bundled-ids.js");
});
describe("bundled root-aware caches", () => {
it("partitions bundled channel ids by active bundled root without re-importing", async () => {
const rootA = makeBundledRoot("openclaw-bundled-ids-a-");
const rootB = makeBundledRoot("openclaw-bundled-ids-b-");
vi.doMock("../../plugins/channel-catalog-registry.js", () => ({
listChannelCatalogEntries: (params?: { env?: NodeJS.ProcessEnv }) => {
const activeRoot = params?.env?.OPENCLAW_BUNDLED_PLUGINS_DIR;
if (activeRoot === rootA.pluginsDir) {
return [{ pluginId: "alpha" }];
}
if (activeRoot === rootB.pluginsDir) {
return [{ pluginId: "beta" }];
}
return [];
},
}));
const bundledIds = await importFreshModule<typeof import("./bundled-ids.js")>(
import.meta.url,
"./bundled-ids.js?scope=root-aware-id-cache",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootA.pluginsDir;
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["alpha"]);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootB.pluginsDir;
expect(bundledIds.listBundledChannelPluginIds()).toEqual(["beta"]);
});
it("partitions bootstrap plugin caches by active bundled root without re-importing", async () => {
const rootA = makeBundledRoot("openclaw-bootstrap-a-");
const rootB = makeBundledRoot("openclaw-bootstrap-b-");
vi.doMock("./bundled-ids.js", () => ({
listBundledChannelPluginIdsForRoot: (packageRoot: string) => {
if (packageRoot === rootA.root) {
return ["alpha"];
}
if (packageRoot === rootB.root) {
return ["beta"];
}
return [];
},
}));
vi.doMock("./bundled.js", () => ({
getBundledChannelPlugin: (id: string) => ({
id,
meta: { id, label: `runtime-${id}` },
capabilities: {},
config: {},
}),
getBundledChannelSetupPlugin: (id: string) => {
const activeRoot = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const suffix =
activeRoot === rootA.pluginsDir ? "A" : activeRoot === rootB.pluginsDir ? "B" : "unknown";
return {
id,
meta: { id, label: `setup-${suffix}` },
capabilities: {},
config: {},
};
},
getBundledChannelSecrets: (id: string) => ({
secretTargetRegistryEntries: [{ id: `runtime-${id}`, targetType: "channel" }],
}),
getBundledChannelSetupSecrets: (id: string) => {
const activeRoot = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const suffix =
activeRoot === rootA.pluginsDir ? "A" : activeRoot === rootB.pluginsDir ? "B" : "unknown";
return {
secretTargetRegistryEntries: [{ id: `setup-${id}-${suffix}`, targetType: "channel" }],
};
},
}));
const bootstrapRegistry = await importFreshModule<typeof import("./bootstrap-registry.js")>(
import.meta.url,
"./bootstrap-registry.js?scope=root-aware-bootstrap-cache",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootA.pluginsDir;
expect(bootstrapRegistry.listBootstrapChannelPluginIds()).toEqual(["alpha"]);
expect(bootstrapRegistry.getBootstrapChannelPlugin("alpha")?.meta.label).toBe("setup-A");
expect(
bootstrapRegistry.getBootstrapChannelSecrets("alpha")?.secretTargetRegistryEntries?.[0]?.id,
).toBe("setup-alpha-A");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = rootB.pluginsDir;
expect(bootstrapRegistry.listBootstrapChannelPluginIds()).toEqual(["beta"]);
expect(bootstrapRegistry.getBootstrapChannelPlugin("beta")?.meta.label).toBe("setup-B");
expect(
bootstrapRegistry.getBootstrapChannelSecrets("beta")?.secretTargetRegistryEntries?.[0]?.id,
).toBe("setup-beta-B");
});
});

View File

@@ -0,0 +1,35 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
const OPENCLAW_PACKAGE_ROOT =
resolveOpenClawPackageRootSync({
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url.startsWith("file:") ? import.meta.url : undefined,
}) ??
(import.meta.url.startsWith("file:")
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
: process.cwd());
export function derivePackageRootFromBundledPluginsDir(pluginsDir: string): string {
const resolvedDir = path.resolve(pluginsDir);
if (path.basename(resolvedDir) !== "extensions") {
return resolvedDir;
}
const parentDir = path.dirname(resolvedDir);
const parentBase = path.basename(parentDir);
if (parentBase === "dist" || parentBase === "dist-runtime") {
return path.dirname(parentDir);
}
return parentDir;
}
export function resolveBundledChannelPackageRoot(env: NodeJS.ProcessEnv = process.env): string {
const bundledPluginsDir = resolveBundledPluginsDir(env);
if (bundledPluginsDir) {
return derivePackageRootFromBundledPluginsDir(bundledPluginsDir);
}
return OPENCLAW_PACKAGE_ROOT;
}

View File

@@ -1,16 +1,14 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { formatErrorMessage } from "../../infra/errors.js";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
listBundledChannelPluginMetadata,
resolveBundledChannelGeneratedPath,
type BundledChannelPluginMetadata,
} from "../../plugins/bundled-channel-runtime.js";
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
import { unwrapDefaultModuleExport } from "../../plugins/module-export.js";
import type { PluginRuntime } from "../../plugins/runtime/types.js";
import { resolveBundledChannelPackageRoot } from "./bundled-root.js";
import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js";
import type { ChannelPlugin } from "./types.plugin.js";
import type { ChannelId } from "./types.public.js";
@@ -54,36 +52,6 @@ type BundledChannelCacheContext = {
};
const log = createSubsystemLogger("channels");
const OPENCLAW_PACKAGE_ROOT =
resolveOpenClawPackageRootSync({
argv1: process.argv[1],
cwd: process.cwd(),
moduleUrl: import.meta.url.startsWith("file:") ? import.meta.url : undefined,
}) ??
(import.meta.url.startsWith("file:")
? path.resolve(fileURLToPath(new URL("../../..", import.meta.url)))
: process.cwd());
function derivePackageRootFromBundledPluginsDir(pluginsDir: string): string {
const resolvedDir = path.resolve(pluginsDir);
if (path.basename(resolvedDir) !== "extensions") {
return resolvedDir;
}
const parentDir = path.dirname(resolvedDir);
const parentBase = path.basename(parentDir);
if (parentBase === "dist" || parentBase === "dist-runtime") {
return path.dirname(parentDir);
}
return parentDir;
}
function resolveBundledChannelPackageRoot(): string {
const bundledPluginsDir = resolveBundledPluginsDir(process.env);
if (bundledPluginsDir) {
return derivePackageRootFromBundledPluginsDir(bundledPluginsDir);
}
return OPENCLAW_PACKAGE_ROOT;
}
function resolveChannelPluginModuleEntry(
moduleExport: unknown,