fix: reuse plugin metadata for config schemas

This commit is contained in:
Peter Steinberger
2026-04-28 01:37:05 +01:00
parent d93e6f6158
commit 4cc42a1d69
5 changed files with 92 additions and 9 deletions

View File

@@ -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.

View File

@@ -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<ConfigFileSnapshot>>());
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<string, unknown> };
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
const channelProps = channelsNode?.properties as Record<string, unknown> | 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");

View File

@@ -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,
});
}

View File

@@ -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();

View File

@@ -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,
);
}