fix(plugins): reuse cold inspect registry snapshots

This commit is contained in:
Vincent Koc
2026-05-01 04:44:27 -07:00
parent d9401c7deb
commit dcd34fbc37
9 changed files with 147 additions and 24 deletions

View File

@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
- Discord/Slack: defer status-reaction cleanup until run finalization so queued, thinking, tool, and terminal reactions no longer flicker during normal progress updates. (#75582)
- Discord/voice: leave Discord voice off for text-only configs unless `channels.discord.voice` is explicitly configured, avoiding default `GuildVoiceStates` traffic and idle gateway CPU pressure for bots that do not use `/vc`. Fixes #73753; refs #74044. Thanks @sanchezm86 and @SecureCloudProjO.
- Discord/voice: rerun configured voice auto-join after Discord gateway RESUMED events and ignore already-destroyed stale voice connections during reconnect cleanup, so health-monitor account restarts can rejoin configured channels. Fixes #40665. Thanks @liz709.
- Plugins/CLI: reuse the cold manifest registry while building plugin status and inspect reports, so large configured plugin sets no longer rediscover the bundled/plugin registry once per inspect row. Thanks @vincentkoc.
- Discord/voice: lengthen the default voice join Ready wait, add configurable `voice.connectTimeoutMs`/`voice.reconnectGraceMs`, and warn before destroying unrecovered disconnected sessions so slow Discord voice handshakes and reconnects no longer fail silently. Fixes #63098; refs #39825 and #65039. Thanks @darealgege, @kzicherman, and @ayochim.
- Gateway/health: refresh cached health RPC snapshots when channel runtime state diverges, so Discord and other channel status reads no longer report stale running or connected values until the cache TTL expires. (#75423) Thanks @clawsweeper.
- Gateway/sessions: keep session-store reads from running stale prune and entry-count cap maintenance during startup, so oversized stores no longer block chat history readiness after updates while writes and `sessions cleanup --enforce` still preserve the cleanup safeguards. Fixes #70050. Thanks @tangda18.

View File

@@ -663,6 +663,7 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
config: params.config,
env: params.env,
pluginIds: setupPluginIds,
manifestRegistry: params.registry,
})) {
changes.push({
pluginId: entry.pluginId,

View File

@@ -131,7 +131,20 @@ export function resolveBundledProviderCompatPluginIds(params: {
workspaceDir?: string;
env?: PluginLoadOptions["env"];
onlyPluginIds?: readonly string[];
manifestRegistry?: PluginManifestRegistry;
}): string[] {
if (params.manifestRegistry) {
const onlyPluginIdSet = createPluginIdScopeSet(params.onlyPluginIds);
return params.manifestRegistry.plugins
.filter(
(plugin) =>
plugin.origin === "bundled" &&
plugin.providers.length > 0 &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
}
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
return listRegistryPluginIds(

View File

@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { createSubsystemLogger } from "../../logging.js";
import { resolvePluginActivationSourceConfig } from "../activation-source-config.js";
import type { PluginLoadOptions } from "../loader.js";
import type { PluginManifestRegistry } from "../manifest-registry.js";
import type { PluginLogger } from "../types.js";
const log = createSubsystemLogger("plugins");
@@ -30,6 +31,7 @@ export type PluginRuntimeLoadContextOptions = {
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
logger?: PluginLogger;
manifestRegistry?: PluginManifestRegistry;
};
export function createPluginRuntimeLoaderLogger(): PluginLogger {
@@ -50,7 +52,11 @@ export function resolvePluginRuntimeLoadContext(
config: rawConfig,
activationSourceConfig: options?.activationSourceConfig,
});
const autoEnabled = applyPluginAutoEnable({ config: rawConfig, env });
const autoEnabled = applyPluginAutoEnable({
config: rawConfig,
env,
manifestRegistry: options?.manifestRegistry,
});
const config = autoEnabled.config;
const workspaceDir =
options?.workspaceDir ?? resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));

View File

@@ -103,6 +103,47 @@ describe("loadPluginMetadataRegistrySnapshot", () => {
);
});
it("honors explicit load options when reusing a resolved runtime context", () => {
const logger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const env = { HOME: "/tmp/context-home" } as NodeJS.ProcessEnv;
const manifestRegistry = { plugins: [], diagnostics: [] };
loadPluginMetadataRegistrySnapshot({
config: { plugins: { allow: ["compat-provider"] } },
activationSourceConfig: { plugins: { allow: ["raw-plugin"] } },
workspaceDir: "/compat-workspace",
env,
logger,
manifestRegistry,
runtimeContext: {
rawConfig: { plugins: { allow: ["raw-plugin"] } },
config: { plugins: { allow: ["raw-plugin"] } },
activationSourceConfig: { plugins: { allow: ["raw-plugin"] } },
autoEnabledReasons: {},
workspaceDir: "/context-workspace",
env,
logger,
},
});
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
config: { plugins: { allow: ["compat-provider"] } },
activationSourceConfig: { plugins: { allow: ["raw-plugin"] } },
workspaceDir: "/compat-workspace",
env,
logger,
manifestRegistry,
mode: "validate",
}),
);
});
it("preserves explicit empty plugin scopes on metadata snapshots", () => {
loadPluginMetadataRegistrySnapshot({
config: { plugins: {} },

View File

@@ -1,9 +1,14 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { loadOpenClawPlugins } from "../loader.js";
import type { PluginManifestRegistry } from "../manifest-registry.js";
import { hasExplicitPluginIdScope } from "../plugin-scope.js";
import type { PluginRegistry } from "../registry.js";
import type { PluginLogger } from "../types.js";
import { buildPluginRuntimeLoadOptions, resolvePluginRuntimeLoadContext } from "./load-context.js";
import {
buildPluginRuntimeLoadOptions,
resolvePluginRuntimeLoadContext,
type PluginRuntimeLoadContext,
} from "./load-context.js";
export function loadPluginMetadataRegistrySnapshot(options?: {
config?: OpenClawConfig;
@@ -13,11 +18,20 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
workspaceDir?: string;
onlyPluginIds?: string[];
loadModules?: boolean;
manifestRegistry?: PluginManifestRegistry;
runtimeContext?: PluginRuntimeLoadContext;
}): PluginRegistry {
const context = resolvePluginRuntimeLoadContext(options);
const context = options?.runtimeContext ?? resolvePluginRuntimeLoadContext(options);
return loadOpenClawPlugins(
buildPluginRuntimeLoadOptions(context, {
...(options?.config !== undefined ? { config: options.config } : {}),
...(options?.activationSourceConfig !== undefined
? { activationSourceConfig: options.activationSourceConfig }
: {}),
...(options?.workspaceDir !== undefined ? { workspaceDir: options.workspaceDir } : {}),
...(options?.env !== undefined ? { env: options.env } : {}),
...(options?.logger !== undefined ? { logger: options.logger } : {}),
throwOnLoadError: true,
cache: false,
activate: false,
@@ -26,6 +40,7 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
...(hasExplicitPluginIdScope(options?.onlyPluginIds)
? { onlyPluginIds: options?.onlyPluginIds }
: {}),
...(options?.manifestRegistry ? { manifestRegistry: options.manifestRegistry } : {}),
}),
);
}

View File

@@ -6,7 +6,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { buildPluginApi } from "./api-builder.js";
import { collectPluginConfigContractMatches } from "./config-contracts.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import type { PluginManifestRecord } from "./manifest-registry.js";
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
import type { PluginRuntime } from "./runtime/types.js";
import { listSetupCliBackendIds, listSetupProviderIds } from "./setup-descriptors.js";
@@ -417,6 +417,7 @@ export function resolvePluginSetupRegistry(params?: {
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
pluginIds?: readonly string[];
manifestRegistry?: PluginManifestRegistry;
}): PluginSetupRegistry {
const env = params?.env ?? process.env;
const scopedPluginIds = params?.pluginIds
@@ -441,12 +442,14 @@ export function resolvePluginSetupRegistry(params?: {
const providerKeys = new Set<string>();
const cliBackendKeys = new Set<string>();
const manifestRegistry = loadSetupManifestRegistry({
config: params?.config,
workspaceDir: params?.workspaceDir,
env,
pluginIds: params?.pluginIds,
});
const manifestRegistry =
params?.manifestRegistry ??
loadSetupManifestRegistry({
config: params?.config,
workspaceDir: params?.workspaceDir,
env,
pluginIds: params?.pluginIds,
});
for (const record of manifestRegistry.plugins) {
if (scopedPluginIds && !scopedPluginIds.has(record.id)) {
@@ -703,6 +706,7 @@ export function resolvePluginSetupAutoEnableReasons(params: {
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
pluginIds?: readonly string[];
manifestRegistry?: PluginManifestRegistry;
}): SetupAutoEnableReason[] {
const env = params.env ?? process.env;
const reasons: SetupAutoEnableReason[] = [];
@@ -713,6 +717,7 @@ export function resolvePluginSetupAutoEnableReasons(params: {
workspaceDir: params.workspaceDir,
env,
pluginIds: params.pluginIds,
manifestRegistry: params.manifestRegistry,
}).autoEnableProbes) {
const raw = entry.probe({
config: params.config,

View File

@@ -13,6 +13,7 @@ import {
const loadConfigMock = vi.fn();
const loadOpenClawPluginsMock = vi.fn();
const loadPluginMetadataRegistrySnapshotMock = vi.fn();
const loadPluginManifestRegistryForPluginRegistryMock = vi.fn();
const loadPluginRegistrySnapshotWithMetadataMock = vi.fn();
const loadPluginManifestRegistryForInstalledIndexMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
@@ -50,6 +51,8 @@ vi.mock("./runtime/metadata-registry-loader.js", () => ({
}));
vi.mock("./plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry: (...args: unknown[]) =>
loadPluginManifestRegistryForPluginRegistryMock(...args),
loadPluginRegistrySnapshotWithMetadata: (...args: unknown[]) =>
loadPluginRegistrySnapshotWithMetadataMock(...args),
}));
@@ -186,10 +189,12 @@ function expectMetadataSnapshotLoaderCall(params: {
}
function expectAutoEnabledStatusLoad(params: { rawConfig: unknown }) {
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
config: params.rawConfig,
env: process.env,
});
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith(
expect.objectContaining({
config: params.rawConfig,
env: process.env,
}),
);
}
function createCompatChainFixture() {
@@ -363,6 +368,7 @@ describe("plugin status reports", () => {
loadConfigMock.mockReset();
loadOpenClawPluginsMock.mockReset();
loadPluginMetadataRegistrySnapshotMock.mockReset();
loadPluginManifestRegistryForPluginRegistryMock.mockReset();
loadPluginRegistrySnapshotWithMetadataMock.mockReset();
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
applyPluginAutoEnableMock.mockReset();
@@ -377,6 +383,10 @@ describe("plugin status reports", () => {
source: "derived",
diagnostics: [],
});
loadPluginManifestRegistryForPluginRegistryMock.mockReturnValue({
plugins: [],
diagnostics: [],
});
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue({
plugins: [],
diagnostics: [],

View File

@@ -1,3 +1,4 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
import { getRuntimeConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -24,6 +25,7 @@ import type { PluginManifestRecord } from "./manifest-registry.js";
import type { PluginDiagnostic } from "./manifest-types.js";
import { tracePluginLifecyclePhase } from "./plugin-lifecycle-trace.js";
import {
loadPluginManifestRegistryForPluginRegistry,
loadPluginRegistrySnapshotWithMetadata,
type PluginRegistrySnapshotDiagnostic,
type PluginRegistrySnapshotSource,
@@ -167,6 +169,7 @@ type PluginReportParams = {
/** Use an explicit env when plugin roots should resolve independently from process.env. */
env?: NodeJS.ProcessEnv;
logger?: PluginLogger;
resolvedConfig?: OpenClawConfig;
};
function buildPluginRecordFromInstalledIndex(
@@ -259,13 +262,27 @@ function buildPluginReport(
params: PluginReportParams | undefined,
loadModules: boolean,
): PluginStatusReport {
const rawConfig = params?.config ?? getRuntimeConfig();
const initialWorkspaceDir =
params?.workspaceDir ??
resolveAgentWorkspaceDir(rawConfig, resolveDefaultAgentId(rawConfig), params?.env);
const manifestRegistry = !loadModules
? loadPluginManifestRegistryForPluginRegistry({
config: rawConfig,
env: params?.env,
workspaceDir: initialWorkspaceDir,
includeDisabled: true,
})
: undefined;
const baseContext = resolvePluginRuntimeLoadContext({
config: params?.config ?? getRuntimeConfig(),
config: rawConfig,
env: params?.env,
logger: params?.logger,
workspaceDir: params?.workspaceDir,
workspaceDir: initialWorkspaceDir,
manifestRegistry,
});
const workspaceDir = baseContext.workspaceDir ?? resolveDefaultAgentWorkspaceDir();
const workspaceDir =
baseContext.workspaceDir ?? initialWorkspaceDir ?? resolveDefaultAgentWorkspaceDir();
const context =
workspaceDir === baseContext.workspaceDir
? baseContext
@@ -273,7 +290,6 @@ function buildPluginReport(
...baseContext,
workspaceDir,
};
const rawConfig = context.rawConfig;
const config = context.config;
// Apply bundled-provider allowlist compat so that `plugins list` and `doctor`
@@ -286,6 +302,7 @@ function buildPluginReport(
config,
workspaceDir,
env: params?.env,
manifestRegistry,
});
const effectiveConfig = withBundledPluginAllowlistCompat({
config,
@@ -336,6 +353,8 @@ function buildPluginReport(
logger: params?.logger,
loadModules: false,
onlyPluginIds,
manifestRegistry,
runtimeContext: context,
}),
{ surface: "status", onlyPluginCount: onlyPluginIds?.length },
);
@@ -376,14 +395,17 @@ export function buildPluginInspectReport(params: {
env?: NodeJS.ProcessEnv;
logger?: PluginLogger;
report?: PluginStatusReport;
resolvedConfig?: OpenClawConfig;
}): PluginInspectReport | null {
const rawConfig = params.config ?? getRuntimeConfig();
const config = resolvePluginRuntimeLoadContext({
config: rawConfig,
env: params.env,
logger: params.logger,
workspaceDir: params.workspaceDir,
}).config;
const config =
params.resolvedConfig ??
resolvePluginRuntimeLoadContext({
config: rawConfig,
env: params.env,
logger: params.logger,
workspaceDir: params.workspaceDir,
}).config;
const report =
params.report ??
buildPluginDiagnosticsReport({
@@ -508,6 +530,12 @@ export function buildAllPluginInspectReports(params?: {
report?: PluginStatusReport;
}): PluginInspectReport[] {
const rawConfig = params?.config ?? getRuntimeConfig();
const config = resolvePluginRuntimeLoadContext({
config: rawConfig,
env: params?.env,
logger: params?.logger,
workspaceDir: params?.workspaceDir,
}).config;
const report =
params?.report ??
buildPluginDiagnosticsReport({
@@ -523,6 +551,9 @@ export function buildAllPluginInspectReports(params?: {
id: plugin.id,
config: rawConfig,
logger: params?.logger,
workspaceDir: params?.workspaceDir,
env: params?.env,
resolvedConfig: config,
report,
}),
)