diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ec253ac6b1..a184d9703bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Plugins/hooks: time out never-settling `agent_end` observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099. +- Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every `config.get`/`config.schema`, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb. - Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending `encoding_format`, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial. - Plugins/install: run dependency installs with npm error-level logging instead of silent mode so failed plugin or hook installs surface actionable npm errors such as EUNSUPPORTEDPROTOCOL instead of `npm install failed:` with no detail. (#73093) Thanks @sanctrl. - Memory/LanceDB: bound memory recall embedding queries with a new `recallMaxChars` setting, prefer the latest user message over channel prompt metadata during auto-recall, and document the knob so small Ollama embedding models avoid context-length failures. Fixes #56780. Thanks @rungmc357 and @zak-collaborator. diff --git a/src/config/runtime-schema.test.ts b/src/config/runtime-schema.test.ts index 0d16b136727..07b681e03e9 100644 --- a/src/config/runtime-schema.test.ts +++ b/src/config/runtime-schema.test.ts @@ -12,6 +12,7 @@ import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js"; const mockLoadConfig = vi.hoisted(() => vi.fn<() => OpenClawConfig>()); const mockReadConfigFileSnapshot = vi.hoisted(() => vi.fn<() => Promise>()); const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn()); +const mockGetCurrentPluginMetadataSnapshot = vi.hoisted(() => vi.fn()); let readBestEffortRuntimeConfigSchema: typeof import("./runtime-schema.js").readBestEffortRuntimeConfigSchema; let loadGatewayRuntimeConfigSchema: typeof import("./runtime-schema.js").loadGatewayRuntimeConfigSchema; @@ -33,6 +34,11 @@ vi.mock("../plugins/plugin-registry.js", () => ({ mockLoadPluginManifestRegistry(...args), })); +vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({ + getCurrentPluginMetadataSnapshot: (...args: unknown[]) => + mockGetCurrentPluginMetadataSnapshot(...args), +})); + function makeSnapshot(params: { valid: boolean; config?: OpenClawConfig }): ConfigFileSnapshot { return { path: "/tmp/openclaw.json", @@ -182,7 +188,7 @@ describe("readBestEffortRuntimeConfigSchema", () => { expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith( expect.objectContaining({ config: { plugins: { entries: { demo: { enabled: true } } } }, - cache: false, + cache: true, }), ); expect(channelProps?.telegram).toBeTruthy(); @@ -198,7 +204,7 @@ describe("readBestEffortRuntimeConfigSchema", () => { expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith( expect.objectContaining({ config: { plugins: { enabled: true } }, - cache: false, + cache: true, }), ); expect(channelProps?.telegram).toBeTruthy(); @@ -223,13 +229,65 @@ describe("loadGatewayRuntimeConfigSchema", () => { expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith( expect.objectContaining({ config: { plugins: { entries: { demo: { enabled: true } } } }, - cache: false, + cache: true, }), ); + expect(mockLoadPluginManifestRegistry.mock.calls[0]?.[0]).not.toHaveProperty( + "bundledChannelConfigCollector", + ); expect(channelProps?.telegram).toBeTruthy(); expect(channelProps?.matrix).toBeTruthy(); }); + it("reuses the current gateway plugin metadata snapshot for config schema requests", () => { + mockGetCurrentPluginMetadataSnapshot.mockReturnValueOnce({ + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "telegram", + name: "Telegram", + description: "Telegram plugin", + origin: "bundled", + channels: ["telegram"], + }, + { + id: "matrix", + name: "Matrix", + description: "Matrix plugin", + origin: "workspace", + channels: ["matrix"], + channelConfigs: { + matrix: { + schema: { + type: "object", + properties: { + homeserver: { type: "string" }, + }, + }, + }, + }, + }, + ], + }, + }); + + const result = loadGatewayRuntimeConfigSchema(); + const schema = result.schema as { properties?: Record }; + const channelsNode = schema.properties?.channels as Record | undefined; + const channelProps = channelsNode?.properties as Record | undefined; + + expect(mockGetCurrentPluginMetadataSnapshot).toHaveBeenCalledWith( + expect.objectContaining({ + config: { plugins: { entries: { demo: { enabled: true } } } }, + }), + ); + expect(mockLoadPluginManifestRegistry).not.toHaveBeenCalled(); + expect(channelProps?.telegram).toBeTruthy(); + expect(JSON.stringify(channelProps?.telegram)).toContain("botToken"); + expect(channelProps?.matrix).toBeTruthy(); + }); + it("does not activate or replace the active plugin registry across repeated schema loads (regression guard for #54816)", () => { // Each MCP connection triggers a config.schema / config.get gateway request which calls // loadGatewayRuntimeConfigSchema. The original bug caused a fresh full plugin registry to @@ -246,7 +304,8 @@ describe("loadGatewayRuntimeConfigSchema", () => { expect(mockLoadPluginManifestRegistry).toHaveBeenCalledTimes(3); for (const call of mockLoadPluginManifestRegistry.mock.calls) { - expect(call[0]).toMatchObject({ cache: false }); + expect(call[0]).toMatchObject({ cache: true }); + expect(call[0]).not.toHaveProperty("bundledChannelConfigCollector"); } expect(getActivePluginRegistry()).toBe(activeRegistry); expect(getActivePluginRegistryKey()).toBe("startup-registry"); diff --git a/src/config/runtime-schema.ts b/src/config/runtime-schema.ts index 37dbec27770..2aab1a3b065 100644 --- a/src/config/runtime-schema.ts +++ b/src/config/runtime-schema.ts @@ -1,5 +1,5 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { collectBundledChannelConfigs } from "../plugins/bundled-channel-config-metadata.js"; +import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { collectChannelSchemaMetadata, @@ -11,13 +11,18 @@ import { buildConfigSchema, type ConfigSchemaResponse } from "./schema.js"; function loadManifestRegistry(config: OpenClawConfig, env?: NodeJS.ProcessEnv) { const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const currentSnapshot = getCurrentPluginMetadataSnapshot({ config, workspaceDir }); + if (currentSnapshot) { + return currentSnapshot.manifestRegistry; + } return loadPluginManifestRegistryForPluginRegistry({ config, - cache: false, + // Bundled channel schemas are already generated into the base schema; avoid + // loading plugin config-schema modules on every config.get/config.schema. + cache: true, env, workspaceDir, includeDisabled: true, - bundledChannelConfigCollector: collectBundledChannelConfigs, }); } diff --git a/src/plugins/current-plugin-metadata-snapshot.test.ts b/src/plugins/current-plugin-metadata-snapshot.test.ts index fef5bd902bf..9b12b241ba0 100644 --- a/src/plugins/current-plugin-metadata-snapshot.test.ts +++ b/src/plugins/current-plugin-metadata-snapshot.test.ts @@ -100,6 +100,19 @@ describe("current plugin metadata snapshot", () => { ).toBeUndefined(); }); + it("keeps source-policy compatibility when storing an auto-enabled runtime config", () => { + const sourceConfig = { channels: { telegram: { botToken: "token" } } }; + const autoEnabledConfig = { + ...sourceConfig, + plugins: { allow: ["telegram"] }, + }; + const snapshot = createSnapshot({ config: sourceConfig }); + setCurrentPluginMetadataSnapshot(snapshot, { config: autoEnabledConfig }); + + expect(getCurrentPluginMetadataSnapshot({ config: sourceConfig })).toBe(snapshot); + expect(getCurrentPluginMetadataSnapshot({ config: autoEnabledConfig })).toBeUndefined(); + }); + it("clears the current snapshot", () => { setCurrentPluginMetadataSnapshot(createSnapshot()); clearCurrentPluginMetadataSnapshot(); diff --git a/src/plugins/current-plugin-metadata-snapshot.ts b/src/plugins/current-plugin-metadata-snapshot.ts index abd6bf5b065..abe686cb42a 100644 --- a/src/plugins/current-plugin-metadata-snapshot.ts +++ b/src/plugins/current-plugin-metadata-snapshot.ts @@ -17,9 +17,10 @@ function normalizeLoadPaths(config: OpenClawConfig | undefined): readonly string export function resolvePluginMetadataSnapshotConfigFingerprint( config: OpenClawConfig | undefined, + options: { policyHash?: string } = {}, ): string { return JSON.stringify({ - policyHash: resolveInstalledPluginIndexPolicyHash(config), + policyHash: options.policyHash ?? resolveInstalledPluginIndexPolicyHash(config), pluginLoadPaths: normalizeLoadPaths(config), }); } @@ -32,7 +33,11 @@ export function setCurrentPluginMetadataSnapshot( ): void { setCurrentPluginMetadataSnapshotState( snapshot, - snapshot ? resolvePluginMetadataSnapshotConfigFingerprint(options.config) : undefined, + snapshot + ? resolvePluginMetadataSnapshotConfigFingerprint(options.config, { + policyHash: snapshot.policyHash, + }) + : undefined, ); }