diff --git a/CHANGELOG.md b/CHANGELOG.md index bf944f598f2..01ed67e8fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/CLI: cache plugin CLI registration entries per command program so completion state generation does not repeat the full plugin sweep in one invocation. Thanks @ScientificProgrammer. - Plugins: reuse gateway-bindable plugin loader cache entries for later default-mode loads without serving default-built registries to gateway-bound requests, reducing repeated plugin registration during dispatch. Refs #61756. Thanks @DmitryPogodaev. - Gateway/secrets: include the caught error message in `secrets.reload` and `secrets.resolve` warning logs while keeping RPC errors generic, so operators can diagnose reload and permission failures. Thanks @davidangularme. - Anthropic-compatible streams: recover text deltas that arrive before their matching content block, so Kimi Code and similar providers do not finish as empty `incomplete_result` replies. Fixes #76007. Thanks @vliuyt. diff --git a/src/plugins/cli.test.ts b/src/plugins/cli.test.ts index 3de5e77608d..c53b7dcb912 100644 --- a/src/plugins/cli.test.ts +++ b/src/plugins/cli.test.ts @@ -197,6 +197,26 @@ describe("registerPluginCliCommands", () => { ); }); + it("reuses loaded plugin CLI entries on repeat calls for the same program", async () => { + const program = createProgram(); + + await registerPluginCliCommands(program, {} as OpenClawConfig); + await registerPluginCliCommands(program, {} as OpenClawConfig); + + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(1); + }); + + it("reloads plugin CLI entries when the requested primary command changes", async () => { + const program = createProgram(); + + await registerPluginCliCommands(program, {} as OpenClawConfig, undefined, undefined, { + primary: "memory", + }); + await registerPluginCliCommands(program, {} as OpenClawConfig); + + expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(2); + }); + it("loads plugin CLI commands from the auto-enabled config snapshot", async () => { const { rawConfig, autoEnabledConfig } = createAutoEnabledCliFixture(); mocks.applyPluginAutoEnable.mockReturnValue({ diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index 27ea2595921..0835c173830 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -17,6 +17,19 @@ type RegisterPluginCliOptions = { primary?: string | null; }; +type PluginCliRegistrationEntries = Awaited< + ReturnType +>; + +const PLUGIN_CLI_ENTRIES_CACHE_KEY = Symbol.for("openclaw.plugin-cli-registration-entries-cache"); + +interface ProgramWithEntriesCache { + [PLUGIN_CLI_ENTRIES_CACHE_KEY]?: { + primary: string | undefined; + entries: PluginCliRegistrationEntries; + }; +} + const logger = createPluginCliLogger(); export const loadValidatedConfigForPluginRegistration = @@ -46,21 +59,27 @@ export async function registerPluginCliCommands( const mode = options?.mode ?? "eager"; const primary = options?.primary ?? undefined; - await registerPluginCliCommandGroups( - program, - await loadPluginCliRegistrationEntriesWithDefaults({ + const programWithCache = program as Command & ProgramWithEntriesCache; + const cached = programWithCache[PLUGIN_CLI_ENTRIES_CACHE_KEY]; + let entries: PluginCliRegistrationEntries; + if (cached && cached.primary === primary) { + entries = cached.entries; + } else { + entries = await loadPluginCliRegistrationEntriesWithDefaults({ cfg, env, loaderOptions, primaryCommand: primary, - }), - { - mode, - primary, - existingCommands: new Set(program.commands.map((cmd) => cmd.name())), - logger, - }, - ); + }); + programWithCache[PLUGIN_CLI_ENTRIES_CACHE_KEY] = { primary, entries }; + } + + await registerPluginCliCommandGroups(program, entries, { + mode, + primary, + existingCommands: new Set(program.commands.map((cmd) => cmd.name())), + logger, + }); } export async function registerPluginCliCommandsFromValidatedConfig(