perf(plugins): reuse compatible metadata snapshots

Reuse compatible workspace-scoped plugin metadata snapshots for unscoped model catalog and manifest-contract readers while preserving env/config/workspace compatibility checks.

Also updates the stale kitchen-sink prerelease canary assertion to the current script default.

Fixes #77519.
Related #77532.
This commit is contained in:
Peter Steinberger
2026-05-05 01:39:34 +01:00
committed by GitHub
parent b38e674c9f
commit 48ff390953
7 changed files with 105 additions and 6 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
- OpenAI/Codex media: advertise Codex audio transcription in runtime and manifest metadata and route active Codex chat models to the OpenAI transcription default instead of sending chat model ids to audio transcription. Thanks @vincentkoc.
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
- Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532)
- Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532)
- Config/plugin auto-enable: prefer the claiming plugin manifest id over a built-in channel alias when auto-allowlisting a configured channel, so WeCom/Yuanbao-style aliases resolve to the installed plugin id. Thanks @Beandon13.
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.

View File

@@ -662,6 +662,38 @@ describe("loadModelCatalog", () => {
]);
});
it("lets read-only manifest catalog reuse the current workspace-scoped snapshot", () => {
loadManifestModelCatalog({
config: {} as OpenClawConfig,
fallbackToMetadataScan: false,
});
expect(currentPluginMetadataSnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({
allowWorkspaceScopedSnapshot: true,
}),
);
expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled();
});
it("passes explicit env when checking current manifest catalog snapshot compatibility", () => {
const env = { HOME: "/tmp/openclaw-model-catalog-env" } as NodeJS.ProcessEnv;
loadManifestModelCatalog({
config: {} as OpenClawConfig,
env,
fallbackToMetadataScan: false,
});
expect(currentPluginMetadataSnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({
env,
allowWorkspaceScopedSnapshot: true,
}),
);
expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled();
});
it("dedupes supplemental models against registry entries", async () => {
mockSingleOpenAiCatalogModel();
augmentCatalogMock.mockResolvedValueOnce([

View File

@@ -123,7 +123,9 @@ export function loadManifestModelCatalog(params: {
}): ModelCatalogEntry[] {
const snapshot = getCurrentPluginMetadataSnapshot({
config: params.config,
env: params.env,
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
...(params.workspaceDir === undefined ? { allowWorkspaceScopedSnapshot: true } : {}),
});
const resolvedSnapshot =
snapshot ??

View File

@@ -87,6 +87,19 @@ describe("current plugin metadata snapshot", () => {
expect(getCurrentPluginMetadataSnapshot({ config })).toBeUndefined();
});
it("can opt into reusing the stored workspace scope for unscoped control-plane readers", () => {
const config = { plugins: { allow: ["demo"] } };
const snapshot = createSnapshot({ config, workspaceDir: "/workspace/a" });
setCurrentPluginMetadataSnapshot(snapshot, { config });
expect(
getCurrentPluginMetadataSnapshot({
config,
allowWorkspaceScopedSnapshot: true,
}),
).toBe(snapshot);
});
it("rejects a current snapshot when plugin load paths change", () => {
const config = { plugins: { load: { paths: ["/plugins/one"] } } };
const snapshot = createSnapshot({ config });

View File

@@ -49,6 +49,7 @@ export function getCurrentPluginMetadataSnapshot(
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
allowWorkspaceScopedSnapshot?: boolean;
} = {},
): PluginMetadataSnapshot | undefined {
const { snapshot: rawSnapshot, configFingerprint } = getCurrentPluginMetadataSnapshotState();
@@ -62,12 +63,15 @@ export function getCurrentPluginMetadataSnapshot(
) {
return undefined;
}
const requestedWorkspaceDir =
params.workspaceDir ??
(params.allowWorkspaceScopedSnapshot === true ? snapshot.workspaceDir : undefined);
if (params.config) {
const requestedConfigFingerprint = resolvePluginMetadataControlPlaneFingerprint(params.config, {
env: params.env,
index: snapshot.index,
policyHash: snapshot.policyHash,
workspaceDir: params.workspaceDir,
workspaceDir: requestedWorkspaceDir,
});
if (configFingerprint && configFingerprint !== requestedConfigFingerprint) {
return undefined;
@@ -76,12 +80,12 @@ export function getCurrentPluginMetadataSnapshot(
return undefined;
}
}
if (snapshot.workspaceDir !== undefined && params.workspaceDir === undefined) {
if (snapshot.workspaceDir !== undefined && requestedWorkspaceDir === undefined) {
return undefined;
}
if (
params.workspaceDir !== undefined &&
(snapshot.workspaceDir ?? "") !== (params.workspaceDir ?? "")
requestedWorkspaceDir !== undefined &&
(snapshot.workspaceDir ?? "") !== (requestedWorkspaceDir ?? "")
) {
return undefined;
}

View File

@@ -46,6 +46,51 @@ describe("loadManifestContractSnapshot", () => {
expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
});
it("opts unscoped callers into the stored workspace-scoped snapshot", () => {
const env = { HOME: "/home/snapshot" } as NodeJS.ProcessEnv;
const current = {
index: { plugins: [] },
plugins: [],
};
mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(current);
expect(loadManifestContractSnapshot({ config: {}, env })).toEqual({
index: current.index,
plugins: current.plugins,
});
expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
config: {},
env,
allowWorkspaceScopedSnapshot: true,
});
expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
});
it("normalizes omitted config before checking unscoped snapshot compatibility", () => {
const env = { HOME: "/home/default-config" } as NodeJS.ProcessEnv;
const snapshot = {
index: { plugins: [{ pluginId: "demo" }] },
plugins: [{ id: "demo" }],
};
mocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot);
expect(loadManifestContractSnapshot({ env })).toEqual({
index: snapshot.index,
plugins: snapshot.plugins,
});
expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
config: {},
env,
allowWorkspaceScopedSnapshot: true,
});
expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
config: {},
env,
});
});
it("falls back to the shared metadata snapshot loader", () => {
const env = { HOME: "/home/fallback" } as NodeJS.ProcessEnv;
const snapshot = {

View File

@@ -96,17 +96,19 @@ export function loadManifestMetadataSnapshot(params: {
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): PluginMetadataSnapshot {
const config = params.config ?? {};
const env = params.env ?? process.env;
const current = getCurrentPluginMetadataSnapshot({
config: params.config,
config,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
...(params.workspaceDir === undefined ? { allowWorkspaceScopedSnapshot: true } : {}),
});
if (current) {
return current;
}
return loadPluginMetadataSnapshot({
config: params.config ?? {},
config,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});