mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
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:
committed by
GitHub
parent
b38e674c9f
commit
48ff390953
@@ -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.
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user