fix(plugins): reuse shared discovery cache

This commit is contained in:
Ayaan Zaidi
2026-04-17 08:28:05 +05:30
parent c6af0437c9
commit 9da4d5f5df
2 changed files with 189 additions and 83 deletions

View File

@@ -90,7 +90,7 @@ function shouldUseDiscoveryCache(env: NodeJS.ProcessEnv): boolean {
return resolveDiscoveryCacheMs(env) > 0;
}
function buildDiscoveryCacheKey(params: {
function buildScopedDiscoveryCacheKey(params: {
workspaceDir?: string;
extraPaths?: string[];
ownershipUid?: number | null;
@@ -102,10 +102,19 @@ function buildDiscoveryCacheKey(params: {
env: params.env,
});
const workspaceKey = roots.workspace ?? "";
const ownershipUid = params.ownershipUid ?? currentUid();
return `scoped::${workspaceKey}::${ownershipUid ?? "none"}::${JSON.stringify(loadPaths)}`;
}
function buildSharedDiscoveryCacheKey(params: {
ownershipUid?: number | null;
env: NodeJS.ProcessEnv;
}): string {
const roots = resolvePluginSourceRoots({ env: params.env });
const configExtensionsRoot = roots.global ?? "";
const bundledRoot = roots.stock ?? "";
const ownershipUid = params.ownershipUid ?? currentUid();
return `${workspaceKey}::${ownershipUid ?? "none"}::${configExtensionsRoot}::${bundledRoot}::${JSON.stringify(loadPaths)}`;
return `shared::${ownershipUid ?? "none"}::${configExtensionsRoot}::${bundledRoot}`;
}
function currentUid(overrideUid?: number | null): number | null {
@@ -344,6 +353,49 @@ function resolvesToSameDirectory(left?: string, right?: string): boolean {
return path.resolve(left) === path.resolve(right);
}
function createDiscoveryResult(): PluginDiscoveryResult {
return {
candidates: [],
diagnostics: [],
};
}
function mergeDiscoveryResult(
target: PluginDiscoveryResult,
source: PluginDiscoveryResult,
seenSources: Set<string>,
): void {
for (const candidate of source.candidates) {
const key = candidate.source;
if (seenSources.has(key)) {
continue;
}
seenSources.add(key);
target.candidates.push(candidate);
}
target.diagnostics.push(...source.diagnostics);
}
function getCachedDiscoveryResult(params: {
cacheEnabled: boolean;
cacheKey: string;
env: NodeJS.ProcessEnv;
load: () => PluginDiscoveryResult;
}): PluginDiscoveryResult {
const ttl = resolveDiscoveryCacheMs(params.env);
if (params.cacheEnabled) {
const cached = discoveryCache.get(params.cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.result;
}
}
const result = params.load();
if (params.cacheEnabled && ttl > 0) {
discoveryCache.set(params.cacheKey, { expiresAt: Date.now() + ttl, result });
}
return result;
}
function readPackageManifest(dir: string, rejectHardlinks = true): PackageManifest | null {
const manifestPath = path.join(dir, "package.json");
const opened = openBoundaryFileSync({
@@ -869,91 +921,95 @@ export function discoverOpenClawPlugins(params: {
}): PluginDiscoveryResult {
const env = params.env ?? process.env;
const cacheEnabled = params.cache !== false && shouldUseDiscoveryCache(env);
const cacheKey = buildDiscoveryCacheKey({
workspaceDir: params.workspaceDir,
extraPaths: params.extraPaths,
ownershipUid: params.ownershipUid,
env,
});
if (cacheEnabled) {
const cached = discoveryCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.result;
}
}
const candidates: PluginCandidate[] = [];
const diagnostics: PluginDiagnostic[] = [];
const seen = new Set<string>();
const workspaceDir = normalizeOptionalString(params.workspaceDir);
const workspaceRoot = workspaceDir ? resolveUserPath(workspaceDir, env) : undefined;
const roots = resolvePluginSourceRoots({ workspaceDir: workspaceRoot, env });
const extra = params.extraPaths ?? [];
for (const extraPath of extra) {
if (typeof extraPath !== "string") {
continue;
}
const trimmed = extraPath.trim();
if (!trimmed) {
continue;
}
discoverFromPath({
rawPath: trimmed,
origin: "config",
const scopedResult = getCachedDiscoveryResult({
cacheEnabled,
cacheKey: buildScopedDiscoveryCacheKey({
workspaceDir: params.workspaceDir,
extraPaths: params.extraPaths,
ownershipUid: params.ownershipUid,
workspaceDir,
env,
candidates,
diagnostics,
seen,
});
}
const workspaceMatchesBundledRoot = resolvesToSameDirectory(workspaceRoot, roots.stock);
if (roots.workspace && workspaceRoot && !workspaceMatchesBundledRoot) {
// Keep workspace auto-discovery constrained to the OpenClaw extensions root.
// Recursively scanning the full workspace treats arbitrary project folders as
// plugin candidates and causes noisy "plugin manifest not found" validation failures.
discoverInDirectory({
dir: roots.workspace,
origin: "workspace",
ownershipUid: params.ownershipUid,
workspaceDir: workspaceRoot,
candidates,
diagnostics,
seen,
});
}
if (roots.stock) {
discoverInDirectory({
dir: roots.stock,
origin: "bundled",
ownershipUid: params.ownershipUid,
candidates,
diagnostics,
seen,
});
}
// Keep auto-discovered global extensions behind bundled plugins.
// Users can still intentionally override via plugins.load.paths (origin=config).
discoverInDirectory({
dir: roots.global,
origin: "global",
ownershipUid: params.ownershipUid,
candidates,
diagnostics,
seen,
}),
env,
load: () => {
const result = createDiscoveryResult();
const seen = new Set<string>();
const extra = params.extraPaths ?? [];
for (const extraPath of extra) {
if (typeof extraPath !== "string") {
continue;
}
const trimmed = extraPath.trim();
if (!trimmed) {
continue;
}
discoverFromPath({
rawPath: trimmed,
origin: "config",
ownershipUid: params.ownershipUid,
workspaceDir,
env,
candidates: result.candidates,
diagnostics: result.diagnostics,
seen,
});
}
const workspaceMatchesBundledRoot = resolvesToSameDirectory(workspaceRoot, roots.stock);
if (roots.workspace && workspaceRoot && !workspaceMatchesBundledRoot) {
// Keep workspace auto-discovery constrained to the OpenClaw extensions root.
// Recursively scanning the full workspace treats arbitrary project folders as
// plugin candidates and causes noisy "plugin manifest not found" validation failures.
discoverInDirectory({
dir: roots.workspace,
origin: "workspace",
ownershipUid: params.ownershipUid,
workspaceDir: workspaceRoot,
candidates: result.candidates,
diagnostics: result.diagnostics,
seen,
});
}
return result;
},
});
const result = { candidates, diagnostics };
if (cacheEnabled) {
const ttl = resolveDiscoveryCacheMs(env);
if (ttl > 0) {
discoveryCache.set(cacheKey, { expiresAt: Date.now() + ttl, result });
}
}
const sharedResult = getCachedDiscoveryResult({
cacheEnabled,
cacheKey: buildSharedDiscoveryCacheKey({
ownershipUid: params.ownershipUid,
env,
}),
env,
load: () => {
const result = createDiscoveryResult();
const seen = new Set<string>();
if (roots.stock) {
discoverInDirectory({
dir: roots.stock,
origin: "bundled",
ownershipUid: params.ownershipUid,
candidates: result.candidates,
diagnostics: result.diagnostics,
seen,
});
}
// Keep auto-discovered global extensions behind bundled plugins.
// Users can still intentionally override via plugins.load.paths (origin=config).
discoverInDirectory({
dir: roots.global,
origin: "global",
ownershipUid: params.ownershipUid,
candidates: result.candidates,
diagnostics: result.diagnostics,
seen,
});
return result;
},
});
const result = createDiscoveryResult();
const seenSources = new Set<string>();
mergeDiscoveryResult(result, scopedResult, seenSources);
mergeDiscoveryResult(result, sharedResult, seenSources);
return result;
}