diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8e1edac79..748085a8b54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Discord/gateway: prevent startup from getting stuck at `awaiting gateway readiness` when Carbon gateway registration races with a lifecycle reconnect. Fixes #52372. (#68159) Thanks @IVY-AI-gif. -- Plugins/cache: restore plugin command and interactive handler registries on loader cache hits, so cached external plugins keep slash commands and callback handlers available after reloads. Fixes #71100. Thanks @BomBastikDE. +- Plugins/cache: restore plugin command and interactive handler registries on loader cache hits without resetting interactive callback dedupe, so cached external plugins keep slash commands and callback handlers available after reloads. Fixes #71100. Thanks @BomBastikDE. - Gateway/OpenAI-compatible: report non-zero token usage for `/v1/chat/completions` when the agent run has only last-call usage metadata available. Fixes #71118. (#71242) Thanks @RenzoMXD. - Plugin SDK/tool-result transforms: restrict harness tool-result middleware to bundled plugins, fail closed on middleware errors, validate rewritten result shapes, preserve Pi per-call ids, and keep Codex media trust checks anchored to raw tool provenance. Thanks @vincentkoc. - Gateway/MCP loopback: apply owner-only tool policy and run before-tool-call hooks on `127.0.0.1/mcp` `tools/list` and `tools/call`, so non-owner bearer callers can no longer see or invoke owner-only tools such as `cron`, `gateway`, and `nodes`, matching the existing HTTP `/tools/invoke` and embedded-agent paths. (#71159) Thanks @mmaps. diff --git a/src/plugins/interactive-registry.ts b/src/plugins/interactive-registry.ts index bcc2b4d2f1d..1563b84c913 100644 --- a/src/plugins/interactive-registry.ts +++ b/src/plugins/interactive-registry.ts @@ -6,6 +6,7 @@ import { validatePluginInteractiveNamespace, } from "./interactive-shared.js"; import { + clearPluginInteractiveHandlerRegistrationsState, clearPluginInteractiveHandlersState, getPluginInteractiveHandlersState, type RegisteredInteractiveHandler, @@ -62,6 +63,10 @@ export function clearPluginInteractiveHandlers(): void { clearPluginInteractiveHandlersState(); } +export function clearPluginInteractiveHandlerRegistrations(): void { + clearPluginInteractiveHandlerRegistrationsState(); +} + export function clearPluginInteractiveHandlersForPlugin(pluginId: string): void { const interactiveHandlers = getPluginInteractiveHandlersState(); for (const [key, value] of interactiveHandlers.entries()) { @@ -78,7 +83,7 @@ export function listPluginInteractiveHandlers(): RegisteredInteractiveHandler[] export function restorePluginInteractiveHandlers( registrations: readonly RegisteredInteractiveHandler[], ): void { - clearPluginInteractiveHandlers(); + clearPluginInteractiveHandlerRegistrations(); const interactiveHandlers = getPluginInteractiveHandlersState(); for (const registration of registrations) { const namespace = normalizePluginInteractiveNamespace(registration.namespace); diff --git a/src/plugins/interactive-state.ts b/src/plugins/interactive-state.ts index 26e4ea9d288..a1d02b65446 100644 --- a/src/plugins/interactive-state.ts +++ b/src/plugins/interactive-state.ts @@ -110,7 +110,11 @@ export function releasePluginInteractiveCallbackDedupe(dedupeKey: string | undef } export function clearPluginInteractiveHandlersState(): void { - getPluginInteractiveHandlersState().clear(); + clearPluginInteractiveHandlerRegistrationsState(); getPluginInteractiveCallbackDedupeState().clear(); getState().inflightCallbackDedupe.clear(); } + +export function clearPluginInteractiveHandlerRegistrationsState(): void { + getPluginInteractiveHandlersState().clear(); +} diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index fe59944f3df..64dd8e9680f 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -26,9 +26,14 @@ import { clearPluginCommands, getPluginCommandSpecs } from "./command-registry-s import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js"; import { createHookRunner } from "./hooks.js"; import { + clearPluginInteractiveHandlerRegistrations, clearPluginInteractiveHandlers, resolvePluginInteractiveNamespaceMatch, } from "./interactive-registry.js"; +import { + claimPluginInteractiveCallbackDedupe, + commitPluginInteractiveCallbackDedupe, +} from "./interactive-state.js"; import { __testing, clearPluginLoaderCache, @@ -3217,8 +3222,16 @@ module.exports = { id: "throws-after-import", register() {} };`, ]); expect(resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")).toBeDefined(); + const dedupeKey = "telegram:hue:callback-1"; + expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_000)).toBe(true); + commitPluginInteractiveCallbackDedupe(dedupeKey, 1_000); + expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_001)).toBe(false); + + loadOpenClawPlugins(loadOptions); + expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_002)).toBe(false); + clearPluginCommands(); - clearPluginInteractiveHandlers(); + clearPluginInteractiveHandlerRegistrations(); expect(getPluginCommandSpecs()).toEqual([]); expect(resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")).toBeNull(); @@ -3234,6 +3247,7 @@ module.exports = { id: "throws-after-import", register() {} };`, namespace: "hue", channel: "telegram", }); + expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_003)).toBe(false); }); it("clears stale detached task runtime registrations on active reloads when no plugin re-registers one", () => { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 91c523afb31..85e1c8e7d5a 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -213,8 +213,8 @@ export class PluginLoadReentryError extends Error { type CachedPluginState = { registry: PluginRegistry; detachedTaskRuntimeRegistration: ReturnType; - commands: ReturnType; - interactiveHandlers: ReturnType; + commands?: ReturnType; + interactiveHandlers?: ReturnType; memoryCapability: ReturnType; memoryCorpusSupplements: ReturnType; agentHarnesses: ReturnType; @@ -1883,10 +1883,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const cached = getCachedPluginRegistry(cacheKey); if (cached) { restoreRegisteredAgentHarnesses(cached.agentHarnesses); - restorePluginCommands(cached.commands); + restorePluginCommands(cached.commands ?? []); restoreRegisteredCompactionProviders(cached.compactionProviders); restoreDetachedTaskLifecycleRuntimeRegistration(cached.detachedTaskRuntimeRegistration); - restorePluginInteractiveHandlers(cached.interactiveHandlers); + restorePluginInteractiveHandlers(cached.interactiveHandlers ?? []); restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders); restoreMemoryPluginState({ capability: cached.memoryCapability,