mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user