Skip to content

Commit a397a64

Browse files
committed
perf: reuse scoped runtime plugin registries
1 parent cd7e3df commit a397a64

8 files changed

Lines changed: 291 additions & 19 deletions

src/agents/runtime-plugins.registry-reuse.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,49 @@ describe("ensureRuntimePluginsLoaded registry reuse", () => {
8787
});
8888
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
8989
});
90+
91+
it("reuses the startup registry when the turn workspace differs from gateway activation", () => {
92+
const config = { plugins: { allow: ["telegram"] } };
93+
const activeRegistry = createRegistryWithPlugin("telegram");
94+
activeRegistry.coreGatewayMethodNames = ["sessions.get", "sessions.list"];
95+
const startupLoadOptions = {
96+
config,
97+
activationSourceConfig: config,
98+
autoEnabledReasons: {},
99+
workspaceDir: "/tmp/gateway-workspace",
100+
onlyPluginIds: ["telegram"],
101+
coreGatewayMethodNames: ["sessions.get", "sessions.list"],
102+
runtimeOptions: {
103+
allowGatewaySubagentBinding: true,
104+
},
105+
preferBuiltPluginArtifacts: true,
106+
};
107+
const { cacheKey } = testing.resolvePluginLoadCacheContext(startupLoadOptions);
108+
setActivePluginRegistry(
109+
activeRegistry,
110+
cacheKey,
111+
"gateway-bindable",
112+
"/tmp/gateway-workspace",
113+
startupLoadOptions.onlyPluginIds,
114+
);
115+
mocks.getCurrentPluginMetadataSnapshot.mockReturnValue({
116+
startup: {
117+
pluginIds: ["telegram"],
118+
},
119+
});
120+
mocks.loadOpenClawPlugins.mockImplementation(() => {
121+
throw new Error("dispatch should reuse the active gateway startup registry");
122+
});
123+
124+
ensureRuntimePluginsLoaded({
125+
config,
126+
workspaceDir: "/tmp/agent-workspace",
127+
});
128+
129+
expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
130+
config,
131+
workspaceDir: "/tmp/agent-workspace",
132+
});
133+
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
134+
});
90135
});

src/agents/runtime-plugins.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44
const hoisted = vi.hoisted(() => ({
55
getCurrentPluginMetadataSnapshot: vi.fn(),
66
ensureStandaloneRuntimePluginRegistryLoaded: vi.fn(),
7+
getLoadedRuntimePluginRegistry: vi.fn(),
78
getActivePluginRuntimeSubagentMode: vi.fn<() => "default" | "explicit" | "gateway-bindable">(
89
() => "default",
910
),
@@ -18,6 +19,10 @@ vi.mock("../plugins/runtime/standalone-runtime-registry-loader.js", () => ({
1819
ensureStandaloneRuntimePluginRegistryLoaded: hoisted.ensureStandaloneRuntimePluginRegistryLoaded,
1920
}));
2021

22+
vi.mock("../plugins/active-runtime-registry.js", () => ({
23+
getLoadedRuntimePluginRegistry: hoisted.getLoadedRuntimePluginRegistry,
24+
}));
25+
2126
vi.mock("../plugins/runtime.js", () => ({
2227
getActivePluginRuntimeSubagentMode: hoisted.getActivePluginRuntimeSubagentMode,
2328
getActivePluginRegistryWorkspaceDir: hoisted.getActivePluginRegistryWorkspaceDir,
@@ -32,6 +37,8 @@ describe("ensureRuntimePluginsLoaded", () => {
3237
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined);
3338
hoisted.ensureStandaloneRuntimePluginRegistryLoaded.mockReset();
3439
hoisted.ensureStandaloneRuntimePluginRegistryLoaded.mockReturnValue(undefined);
40+
hoisted.getLoadedRuntimePluginRegistry.mockReset();
41+
hoisted.getLoadedRuntimePluginRegistry.mockReturnValue(undefined);
3542
hoisted.getActivePluginRuntimeSubagentMode.mockReset();
3643
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("default");
3744
hoisted.getActivePluginRegistryWorkspaceDir.mockReset();
@@ -147,6 +154,84 @@ describe("ensureRuntimePluginsLoaded", () => {
147154
});
148155
});
149156

157+
it("reuses the active startup registry workspace only on a compatible cache hit", () => {
158+
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
159+
startup: {
160+
pluginIds: ["telegram"],
161+
},
162+
});
163+
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("gateway-bindable");
164+
hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue("/tmp/gateway-workspace");
165+
hoisted.getLoadedRuntimePluginRegistry.mockReturnValue({});
166+
167+
ensureRuntimePluginsLoaded({
168+
config: {} as never,
169+
workspaceDir: "/tmp/agent-workspace",
170+
allowGatewaySubagentBinding: true,
171+
});
172+
173+
expect(hoisted.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({
174+
config: {} as never,
175+
workspaceDir: "/tmp/agent-workspace",
176+
});
177+
expect(hoisted.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith({
178+
requiredPluginIds: ["telegram"],
179+
workspaceDir: "/tmp/gateway-workspace",
180+
loadOptions: {
181+
config: {} as never,
182+
onlyPluginIds: ["telegram"],
183+
workspaceDir: "/tmp/gateway-workspace",
184+
forceFullRuntimeForChannelPlugins: true,
185+
runtimeOptions: {
186+
allowGatewaySubagentBinding: true,
187+
},
188+
},
189+
});
190+
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).not.toHaveBeenCalled();
191+
});
192+
193+
it("loads from the requested workspace when active startup registry reuse misses", () => {
194+
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue({
195+
startup: {
196+
pluginIds: ["telegram"],
197+
},
198+
});
199+
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("gateway-bindable");
200+
hoisted.getActivePluginRegistryWorkspaceDir.mockReturnValue("/tmp/gateway-workspace");
201+
202+
ensureRuntimePluginsLoaded({
203+
config: {} as never,
204+
workspaceDir: "/tmp/agent-workspace",
205+
allowGatewaySubagentBinding: true,
206+
});
207+
208+
expect(hoisted.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith({
209+
requiredPluginIds: ["telegram"],
210+
workspaceDir: "/tmp/gateway-workspace",
211+
loadOptions: {
212+
config: {} as never,
213+
onlyPluginIds: ["telegram"],
214+
workspaceDir: "/tmp/gateway-workspace",
215+
forceFullRuntimeForChannelPlugins: true,
216+
runtimeOptions: {
217+
allowGatewaySubagentBinding: true,
218+
},
219+
},
220+
});
221+
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
222+
requiredPluginIds: ["telegram"],
223+
loadOptions: {
224+
config: {} as never,
225+
onlyPluginIds: ["telegram"],
226+
workspaceDir: "/tmp/agent-workspace",
227+
forceFullRuntimeForChannelPlugins: true,
228+
runtimeOptions: {
229+
allowGatewaySubagentBinding: true,
230+
},
231+
},
232+
});
233+
});
234+
150235
it("lets the loader decide when startup ids match but config changes", () => {
151236
const config = {
152237
plugins: {

src/agents/runtime-plugins.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import type { OpenClawConfig } from "../config/types.openclaw.js";
66
import { normalizePluginsConfig } from "../plugins/config-state.js";
77
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
8-
import { getActivePluginRuntimeSubagentMode } from "../plugins/runtime.js";
8+
import {
9+
getActivePluginRegistryWorkspaceDir,
10+
getActivePluginRuntimeSubagentMode,
11+
} from "../plugins/runtime.js";
12+
import { getLoadedRuntimePluginRegistry } from "../plugins/active-runtime-registry.js";
913
import { ensureStandaloneRuntimePluginRegistryLoaded } from "../plugins/runtime/standalone-runtime-registry-loader.js";
1014
import { resolveUserPath } from "../utils.js";
1115

@@ -52,16 +56,32 @@ export function ensureRuntimePluginsLoaded(params: {
5256
const allowGatewaySubagentBinding =
5357
params.allowGatewaySubagentBinding === true ||
5458
getActivePluginRuntimeSubagentMode() === "gateway-bindable";
59+
const baseLoadOptions = {
60+
config: params.config,
61+
workspaceDir,
62+
...(startupPluginIds === undefined ? {} : { onlyPluginIds: startupPluginIds }),
63+
...(startupPluginIds === undefined ? {} : { forceFullRuntimeForChannelPlugins: true }),
64+
runtimeOptions: allowGatewaySubagentBinding
65+
? { allowGatewaySubagentBinding: true }
66+
: undefined,
67+
};
68+
const activeWorkspaceDir =
69+
startupPluginIds === undefined ? undefined : getActivePluginRegistryWorkspaceDir();
70+
if (activeWorkspaceDir && activeWorkspaceDir !== workspaceDir) {
71+
const activeRegistry = getLoadedRuntimePluginRegistry({
72+
requiredPluginIds: startupPluginIds,
73+
loadOptions: {
74+
...baseLoadOptions,
75+
workspaceDir: activeWorkspaceDir,
76+
},
77+
workspaceDir: activeWorkspaceDir,
78+
});
79+
if (activeRegistry) {
80+
return;
81+
}
82+
}
5583
ensureStandaloneRuntimePluginRegistryLoaded({
5684
requiredPluginIds: startupPluginIds,
57-
loadOptions: {
58-
config: params.config,
59-
workspaceDir,
60-
...(startupPluginIds === undefined ? {} : { onlyPluginIds: startupPluginIds }),
61-
...(startupPluginIds === undefined ? {} : { forceFullRuntimeForChannelPlugins: true }),
62-
runtimeOptions: allowGatewaySubagentBinding
63-
? { allowGatewaySubagentBinding: true }
64-
: undefined,
65-
},
85+
loadOptions: baseLoadOptions,
6686
});
6787
}

src/plugins/loader.runtime-registry.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,68 @@ describe("getCompatibleActivePluginRegistry", () => {
665665
}),
666666
).toBeUndefined();
667667
});
668+
669+
it("reuses a scoped gateway startup registry for compatible provider-hook subset loads", () => {
670+
const registry = createEmptyPluginRegistry();
671+
registry.plugins.push(
672+
createLoadedPluginRecord("acpx"),
673+
createLoadedPluginRecord("telegram"),
674+
createLoadedPluginRecord("travel-hub"),
675+
);
676+
registry.coreGatewayMethodNames = ["sessions.get", "sessions.list"];
677+
const config = {
678+
plugins: {
679+
allow: ["acpx", "telegram", "travel-hub"],
680+
},
681+
};
682+
const startupOptions = {
683+
config,
684+
activationSourceConfig: config,
685+
autoEnabledReasons: {},
686+
workspaceDir: "/tmp/workspace-a",
687+
onlyPluginIds: ["acpx", "telegram", "travel-hub"],
688+
coreGatewayMethodNames: ["sessions.get", "sessions.list"],
689+
runtimeOptions: {
690+
allowGatewaySubagentBinding: true,
691+
},
692+
preferBuiltPluginArtifacts: true,
693+
};
694+
const { cacheKey } = testing.resolvePluginLoadCacheContext(startupOptions);
695+
setActivePluginRegistry(
696+
registry,
697+
cacheKey,
698+
"gateway-bindable",
699+
"/tmp/workspace-a",
700+
startupOptions.onlyPluginIds,
701+
);
702+
703+
expect(
704+
testing.getCompatibleActivePluginRegistry({
705+
config,
706+
workspaceDir: "/tmp/workspace-a",
707+
onlyPluginIds: ["travel-hub"],
708+
runtimeOptions: {
709+
allowGatewaySubagentBinding: true,
710+
},
711+
}),
712+
).toBe(registry);
713+
714+
expect(
715+
testing.getCompatibleActivePluginRegistry({
716+
config: {
717+
plugins: {
718+
allow: ["acpx", "telegram", "travel-hub"],
719+
load: { paths: ["/tmp/changed.js"] },
720+
},
721+
},
722+
workspaceDir: "/tmp/workspace-a",
723+
onlyPluginIds: ["travel-hub"],
724+
runtimeOptions: {
725+
allowGatewaySubagentBinding: true,
726+
},
727+
}),
728+
).toBeUndefined();
729+
});
668730
});
669731

670732
describe("resolveRuntimePluginRegistry", () => {

0 commit comments

Comments
 (0)