fix: cache external plugin catalog lookups in auto-enable (#66246) (thanks @yfge)

* fix: cache external plugin catalog lookups in auto-enable

Fixes openclaw/openclaw#66159

* test: restore readFileSync spy in plugin auto-enable test

* refactor: distill plugin auto-enable cache path

* fix: cache external plugin catalog lookups in auto-enable (#66246) (thanks @yfge)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
拐爷&&老拐瘦
2026-04-14 12:11:02 +08:00
committed by GitHub
parent 55604a9a91
commit 852484965f
4 changed files with 99 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
- Agents/gateway-tool: reject `config.patch` and `config.apply` calls from the model-facing gateway tool when they would newly enable any flag enumerated by `openclaw security audit` (for example `dangerouslyDisableDeviceAuth`, `allowInsecureAuth`, `dangerouslyAllowHostHeaderOriginFallback`, `hooks.gmail.allowUnsafeExternalContent`, `tools.exec.applyPatch.workspaceOnly: false`); already-enabled flags pass through unchanged so non-dangerous edits in the same patch still apply, and direct authenticated operator RPC behavior is unchanged. (#62006) Thanks @eleqtrizit.
- Telegram/forum topics: persist learned topic names to the Telegram session sidecar store so agent context can keep using human topic names after a restart instead of relearning from future service metadata. (#66107) Thanks @obviyus.
- Doctor/systemd: keep `openclaw doctor --repair` and service reinstall from re-embedding dotenv-backed secrets in user systemd units, while preserving newer inline overrides over stale state-dir `.env` values. (#66249) Thanks @tmimmanuel.
- Doctor/plugins: cache external `preferOver` catalog lookups within each plugin auto-enable pass so large `agents.list` configs no longer peg CPU and repeatedly reread plugin catalogs during doctor/plugins resolution. (#66246) Thanks @yfge.
## 2026.4.14-beta.1

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
applyPluginAutoEnable,
materializePluginAutoEnableCandidates,
@@ -104,6 +104,83 @@ describe("applyPluginAutoEnable channels", () => {
expect(result.config.plugins?.entries?.["env-primary"]).toBeUndefined();
});
it("memoizes external catalog preferOver lookups within one auto-enable pass", () => {
const stateDir = makeTempDir();
const catalogPath = path.join(stateDir, "plugins", "catalog.json");
fs.mkdirSync(path.dirname(catalogPath), { recursive: true });
fs.writeFileSync(
catalogPath,
JSON.stringify({
entries: [
{
name: "@openclaw/env-primary",
openclaw: {
channel: {
id: "env-primary",
label: "Env Primary",
selectionLabel: "Env Primary",
docsPath: "/channels/env-primary",
blurb: "Env primary entry",
},
install: {
npmSpec: "@openclaw/env-primary",
},
},
},
{
name: "@openclaw/env-secondary",
openclaw: {
channel: {
id: "env-secondary",
label: "Env Secondary",
selectionLabel: "Env Secondary",
docsPath: "/channels/env-secondary",
blurb: "Env secondary entry",
preferOver: ["env-primary"],
},
install: {
npmSpec: "@openclaw/env-secondary",
},
},
},
],
}),
"utf-8",
);
const readFileSpy = vi.spyOn(fs, "readFileSync");
try {
materializePluginAutoEnableCandidates({
config: {
channels: {
"env-primary": { token: "primary" },
"env-secondary": { token: "secondary" },
},
},
candidates: Array.from({ length: 20 }, (_, index) => ({
pluginId: index % 2 === 0 ? "env-primary" : "env-secondary",
kind: "channel-configured" as const,
channelId: index % 2 === 0 ? "env-primary" : "env-secondary",
})),
env: {
...makeIsolatedEnv(),
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
},
manifestRegistry: makeRegistry([]),
});
expect(
readFileSpy.mock.calls.filter(([filePath]) =>
String(filePath).endsWith("plugins/catalog.json"),
),
).toHaveLength(2);
} finally {
readFileSpy.mockRestore();
}
});
describe("third-party channel plugins (pluginId ≠ channelId)", () => {
it("uses the plugin manifest id, not the channel id, for plugins.entries", () => {
const result = applyWithApnChannelConfig();

View File

@@ -122,6 +122,10 @@ function resolvePreferredOverIds(
return resolveExternalCatalogPreferOver(channelId, env);
}
function getPluginAutoEnableCandidateCacheKey(candidate: PluginAutoEnableCandidate): string {
return `${candidate.pluginId}:${candidate.kind === "channel-configured" ? candidate.channelId : candidate.pluginId}`;
}
export function shouldSkipPreferredPluginAutoEnable(params: {
config: OpenClawConfig;
entry: PluginAutoEnableCandidate;
@@ -130,7 +134,19 @@ export function shouldSkipPreferredPluginAutoEnable(params: {
registry: PluginManifestRegistry;
isPluginDenied: (config: OpenClawConfig, pluginId: string) => boolean;
isPluginExplicitlyDisabled: (config: OpenClawConfig, pluginId: string) => boolean;
preferOverCache: Map<string, string[]>;
}): boolean {
const getPreferredOverIds = (candidate: PluginAutoEnableCandidate): string[] => {
const cacheKey = getPluginAutoEnableCandidateCacheKey(candidate);
const cached = params.preferOverCache.get(cacheKey);
if (cached) {
return cached;
}
const resolved = resolvePreferredOverIds(candidate, params.env, params.registry);
params.preferOverCache.set(cacheKey, resolved);
return resolved;
};
for (const other of params.configured) {
if (other.pluginId === params.entry.pluginId) {
continue;
@@ -141,9 +157,7 @@ export function shouldSkipPreferredPluginAutoEnable(params: {
) {
continue;
}
if (
resolvePreferredOverIds(other, params.env, params.registry).includes(params.entry.pluginId)
) {
if (getPreferredOverIds(other).includes(params.entry.pluginId)) {
return true;
}
}

View File

@@ -660,6 +660,8 @@ export function materializePluginAutoEnableCandidatesInternal(params: {
return { config: next, changes, autoEnabledReasons: {} };
}
const preferOverCache = new Map<string, string[]>();
for (const entry of params.candidates) {
const builtInChannelId = normalizeChatChannelId(entry.pluginId);
if (isPluginDenied(next, entry.pluginId) || isPluginExplicitlyDisabled(next, entry.pluginId)) {
@@ -674,6 +676,7 @@ export function materializePluginAutoEnableCandidatesInternal(params: {
registry: params.manifestRegistry,
isPluginDenied,
isPluginExplicitlyDisabled,
preferOverCache,
})
) {
continue;