From 9de2bc6ffcc4f8d624fa819381ca419fcbde15f8 Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 16:18:55 +0100 Subject: [PATCH] refactor: reuse startup plugin metadata snapshot --- CHANGELOG.md | 1 + src/config/io.ts | 24 +++++++++++ src/config/types.openclaw.ts | 2 + .../validation.channel-metadata.test.ts | 27 ++++++++++++ src/config/validation.ts | 10 +++++ src/gateway/server-startup-plugins.test.ts | 42 +++++++++++++++++++ src/gateway/server-startup-plugins.ts | 3 ++ src/gateway/server.impl.ts | 1 + 8 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a173efe42..2b1aa919d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay. - Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh. +- Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd. ## 2026.4.26 diff --git a/src/config/io.ts b/src/config/io.ts index a86e96785d8..c0c50d1c4d3 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import JSON5 from "json5"; +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope-config.js"; import { ensureOwnerDisplaySecret } from "../agents/owner-display.js"; import { applyRuntimeLegacyConfigMigrations } from "../commands/doctor/shared/runtime-compat-api.js"; import { loadDotEnv } from "../infra/dotenv.js"; @@ -23,6 +24,10 @@ import { resolveInstalledPluginIndexRecordsStorePath, writePersistedInstalledPluginIndexInstallRecordsSync, } from "../plugins/installed-plugin-index-records.js"; +import { + loadPluginMetadataSnapshot, + type PluginMetadataSnapshot, +} from "../plugins/plugin-metadata-snapshot.js"; import { sanitizeTerminalText } from "../terminal/safe-text.js"; import { isRecord } from "../utils.js"; import { VERSION } from "../version.js"; @@ -1166,6 +1171,7 @@ function createConfigFileSnapshot(params: { issues: ConfigFileSnapshot["issues"]; warnings: ConfigFileSnapshot["warnings"]; legacyIssues: LegacyConfigIssue[]; + pluginMetadataSnapshot?: PluginMetadataSnapshot; }): ConfigFileSnapshot { const sourceConfig = asResolvedSourceConfig(params.sourceConfig); const runtimeConfig = asRuntimeConfig(params.runtimeConfig); @@ -1183,6 +1189,9 @@ function createConfigFileSnapshot(params: { issues: params.issues, warnings: params.warnings, legacyIssues: params.legacyIssues, + ...(params.pluginMetadataSnapshot + ? { pluginMetadataSnapshot: params.pluginMetadataSnapshot } + : {}), }; } @@ -1745,10 +1754,24 @@ export function createConfigIO( ? hashConfigRaw(installMigration.persistedRootRaw) : hash; fallbackSourceConfig = coerceConfig(effectiveConfigRaw); + let pluginMetadataSnapshot: PluginMetadataSnapshot | undefined; + const loadValidationPluginMetadataSnapshot = (config: OpenClawConfig) => { + if (pluginMetadataSnapshot) { + return pluginMetadataSnapshot; + } + const defaultAgentId = resolveDefaultAgentId(config); + pluginMetadataSnapshot = loadPluginMetadataSnapshot({ + config, + workspaceDir: resolveAgentWorkspaceDir(config, defaultAgentId), + env: deps.env, + }); + return pluginMetadataSnapshot; + }; const validated = await deps.measure("config.snapshot.read.validate", () => validateConfigObjectWithPlugins(effectiveConfigRaw, { env: deps.env, pluginValidation: overrides.pluginValidation, + loadPluginMetadataSnapshot: loadValidationPluginMetadataSnapshot, }), ); if (!validated.ok) { @@ -1789,6 +1812,7 @@ export function createConfigIO( issues: [], warnings: [...validated.warnings, ...envVarWarnings], legacyIssues: legacyResolution.sourceLegacyIssues, + pluginMetadataSnapshot, }), envSnapshotForRestore: readResolution.envSnapshotForRestore, }), diff --git a/src/config/types.openclaw.ts b/src/config/types.openclaw.ts index d167e4d1c90..9aa3cea19e4 100644 --- a/src/config/types.openclaw.ts +++ b/src/config/types.openclaw.ts @@ -1,3 +1,4 @@ +import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import type { SilentReplyPolicyShape, SilentReplyRewriteShape, @@ -184,4 +185,5 @@ export type ConfigFileSnapshot = { issues: ConfigValidationIssue[]; warnings: ConfigValidationIssue[]; legacyIssues: LegacyConfigIssue[]; + pluginMetadataSnapshot?: PluginMetadataSnapshot; }; diff --git a/src/config/validation.channel-metadata.test.ts b/src/config/validation.channel-metadata.test.ts index 0e007049a3c..5ba25caf367 100644 --- a/src/config/validation.channel-metadata.test.ts +++ b/src/config/validation.channel-metadata.test.ts @@ -239,6 +239,7 @@ describe("validateConfigObjectWithPlugins bundled allowlist compatibility", () = const result = validateConfigObjectWithPlugins( { plugins: { + allow: ["opik"], entries: { opik: { enabled: true, @@ -261,4 +262,30 @@ describe("validateConfigObjectWithPlugins bundled allowlist compatibility", () = }); } }); + + it("loads a plugin metadata snapshot once during plugin validation", () => { + const loadPluginMetadataSnapshot = vi.fn((_config: unknown) => ({ + manifestRegistry: createPluginConfigSchemaRegistry(), + })); + + const result = validateConfigObjectWithPlugins( + { + plugins: { + allow: ["opik"], + entries: { + opik: { + enabled: true, + }, + }, + }, + }, + { + loadPluginMetadataSnapshot, + }, + ); + + expect(result.ok).toBe(true); + expect(loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); + expect(mockLoadPluginManifestRegistry).not.toHaveBeenCalled(); + }); }); diff --git a/src/config/validation.ts b/src/config/validation.ts index 7176b22ca44..c8ea9eb8af6 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -711,6 +711,9 @@ type ValidateConfigWithPluginsParams = { env?: NodeJS.ProcessEnv; pluginValidation?: "full" | "skip"; pluginMetadataSnapshot?: Pick; + loadPluginMetadataSnapshot?: ( + config: OpenClawConfig, + ) => Pick; }; export function validateConfigObjectWithPlugins( @@ -722,6 +725,7 @@ export function validateConfigObjectWithPlugins( env: params?.env, pluginValidation: params?.pluginValidation ?? "full", pluginMetadataSnapshot: params?.pluginMetadataSnapshot, + loadPluginMetadataSnapshot: params?.loadPluginMetadataSnapshot, }); } @@ -734,6 +738,7 @@ export function validateConfigObjectRawWithPlugins( env: params?.env, pluginValidation: params?.pluginValidation ?? "full", pluginMetadataSnapshot: params?.pluginMetadataSnapshot, + loadPluginMetadataSnapshot: params?.loadPluginMetadataSnapshot, }); } @@ -810,6 +815,11 @@ function validateConfigObjectWithPluginsBase( }; const loadValidationRegistry = (): RegistryInfo => { + const pluginMetadataSnapshot = opts.loadPluginMetadataSnapshot?.(config); + if (pluginMetadataSnapshot) { + registryInfo = { registry: pluginMetadataSnapshot.manifestRegistry }; + return registryInfo; + } const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const registry = loadPluginManifestRegistryForPluginRegistry({ config, diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index 5e2bdd6e2eb..15dfeaec207 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; const applyPluginAutoEnable = vi.hoisted(() => vi.fn((params: { config: unknown }) => ({ @@ -22,6 +23,45 @@ const resolveBundledRuntimeDependencyPackageInstallRoot = vi.hoisted(() => vi.fn((_packageRoot: string, _params: unknown) => "/runtime"), ); const pluginManifestRegistry = vi.hoisted(() => ({ plugins: [], diagnostics: [] })); +const pluginMetadataSnapshot = vi.hoisted( + (): PluginMetadataSnapshot => ({ + index: { + version: 1, + hostContractVersion: "test", + compatRegistryVersion: "test", + migrationVersion: 1, + policyHash: "policy", + generatedAtMs: 0, + installRecords: {}, + plugins: [], + diagnostics: [], + }, + registryDiagnostics: [], + manifestRegistry: pluginManifestRegistry, + plugins: [], + diagnostics: [], + byPluginId: new Map(), + normalizePluginId: (pluginId) => pluginId, + owners: { + channels: new Map(), + channelConfigs: new Map(), + providers: new Map(), + modelCatalogProviders: new Map(), + cliBackends: new Map(), + setupProviders: new Map(), + commandAliases: new Map(), + contracts: new Map(), + }, + metrics: { + registrySnapshotMs: 0, + manifestRegistryMs: 0, + ownerMapsMs: 0, + totalMs: 0, + indexPluginCount: 0, + manifestPluginCount: 0, + }, + }), +); const pluginLookUpTableMetrics = vi.hoisted(() => ({ registrySnapshotMs: 0, manifestRegistryMs: 0, @@ -259,6 +299,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { cfgAtStart: runtimeConfig, activationSourceConfig: sourceConfig, startupRuntimeConfig: runtimeConfig, + pluginMetadataSnapshot, minimalTestGateway: false, log, }); @@ -270,6 +311,7 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { expect(loadPluginLookUpTable).toHaveBeenCalledWith( expect.objectContaining({ activationSourceConfig: sourceConfig, + metadataSnapshot: pluginMetadataSnapshot, config: expect.objectContaining({ channels: expect.objectContaining({ telegram: expect.objectContaining({ diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 7a70b68569e..ea1a1f7111c 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -10,6 +10,7 @@ import { scanBundledPluginRuntimeDeps, } from "../plugins/bundled-runtime-deps.js"; import { loadPluginLookUpTable } from "../plugins/plugin-lookup-table.js"; +import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import { mergeActivationSectionsIntoRuntimeConfig } from "./plugin-activation-runtime-config.js"; @@ -95,6 +96,7 @@ export async function prepareGatewayPluginBootstrap(params: { cfgAtStart: OpenClawConfig; activationSourceConfig?: OpenClawConfig; startupRuntimeConfig: OpenClawConfig; + pluginMetadataSnapshot?: PluginMetadataSnapshot; minimalTestGateway: boolean; log: GatewayPluginBootstrapLog; }) { @@ -149,6 +151,7 @@ export async function prepareGatewayPluginBootstrap(params: { workspaceDir: defaultWorkspaceDir, env: process.env, activationSourceConfig, + metadataSnapshot: params.pluginMetadataSnapshot, }); const deferredConfiguredChannelPluginIds = [ ...(pluginLookUpTable?.startup.configuredDeferredChannelPluginIds ?? []), diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index cf25183dc79..39abd07c211 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -411,6 +411,7 @@ export async function startGatewayServer( cfgAtStart, activationSourceConfig: startupActivationSourceConfig, startupRuntimeConfig, + pluginMetadataSnapshot: configSnapshot.pluginMetadataSnapshot, minimalTestGateway, log, }),