mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix: memoize plugin descriptor config keys (#76240)
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
93
src/plugins/tool-descriptor-cache.test.ts
Normal file
93
src/plugins/tool-descriptor-cache.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
resolveRuntimeConfigCacheKey: vi.fn((value: unknown) => {
|
||||
const id =
|
||||
value && typeof value === "object" && "id" in value
|
||||
? String((value as { id?: unknown }).id)
|
||||
: "config";
|
||||
return `config:${id}`;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
resolveRuntimeConfigCacheKey: hoisted.resolveRuntimeConfigCacheKey,
|
||||
}));
|
||||
|
||||
import {
|
||||
buildPluginToolDescriptorCacheKey,
|
||||
createPluginToolDescriptorConfigCacheKeyMemo,
|
||||
resetPluginToolDescriptorCache,
|
||||
} from "./tool-descriptor-cache.js";
|
||||
|
||||
describe("plugin tool descriptor cache keys", () => {
|
||||
afterEach(() => {
|
||||
hoisted.resolveRuntimeConfigCacheKey.mockClear();
|
||||
resetPluginToolDescriptorCache();
|
||||
});
|
||||
|
||||
it("memoizes config cache keys across plugin descriptor keys in one resolution pass", () => {
|
||||
const config = {
|
||||
id: "runtime",
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
|
||||
|
||||
for (let index = 0; index < 25; index += 1) {
|
||||
buildPluginToolDescriptorCacheKey({
|
||||
pluginId: `plugin-${index}`,
|
||||
source: `/tmp/plugin-${index}.js`,
|
||||
contractToolNames: [`tool_${index}`],
|
||||
ctx: {
|
||||
config,
|
||||
runtimeConfig: config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main",
|
||||
sessionId: "session",
|
||||
},
|
||||
currentRuntimeConfig: config,
|
||||
configCacheKeyMemo,
|
||||
});
|
||||
}
|
||||
|
||||
expect(hoisted.resolveRuntimeConfigCacheKey).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps distinct config objects distinct within the memo", () => {
|
||||
const firstConfig = { id: "first" } as never;
|
||||
const secondConfig = { id: "second" } as never;
|
||||
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
|
||||
|
||||
const firstKey = buildPluginToolDescriptorCacheKey({
|
||||
pluginId: "demo",
|
||||
source: "/tmp/demo.js",
|
||||
contractToolNames: ["demo"],
|
||||
ctx: {
|
||||
config: firstConfig,
|
||||
runtimeConfig: firstConfig,
|
||||
},
|
||||
currentRuntimeConfig: firstConfig,
|
||||
configCacheKeyMemo,
|
||||
});
|
||||
const secondKey = buildPluginToolDescriptorCacheKey({
|
||||
pluginId: "demo",
|
||||
source: "/tmp/demo.js",
|
||||
contractToolNames: ["demo"],
|
||||
ctx: {
|
||||
config: secondConfig,
|
||||
runtimeConfig: secondConfig,
|
||||
},
|
||||
currentRuntimeConfig: secondConfig,
|
||||
configCacheKeyMemo,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimeConfigCacheKey).toHaveBeenCalledTimes(2);
|
||||
expect(firstKey).not.toBe(secondKey);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,12 @@ const descriptorCache = new Map<string, CachedPluginToolDescriptor[]>();
|
||||
let descriptorCacheObjectIds = new WeakMap<object, number>();
|
||||
let nextDescriptorCacheObjectId = 1;
|
||||
|
||||
export type PluginToolDescriptorConfigCacheKeyMemo = WeakMap<object, string | number | null>;
|
||||
|
||||
export function createPluginToolDescriptorConfigCacheKeyMemo(): PluginToolDescriptorConfigCacheKeyMemo {
|
||||
return new WeakMap();
|
||||
}
|
||||
|
||||
export function resetPluginToolDescriptorCache(): void {
|
||||
descriptorCache.clear();
|
||||
descriptorCacheObjectIds = new WeakMap();
|
||||
@@ -49,26 +55,38 @@ function getDescriptorCacheObjectId(value: object | null | undefined): number |
|
||||
|
||||
function getDescriptorConfigCacheKey(
|
||||
value: PluginLoadOptions["config"] | null | undefined,
|
||||
memo?: PluginToolDescriptorConfigCacheKeyMemo,
|
||||
): string | number | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return resolveRuntimeConfigCacheKey(value);
|
||||
} catch {
|
||||
return getDescriptorCacheObjectId(value);
|
||||
const cached = memo?.get(value);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
let resolved: string | number | null;
|
||||
try {
|
||||
resolved = resolveRuntimeConfigCacheKey(value);
|
||||
} catch {
|
||||
resolved = getDescriptorCacheObjectId(value);
|
||||
}
|
||||
memo?.set(value, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function buildDescriptorContextCacheKey(params: {
|
||||
ctx: OpenClawPluginToolContext;
|
||||
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
||||
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
|
||||
}): string {
|
||||
const { ctx } = params;
|
||||
return JSON.stringify({
|
||||
config: getDescriptorConfigCacheKey(ctx.config),
|
||||
runtimeConfig: getDescriptorConfigCacheKey(ctx.runtimeConfig),
|
||||
currentRuntimeConfig: getDescriptorConfigCacheKey(params.currentRuntimeConfig),
|
||||
config: getDescriptorConfigCacheKey(ctx.config, params.configCacheKeyMemo),
|
||||
runtimeConfig: getDescriptorConfigCacheKey(ctx.runtimeConfig, params.configCacheKeyMemo),
|
||||
currentRuntimeConfig: getDescriptorConfigCacheKey(
|
||||
params.currentRuntimeConfig,
|
||||
params.configCacheKeyMemo,
|
||||
),
|
||||
fsPolicy: ctx.fsPolicy ?? null,
|
||||
workspaceDir: ctx.workspaceDir ?? null,
|
||||
agentDir: ctx.agentDir ?? null,
|
||||
@@ -92,6 +110,7 @@ export function buildPluginToolDescriptorCacheKey(params: {
|
||||
contractToolNames: readonly string[];
|
||||
ctx: OpenClawPluginToolContext;
|
||||
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
||||
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
version: PLUGIN_TOOL_DESCRIPTOR_CACHE_VERSION,
|
||||
@@ -103,6 +122,7 @@ export function buildPluginToolDescriptorCacheKey(params: {
|
||||
context: buildDescriptorContextCacheKey({
|
||||
ctx: params.ctx,
|
||||
currentRuntimeConfig: params.currentRuntimeConfig,
|
||||
configCacheKeyMemo: params.configCacheKeyMemo,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
|
||||
import {
|
||||
buildPluginToolDescriptorCacheKey,
|
||||
capturePluginToolDescriptor,
|
||||
createPluginToolDescriptorConfigCacheKeyMemo,
|
||||
readCachedPluginToolDescriptors,
|
||||
type CachedPluginToolDescriptor,
|
||||
type PluginToolDescriptorConfigCacheKeyMemo,
|
||||
writeCachedPluginToolDescriptors,
|
||||
} from "./tool-descriptor-cache.js";
|
||||
import type { OpenClawPluginToolContext } from "./types.js";
|
||||
@@ -395,6 +397,7 @@ function buildPluginDescriptorCacheKey(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
ctx: OpenClawPluginToolContext;
|
||||
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
||||
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
|
||||
}): string {
|
||||
return buildPluginToolDescriptorCacheKey({
|
||||
pluginId: params.plugin.id,
|
||||
@@ -403,6 +406,7 @@ function buildPluginDescriptorCacheKey(params: {
|
||||
contractToolNames: params.plugin.contracts?.tools ?? [],
|
||||
ctx: params.ctx,
|
||||
currentRuntimeConfig: params.currentRuntimeConfig,
|
||||
configCacheKeyMemo: params.configCacheKeyMemo,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -493,6 +497,7 @@ function resolveCachedPluginTools(params: {
|
||||
loadContext: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
||||
runtimeOptions: PluginLoadOptions["runtimeOptions"];
|
||||
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
||||
configCacheKeyMemo: PluginToolDescriptorConfigCacheKeyMemo;
|
||||
}): { tools: AnyAgentTool[]; handledPluginIds: Set<string> } {
|
||||
const tools: AnyAgentTool[] = [];
|
||||
const handledPluginIds = new Set<string>();
|
||||
@@ -535,6 +540,7 @@ function resolveCachedPluginTools(params: {
|
||||
plugin,
|
||||
ctx: params.ctx,
|
||||
currentRuntimeConfig: params.currentRuntimeConfig,
|
||||
configCacheKeyMemo: params.configCacheKeyMemo,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
@@ -714,6 +720,7 @@ export function resolvePluginTools(params: {
|
||||
const existing = params.existingToolNames ?? new Set<string>();
|
||||
const existingNormalized = new Set(Array.from(existing, (tool) => normalizeToolName(tool)));
|
||||
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
||||
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
|
||||
let currentRuntimeConfigForDescriptorCache: PluginLoadOptions["config"] | null | undefined =
|
||||
params.context.runtimeConfig;
|
||||
if (currentRuntimeConfigForDescriptorCache === undefined && params.context.getRuntimeConfig) {
|
||||
@@ -737,6 +744,7 @@ export function resolvePluginTools(params: {
|
||||
loadContext: context,
|
||||
runtimeOptions,
|
||||
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
|
||||
configCacheKeyMemo,
|
||||
});
|
||||
tools.push(...cached.tools);
|
||||
const runtimePluginIds = onlyPluginIds.filter(
|
||||
@@ -922,6 +930,7 @@ export function resolvePluginTools(params: {
|
||||
plugin: manifestPlugin,
|
||||
ctx: params.context,
|
||||
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
|
||||
configCacheKeyMemo,
|
||||
}),
|
||||
descriptors,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user