Skip to content

Commit 28bc96c

Browse files
committed
fix: memoize plugin descriptor config keys
1 parent 10448a0 commit 28bc96c

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
1717
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
1818
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
19+
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
1920
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
2021
- Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof.
2122
- Gateway/status: add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway, so frozen or port-conflict reports include the data needed for root-cause triage. Refs #49012. Thanks @vincentkoc.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
const hoisted = vi.hoisted(() => ({
4+
resolveRuntimeConfigCacheKey: vi.fn((value: unknown) => {
5+
const id =
6+
value && typeof value === "object" && "id" in value
7+
? String((value as { id?: unknown }).id)
8+
: "config";
9+
return `config:${id}`;
10+
}),
11+
}));
12+
13+
vi.mock("../config/runtime-snapshot.js", () => ({
14+
resolveRuntimeConfigCacheKey: hoisted.resolveRuntimeConfigCacheKey,
15+
}));
16+
17+
import {
18+
buildPluginToolDescriptorCacheKey,
19+
createPluginToolDescriptorConfigCacheKeyMemo,
20+
resetPluginToolDescriptorCache,
21+
} from "./tool-descriptor-cache.js";
22+
23+
describe("plugin tool descriptor cache keys", () => {
24+
afterEach(() => {
25+
hoisted.resolveRuntimeConfigCacheKey.mockClear();
26+
resetPluginToolDescriptorCache();
27+
});
28+
29+
it("memoizes config cache keys across plugin descriptor keys in one resolution pass", () => {
30+
const config = {
31+
id: "runtime",
32+
plugins: {
33+
entries: {
34+
demo: { enabled: true },
35+
},
36+
},
37+
} as never;
38+
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
39+
40+
for (let index = 0; index < 25; index += 1) {
41+
buildPluginToolDescriptorCacheKey({
42+
pluginId: `plugin-${index}`,
43+
source: `/tmp/plugin-${index}.js`,
44+
contractToolNames: [`tool_${index}`],
45+
ctx: {
46+
config,
47+
runtimeConfig: config,
48+
workspaceDir: "/tmp/workspace",
49+
agentDir: "/tmp/agent",
50+
agentId: "main",
51+
sessionKey: "agent:main",
52+
sessionId: "session",
53+
},
54+
currentRuntimeConfig: config,
55+
configCacheKeyMemo,
56+
});
57+
}
58+
59+
expect(hoisted.resolveRuntimeConfigCacheKey).toHaveBeenCalledTimes(1);
60+
});
61+
62+
it("keeps distinct config objects distinct within the memo", () => {
63+
const firstConfig = { id: "first" } as never;
64+
const secondConfig = { id: "second" } as never;
65+
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
66+
67+
const firstKey = buildPluginToolDescriptorCacheKey({
68+
pluginId: "demo",
69+
source: "/tmp/demo.js",
70+
contractToolNames: ["demo"],
71+
ctx: {
72+
config: firstConfig,
73+
runtimeConfig: firstConfig,
74+
},
75+
currentRuntimeConfig: firstConfig,
76+
configCacheKeyMemo,
77+
});
78+
const secondKey = buildPluginToolDescriptorCacheKey({
79+
pluginId: "demo",
80+
source: "/tmp/demo.js",
81+
contractToolNames: ["demo"],
82+
ctx: {
83+
config: secondConfig,
84+
runtimeConfig: secondConfig,
85+
},
86+
currentRuntimeConfig: secondConfig,
87+
configCacheKeyMemo,
88+
});
89+
90+
expect(hoisted.resolveRuntimeConfigCacheKey).toHaveBeenCalledTimes(2);
91+
expect(firstKey).not.toBe(secondKey);
92+
});
93+
});

src/plugins/tool-descriptor-cache.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const descriptorCache = new Map<string, CachedPluginToolDescriptor[]>();
1919
let descriptorCacheObjectIds = new WeakMap<object, number>();
2020
let nextDescriptorCacheObjectId = 1;
2121

22+
export type PluginToolDescriptorConfigCacheKeyMemo = WeakMap<object, string | number | null>;
23+
24+
export function createPluginToolDescriptorConfigCacheKeyMemo(): PluginToolDescriptorConfigCacheKeyMemo {
25+
return new WeakMap();
26+
}
27+
2228
export function resetPluginToolDescriptorCache(): void {
2329
descriptorCache.clear();
2430
descriptorCacheObjectIds = new WeakMap();
@@ -49,26 +55,38 @@ function getDescriptorCacheObjectId(value: object | null | undefined): number |
4955

5056
function getDescriptorConfigCacheKey(
5157
value: PluginLoadOptions["config"] | null | undefined,
58+
memo?: PluginToolDescriptorConfigCacheKeyMemo,
5259
): string | number | null {
5360
if (!value) {
5461
return null;
5562
}
63+
const cached = memo?.get(value);
64+
if (cached !== undefined) {
65+
return cached;
66+
}
67+
let resolved: string | number | null;
5668
try {
57-
return resolveRuntimeConfigCacheKey(value);
69+
resolved = resolveRuntimeConfigCacheKey(value);
5870
} catch {
59-
return getDescriptorCacheObjectId(value);
71+
resolved = getDescriptorCacheObjectId(value);
6072
}
73+
memo?.set(value, resolved);
74+
return resolved;
6175
}
6276

6377
function buildDescriptorContextCacheKey(params: {
6478
ctx: OpenClawPluginToolContext;
6579
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
80+
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
6681
}): string {
6782
const { ctx } = params;
6883
return JSON.stringify({
69-
config: getDescriptorConfigCacheKey(ctx.config),
70-
runtimeConfig: getDescriptorConfigCacheKey(ctx.runtimeConfig),
71-
currentRuntimeConfig: getDescriptorConfigCacheKey(params.currentRuntimeConfig),
84+
config: getDescriptorConfigCacheKey(ctx.config, params.configCacheKeyMemo),
85+
runtimeConfig: getDescriptorConfigCacheKey(ctx.runtimeConfig, params.configCacheKeyMemo),
86+
currentRuntimeConfig: getDescriptorConfigCacheKey(
87+
params.currentRuntimeConfig,
88+
params.configCacheKeyMemo,
89+
),
7290
fsPolicy: ctx.fsPolicy ?? null,
7391
workspaceDir: ctx.workspaceDir ?? null,
7492
agentDir: ctx.agentDir ?? null,
@@ -92,6 +110,7 @@ export function buildPluginToolDescriptorCacheKey(params: {
92110
contractToolNames: readonly string[];
93111
ctx: OpenClawPluginToolContext;
94112
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
113+
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
95114
}): string {
96115
return JSON.stringify({
97116
version: PLUGIN_TOOL_DESCRIPTOR_CACHE_VERSION,
@@ -103,6 +122,7 @@ export function buildPluginToolDescriptorCacheKey(params: {
103122
context: buildDescriptorContextCacheKey({
104123
ctx: params.ctx,
105124
currentRuntimeConfig: params.currentRuntimeConfig,
125+
configCacheKeyMemo: params.configCacheKeyMemo,
106126
}),
107127
});
108128
}

src/plugins/tools.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
2121
import {
2222
buildPluginToolDescriptorCacheKey,
2323
capturePluginToolDescriptor,
24+
createPluginToolDescriptorConfigCacheKeyMemo,
2425
readCachedPluginToolDescriptors,
2526
type CachedPluginToolDescriptor,
27+
type PluginToolDescriptorConfigCacheKeyMemo,
2628
writeCachedPluginToolDescriptors,
2729
} from "./tool-descriptor-cache.js";
2830
import type { OpenClawPluginToolContext } from "./types.js";
@@ -395,6 +397,7 @@ function buildPluginDescriptorCacheKey(params: {
395397
plugin: PluginManifestRecord;
396398
ctx: OpenClawPluginToolContext;
397399
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
400+
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
398401
}): string {
399402
return buildPluginToolDescriptorCacheKey({
400403
pluginId: params.plugin.id,
@@ -403,6 +406,7 @@ function buildPluginDescriptorCacheKey(params: {
403406
contractToolNames: params.plugin.contracts?.tools ?? [],
404407
ctx: params.ctx,
405408
currentRuntimeConfig: params.currentRuntimeConfig,
409+
configCacheKeyMemo: params.configCacheKeyMemo,
406410
});
407411
}
408412

@@ -493,6 +497,7 @@ function resolveCachedPluginTools(params: {
493497
loadContext: ReturnType<typeof resolvePluginRuntimeLoadContext>;
494498
runtimeOptions: PluginLoadOptions["runtimeOptions"];
495499
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
500+
configCacheKeyMemo: PluginToolDescriptorConfigCacheKeyMemo;
496501
}): { tools: AnyAgentTool[]; handledPluginIds: Set<string> } {
497502
const tools: AnyAgentTool[] = [];
498503
const handledPluginIds = new Set<string>();
@@ -535,6 +540,7 @@ function resolveCachedPluginTools(params: {
535540
plugin,
536541
ctx: params.ctx,
537542
currentRuntimeConfig: params.currentRuntimeConfig,
543+
configCacheKeyMemo: params.configCacheKeyMemo,
538544
}),
539545
);
540546
if (
@@ -714,6 +720,7 @@ export function resolvePluginTools(params: {
714720
const existing = params.existingToolNames ?? new Set<string>();
715721
const existingNormalized = new Set(Array.from(existing, (tool) => normalizeToolName(tool)));
716722
const allowlist = normalizeAllowlist(params.toolAllowlist);
723+
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
717724
let currentRuntimeConfigForDescriptorCache: PluginLoadOptions["config"] | null | undefined =
718725
params.context.runtimeConfig;
719726
if (currentRuntimeConfigForDescriptorCache === undefined && params.context.getRuntimeConfig) {
@@ -737,6 +744,7 @@ export function resolvePluginTools(params: {
737744
loadContext: context,
738745
runtimeOptions,
739746
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
747+
configCacheKeyMemo,
740748
});
741749
tools.push(...cached.tools);
742750
const runtimePluginIds = onlyPluginIds.filter(
@@ -922,6 +930,7 @@ export function resolvePluginTools(params: {
922930
plugin: manifestPlugin,
923931
ctx: params.context,
924932
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
933+
configCacheKeyMemo,
925934
}),
926935
descriptors,
927936
});

0 commit comments

Comments
 (0)