mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: reuse plugin metadata for config schemas
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user