diff --git a/CHANGELOG.md b/CHANGELOG.md index c54ac1d38b5..c21c4e4165f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 6c7ae9e15d9..d7b8b965f3d 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -663,6 +663,7 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: { config: params.config, env: params.env, pluginIds: setupPluginIds, + manifestRegistry: params.registry, })) { changes.push({ pluginId: entry.pluginId, diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index aabfb1710d3..1459a9cb380 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -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( diff --git a/src/plugins/runtime/load-context.ts b/src/plugins/runtime/load-context.ts index 0c970682b8f..815c14ac37e 100644 --- a/src/plugins/runtime/load-context.ts +++ b/src/plugins/runtime/load-context.ts @@ -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)); diff --git a/src/plugins/runtime/metadata-registry-loader.test.ts b/src/plugins/runtime/metadata-registry-loader.test.ts index 2f8c60e4bd9..070ba96b702 100644 --- a/src/plugins/runtime/metadata-registry-loader.test.ts +++ b/src/plugins/runtime/metadata-registry-loader.test.ts @@ -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: {} }, diff --git a/src/plugins/runtime/metadata-registry-loader.ts b/src/plugins/runtime/metadata-registry-loader.ts index 5723037ba1f..a1e2ec2c0b7 100644 --- a/src/plugins/runtime/metadata-registry-loader.ts +++ b/src/plugins/runtime/metadata-registry-loader.ts @@ -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 } : {}), }), ); } diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index b4069c0a533..35d6c3bd864 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -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(); const cliBackendKeys = new Set(); - 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, diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 9b39a5bd439..7a9dc4681ad 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -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: [], diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 85db36b9de9..1d16ca3eb20 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -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, }), )