perf(agents): reuse gateway plugin preparation (#105646)

* perf(agents): reuse gateway plugin preparation

* chore: keep release notes out of PR
This commit is contained in:
Peter Steinberger
2026-07-12 12:59:26 -07:00
committed by GitHub
parent e3c338f664
commit 1b281de6df
12 changed files with 203 additions and 9 deletions
+47 -1
View File
@@ -1,5 +1,6 @@
// Load context tests cover agent and workspace context resolution for plugin runtimes.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
const loadConfigMock = vi.fn<typeof import("../../config/config.js").loadConfig>();
const applyPluginAutoEnableMock =
@@ -20,7 +21,8 @@ const metadataSnapshot = {
policyHash: "policy",
workspaceDir: "/resolved-workspace",
};
const loadPluginMetadataSnapshotMock = vi.fn(() => metadataSnapshot);
type MetadataSnapshotMock = typeof metadataSnapshot & { pluginIds?: readonly string[] };
const loadPluginMetadataSnapshotMock = vi.fn((): MetadataSnapshotMock => metadataSnapshot);
const isPluginMetadataSnapshotCompatibleMock = vi.fn(() => true);
const getCurrentPluginMetadataSnapshotMock = vi.fn(() => undefined);
const setCurrentPluginMetadataSnapshotMock = vi.fn();
@@ -190,6 +192,50 @@ describe("resolvePluginRuntimeLoadContext", () => {
});
});
it("reuses auto-enable results until Gateway config or metadata changes", () => {
const rawConfig = { plugins: {} };
const env = process.env;
const initialSnapshot = { ...metadataSnapshot, pluginIds: ["openai"] };
loadPluginMetadataSnapshotMock
.mockReturnValueOnce(initialSnapshot)
.mockReturnValueOnce({ ...initialSnapshot, pluginIds: ["openai"] })
.mockReturnValueOnce({ ...initialSnapshot, policyHash: "changed" })
.mockReturnValueOnce(initialSnapshot);
const first = resolvePluginRuntimeLoadContext({ config: rawConfig, env });
const second = resolvePluginRuntimeLoadContext({ config: rawConfig, env });
resolvePluginRuntimeLoadContext({ config: rawConfig, env });
resolvePluginRuntimeLoadContext({ config: { plugins: {} }, env });
expect(second.config).toBe(first.config);
expect(applyPluginAutoEnableMock).toHaveBeenCalledTimes(3);
});
it("invalidates auto-enable results when config or process env mutates in place", () => {
const rawConfig: OpenClawConfig = { plugins: {} };
const env = process.env;
const envKey = "OPENCLAW_TEST_PLUGIN_AUTO_ENABLE_FINGERPRINT";
const previousEnvValue = env[envKey];
delete env[envKey];
try {
resolvePluginRuntimeLoadContext({ config: rawConfig, env });
resolvePluginRuntimeLoadContext({ config: rawConfig, env });
rawConfig.plugins = { entries: { demo: { enabled: true } } };
resolvePluginRuntimeLoadContext({ config: rawConfig, env });
env[envKey] = "changed";
resolvePluginRuntimeLoadContext({ config: rawConfig, env });
expect(applyPluginAutoEnableMock).toHaveBeenCalledTimes(3);
} finally {
if (previousEnvValue === undefined) {
delete env[envKey];
} else {
env[envKey] = previousEnvValue;
}
}
});
it("threads install records from the metadata snapshot into the context and load options", () => {
const snapshotWithRecords = {
...metadataSnapshot,
+92 -2
View File
@@ -1,6 +1,10 @@
// Plugin runtime load context helpers resolve agent and workspace facts for runtime activation.
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { getRuntimeConfig } from "../../config/config.js";
import {
fingerprintPluginAutoEnableConfig,
fingerprintPluginAutoEnableEnv,
} from "../../config/plugin-auto-enable.apply.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginInstallRecord } from "../../config/types.plugins.js";
@@ -14,6 +18,7 @@ import {
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../installed-plugin-index-install-records.js";
import type { PluginLoadOptions } from "../loader.js";
import type { PluginManifestRegistry } from "../manifest-registry.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "../plugin-metadata-lifecycle.js";
import {
isPluginMetadataSnapshotCompatible,
resolvePluginMetadataSnapshot,
@@ -22,6 +27,90 @@ import type { PluginLogger } from "../types.js";
const log = createSubsystemLogger("plugins");
type CurrentAutoEnableCacheEntry = {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
autoEnableConfigFingerprint: string;
autoEnableEnvFingerprint: string;
metadataConfigFingerprint: string | undefined;
pluginIds: readonly string[] | undefined;
policyHash: string;
result: ReturnType<typeof applyPluginAutoEnable>;
workspaceDir: string | undefined;
};
let currentAutoEnableCache: CurrentAutoEnableCacheEntry | undefined;
registerPluginMetadataProcessMemoLifecycleClear(() => {
currentAutoEnableCache = undefined;
});
function samePluginIds(
left: readonly string[] | undefined,
right: readonly string[] | undefined,
): boolean {
return (
left === right ||
(left !== undefined &&
right !== undefined &&
left.length === right.length &&
left.every((pluginId, index) => pluginId === right[index]))
);
}
function applyCurrentPluginAutoEnable(params: {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
manifestRegistry: PluginManifestRegistry | undefined;
snapshot: ReturnType<typeof resolvePluginMetadataSnapshot> | undefined;
}): ReturnType<typeof applyPluginAutoEnable> {
if (!params.snapshot || !params.manifestRegistry || params.env !== process.env) {
return applyPluginAutoEnable({
config: params.config,
env: params.env,
manifestRegistry: params.manifestRegistry,
discovery: params.snapshot?.discovery,
});
}
// Gateway plugin metadata and config are replacement snapshots. Reuse only while
// mutable config/env content still matches; reload/close lifecycle clears the slot.
const workspaceDir = params.snapshot.workspaceDir ?? params.workspaceDir;
const autoEnableConfigFingerprint = fingerprintPluginAutoEnableConfig(params.config);
const autoEnableEnvFingerprint = fingerprintPluginAutoEnableEnv(params.env);
const cached = currentAutoEnableCache;
if (
cached?.config === params.config &&
cached.env === params.env &&
cached.autoEnableConfigFingerprint === autoEnableConfigFingerprint &&
cached.autoEnableEnvFingerprint === autoEnableEnvFingerprint &&
cached.metadataConfigFingerprint === params.snapshot.configFingerprint &&
cached.policyHash === params.snapshot.policyHash &&
cached.workspaceDir === workspaceDir &&
samePluginIds(cached.pluginIds, params.snapshot.pluginIds)
) {
return cached.result;
}
const result = applyPluginAutoEnable({
config: params.config,
env: params.env,
manifestRegistry: params.manifestRegistry,
discovery: params.snapshot.discovery,
});
currentAutoEnableCache = {
config: params.config,
env: params.env,
autoEnableConfigFingerprint,
autoEnableEnvFingerprint,
metadataConfigFingerprint: params.snapshot.configFingerprint,
pluginIds: params.snapshot.pluginIds,
policyHash: params.snapshot.policyHash,
result,
workspaceDir,
};
return result;
}
/** Resolved plugin runtime load context shared by runtime loader callers. */
export type PluginRuntimeLoadContext = {
rawConfig: OpenClawConfig;
@@ -90,11 +179,12 @@ export function resolvePluginRuntimeLoadContext(
config: rawConfig,
activationSourceConfig: options?.activationSourceConfig,
});
const autoEnabled = applyPluginAutoEnable({
const autoEnabled = applyCurrentPluginAutoEnable({
config: rawConfig,
env,
workspaceDir: rawWorkspaceDir,
manifestRegistry,
discovery: initialMetadataSnapshot?.discovery,
snapshot: initialMetadataSnapshot,
});
const config = autoEnabled.config;
const workspaceDir =