惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
博客园 - 聂微东
博客园 - 【当耐特】
博客园_首页
The Cloudflare Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Apple Machine Learning Research
Apple Machine Learning Research
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
雷峰网
雷峰网
量子位
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
IT之家
IT之家
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
V
Visual Studio Blog
F
Fortinet All Blogs
Martin Fowler
Martin Fowler

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: cache plugin tool factories by context · openclaw/op...
steipete · 2026-05-02 · via Recent Commits to openclaw:main

@@ -26,6 +26,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({

26262727

let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;

2828

let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;

29+

let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache;

2930

let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;

3031

let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;

3132

let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;

@@ -58,14 +59,15 @@ function createContext() {

5859

}

59606061

function createResolveToolsParams(params?: {

62+

context?: ReturnType<typeof createContext> & Record<string, unknown>;

6163

toolAllowlist?: readonly string[];

6264

existingToolNames?: Set<string>;

6365

env?: NodeJS.ProcessEnv;

6466

suppressNameConflicts?: boolean;

6567

allowGatewaySubagentBinding?: boolean;

6668

}) {

6769

return {

68-

context: createContext() as never,

70+

context: (params?.context ?? createContext()) as never,

6971

...(params?.toolAllowlist ? { toolAllowlist: [...params.toolAllowlist] } : {}),

7072

...(params?.existingToolNames ? { existingToolNames: params.existingToolNames } : {}),

7173

...(params?.env ? { env: params.env } : {}),

@@ -360,7 +362,8 @@ function expectConflictingCoreNameResolution(params: {

360362361363

describe("resolvePluginTools optional tools", () => {

362364

beforeAll(async () => {

363-

({ buildPluginToolMetadataKey, resolvePluginTools } = await import("./tools.js"));

365+

({ buildPluginToolMetadataKey, resetPluginToolFactoryCache, resolvePluginTools } =

366+

await import("./tools.js"));

364367

({ pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry } =

365368

await import("./runtime.js"));

366369

({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =

@@ -380,11 +383,13 @@ describe("resolvePluginTools optional tools", () => {

380383

}));

381384

resetPluginRuntimeStateForTest?.();

382385

clearCurrentPluginMetadataSnapshot?.();

386+

resetPluginToolFactoryCache?.();

383387

});

384388385389

afterEach(() => {

386390

resetPluginRuntimeStateForTest?.();

387391

clearCurrentPluginMetadataSnapshot?.();

392+

resetPluginToolFactoryCache?.();

388393

setLoggerOverride(null);

389394

loggingState.rawConsole = null;

390395

resetLogger();

@@ -812,6 +817,163 @@ describe("resolvePluginTools optional tools", () => {

812817

expect(warnSpy).not.toHaveBeenCalled();

813818

});

814819820+

it("caches plugin tool factory results for equivalent request context", () => {

821+

const factory = vi.fn(() => makeTool("cached_tool"));

822+

setRegistry([

823+

{

824+

pluginId: "cache-test",

825+

optional: false,

826+

source: "/tmp/cache-test.js",

827+

names: ["cached_tool"],

828+

factory,

829+

},

830+

]);

831+832+

const first = resolvePluginTools(createResolveToolsParams({ context: createContext() }));

833+

const second = resolvePluginTools(createResolveToolsParams({ context: createContext() }));

834+835+

expectResolvedToolNames(first, ["cached_tool"]);

836+

expectResolvedToolNames(second, ["cached_tool"]);

837+

expect(factory).toHaveBeenCalledTimes(1);

838+

expect(second[0]).toBe(first[0]);

839+

});

840+841+

it("does not reuse plugin tool factory results across sandbox context changes", () => {

842+

const factory = vi.fn((rawCtx: unknown) => {

843+

const ctx = rawCtx as { sandboxed?: boolean };

844+

return ctx.sandboxed ? null : makeTool("sandbox_sensitive_tool");

845+

});

846+

setRegistry([

847+

{

848+

pluginId: "sandbox-sensitive",

849+

optional: false,

850+

source: "/tmp/sandbox-sensitive.js",

851+

names: ["sandbox_sensitive_tool"],

852+

factory,

853+

},

854+

]);

855+856+

const hostTools = resolvePluginTools(

857+

createResolveToolsParams({

858+

context: { ...createContext(), sandboxed: false },

859+

}),

860+

);

861+

const sandboxedTools = resolvePluginTools(

862+

createResolveToolsParams({

863+

context: { ...createContext(), sandboxed: true },

864+

}),

865+

);

866+867+

expectResolvedToolNames(hostTools, ["sandbox_sensitive_tool"]);

868+

expect(sandboxedTools).toEqual([]);

869+

expect(factory).toHaveBeenCalledTimes(2);

870+

});

871+872+

it("does not reuse plugin tool factory results across runtime config changes", () => {

873+

const firstRuntimeConfig = {

874+

...createContext().config,

875+

plugins: { ...createContext().config.plugins, allow: ["runtime_sensitive_tool"] },

876+

};

877+

const secondRuntimeConfig = {

878+

...createContext().config,

879+

plugins: { ...createContext().config.plugins, allow: ["runtime_sensitive_next_tool"] },

880+

};

881+

const factory = vi.fn((rawCtx: unknown) => {

882+

const ctx = rawCtx as { runtimeConfig?: { plugins?: { allow?: string[] } } };

883+

return makeTool(ctx.runtimeConfig?.plugins?.allow?.[0] ?? "runtime_missing_tool");

884+

});

885+

setRegistry([

886+

{

887+

pluginId: "runtime-sensitive",

888+

optional: false,

889+

source: "/tmp/runtime-sensitive.js",

890+

names: ["runtime_sensitive_tool", "runtime_sensitive_next_tool"],

891+

factory,

892+

},

893+

]);

894+895+

const first = resolvePluginTools(

896+

createResolveToolsParams({

897+

context: { ...createContext(), runtimeConfig: firstRuntimeConfig as never },

898+

}),

899+

);

900+

const second = resolvePluginTools(

901+

createResolveToolsParams({

902+

context: { ...createContext(), runtimeConfig: secondRuntimeConfig as never },

903+

}),

904+

);

905+906+

expectResolvedToolNames(first, ["runtime_sensitive_tool"]);

907+

expectResolvedToolNames(second, ["runtime_sensitive_next_tool"]);

908+

expect(factory).toHaveBeenCalledTimes(2);

909+

});

910+911+

it("reuses plugin tool factory results when only runtime config getter identity changes", () => {

912+

const runtimeConfig = {

913+

...createContext().config,

914+

plugins: { ...createContext().config.plugins, allow: ["getter_sensitive_tool"] },

915+

};

916+

const factory = vi.fn((rawCtx: unknown) => {

917+

const ctx = rawCtx as { getRuntimeConfig?: () => { plugins?: { allow?: string[] } } };

918+

return makeTool(ctx.getRuntimeConfig?.()?.plugins?.allow?.[0] ?? "getter_missing_tool");

919+

});

920+

setRegistry([

921+

{

922+

pluginId: "getter-sensitive",

923+

optional: false,

924+

source: "/tmp/getter-sensitive.js",

925+

names: ["getter_sensitive_tool"],

926+

factory,

927+

},

928+

]);

929+930+

const context = createContext();

931+

const first = resolvePluginTools(

932+

createResolveToolsParams({

933+

context: { ...context, getRuntimeConfig: () => runtimeConfig as never },

934+

}),

935+

);

936+

const second = resolvePluginTools(

937+

createResolveToolsParams({

938+

context: { ...context, getRuntimeConfig: () => runtimeConfig as never },

939+

}),

940+

);

941+942+

expectResolvedToolNames(first, ["getter_sensitive_tool"]);

943+

expectResolvedToolNames(second, ["getter_sensitive_tool"]);

944+

expect(factory).toHaveBeenCalledTimes(1);

945+

});

946+947+

it("reads live runtime config once per plugin tool resolution for cache keys", () => {

948+

const runtimeConfig = createContext().config;

949+

const getRuntimeConfig = vi.fn(() => runtimeConfig);

950+

setRegistry([

951+

{

952+

pluginId: "getter-a",

953+

optional: false,

954+

source: "/tmp/getter-a.js",

955+

names: ["getter_a_tool"],

956+

factory: () => makeTool("getter_a_tool"),

957+

},

958+

{

959+

pluginId: "getter-b",

960+

optional: false,

961+

source: "/tmp/getter-b.js",

962+

names: ["getter_b_tool"],

963+

factory: () => makeTool("getter_b_tool"),

964+

},

965+

]);

966+967+

const tools = resolvePluginTools(

968+

createResolveToolsParams({

969+

context: { ...createContext(), getRuntimeConfig: getRuntimeConfig as never },

970+

}),

971+

);

972+973+

expectResolvedToolNames(tools, ["getter_a_tool", "getter_b_tool"]);

974+

expect(getRuntimeConfig).toHaveBeenCalledTimes(1);

975+

});

976+815977

it("skips factory-returned tools outside the manifest tool contract", () => {

816978

const registry = setRegistry([

817979

{