From a3519e362ff1a370f24290a0318f39e8da892d64 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 29 Apr 2026 02:21:02 -0700 Subject: [PATCH] fix(plugins): reuse config alias scans --- CHANGELOG.md | 2 ++ src/plugins/config-state.test.ts | 22 +++++++++++++++++++++- src/plugins/config-state.ts | 23 ++++++++++++++++++++--- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb0175d04e..28e8b9df8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,11 +40,13 @@ Docs: https://docs.openclaw.ai - Agents/exec: launch zsh, bash, and fish host exec shells with startup files suppressed while preserving existing PATH fallbacks, so daemon env is not overridden by shell startup files. Carries forward #40200; fixes #40179. Thanks @NewdlDewdl. - Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc. - Plugins/QA: add a Kitchen Sink plugin gauntlet that installs the external package, checks command inventory, MCP tools, channel status, provider turns, gateway RSS, CPU, and fatal log anomalies. Thanks @vincentkoc. +- Plugins/config: reuse the bundled plugin alias scan within a single config normalization pass, so Kitchen Sink-style plugin configs no longer peg Gateway CPU by repeatedly rescanning bundled metadata before agent turns. Thanks @vincentkoc. - Plugins/channels: reject malformed runtime channel registrations that omit required config helpers before they can poison channel status. Thanks @vincentkoc. - MCP/plugins: serialize raw plugin tool return values through the plugin-tools MCP bridge so Kitchen Sink-style tools no longer surface `undefined` content. Thanks @vincentkoc. - Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit `deferralTimeoutMs: 0` indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc. - Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc. - Gateway/readiness: include an `eventLoop` diagnostic block in local or authenticated `/readyz` responses with event-loop delay (p99 and max), event-loop utilization, CPU core ratio, and a `degraded` flag, so operators can see when slow startups or runaway turns stall the event loop. Thanks @vincentkoc. +- Gateway/agents: schedule accepted agent runs after the accepted RPC frame has a chance to flush, so pre-turn prompt/context work is less likely to starve immediate `agent.wait` callers. Thanks @vincentkoc. - CLI/update: tolerate stale memory-runtime import failures during best-effort CLI process teardown, so `openclaw update` replacing hashed runtime chunks before the finalizer runs no longer surfaces as exit-time `Cannot find module` noise. Thanks @vincentkoc. - CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki. - CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007. diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index 5b36dcf0470..e5ecde9d8b6 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import * as bundledPluginMetadata from "./bundled-plugin-metadata.js"; import { createPluginActivationSource, normalizePluginsConfig, @@ -154,6 +155,25 @@ describe("normalizePluginsConfig", () => { expect(result.entries.google?.enabled).toBe(true); expect(result.entries.minimax?.enabled).toBe(false); }); + + it("reuses the bundled alias scan during one config normalization", () => { + const listBundledMetadata = vi.spyOn(bundledPluginMetadata, "listBundledPluginMetadata"); + + const result = normalizePluginsConfig({ + allow: ["unknown-plugin-one", "unknown-plugin-two"], + deny: ["unknown-plugin-three"], + entries: { + "unknown-plugin-four": { + enabled: true, + }, + }, + }); + + expect(result.allow).toEqual(["unknown-plugin-one", "unknown-plugin-two"]); + expect(result.deny).toEqual(["unknown-plugin-three"]); + expect(result.entries["unknown-plugin-four"]?.enabled).toBe(true); + expect(listBundledMetadata).toHaveBeenCalledTimes(1); + }); }); describe("resolveEffectiveEnableState", () => { diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index 9893127042e..7f33b889dde 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -18,6 +18,7 @@ import { hasExplicitPluginConfig as hasExplicitPluginConfigShared, isBundledChannelEnabledByChannelConfig as isBundledChannelEnabledByChannelConfigShared, normalizePluginsConfigWithResolver, + type NormalizePluginId, type NormalizedPluginsConfig as SharedNormalizedPluginsConfig, } from "./config-normalization-shared.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; @@ -76,20 +77,36 @@ function getBundledPluginAliasLookup(): ReadonlyMap { return bundledPluginAliasLookup; } -export function normalizePluginId(id: string): string { +function normalizePluginIdWithLookup( + id: string, + getAliasLookup: () => ReadonlyMap, +): string { const trimmed = normalizeOptionalString(id) ?? ""; const normalized = normalizeOptionalLowercaseString(trimmed) ?? ""; const builtInAlias = BUILT_IN_PLUGIN_ALIAS_LOOKUP.get(normalized); if (builtInAlias) { return builtInAlias; } - return getBundledPluginAliasLookup().get(normalized) ?? trimmed; + return getAliasLookup().get(normalized) ?? trimmed; +} + +function createScopedPluginIdNormalizer(): NormalizePluginId { + let lookup: ReadonlyMap | undefined; + return (id) => + normalizePluginIdWithLookup(id, () => { + lookup ??= getBundledPluginAliasLookup(); + return lookup; + }); +} + +export function normalizePluginId(id: string): string { + return normalizePluginIdWithLookup(id, getBundledPluginAliasLookup); } export const normalizePluginsConfig = ( config?: OpenClawConfig["plugins"], ): NormalizedPluginsConfig => { - return normalizePluginsConfigWithResolver(config, normalizePluginId); + return normalizePluginsConfigWithResolver(config, createScopedPluginIdNormalizer()); }; export function createPluginActivationSource(params: {