fix(plugins): preserve interactive dedupe on cache restore

This commit is contained in:
Peter Steinberger
2026-04-24 23:02:00 +01:00
parent 2f23b84dc4
commit 535a1d699e
5 changed files with 31 additions and 8 deletions

View File

@@ -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.

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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", () => {

View File

@@ -213,8 +213,8 @@ export class PluginLoadReentryError extends Error {
type CachedPluginState = {
registry: PluginRegistry;
detachedTaskRuntimeRegistration: ReturnType<typeof getDetachedTaskLifecycleRuntimeRegistration>;
commands: ReturnType<typeof listRegisteredPluginCommands>;
interactiveHandlers: ReturnType<typeof listPluginInteractiveHandlers>;
commands?: ReturnType<typeof listRegisteredPluginCommands>;
interactiveHandlers?: ReturnType<typeof listPluginInteractiveHandlers>;
memoryCapability: ReturnType<typeof getMemoryCapabilityRegistration>;
memoryCorpusSupplements: ReturnType<typeof listMemoryCorpusSupplements>;
agentHarnesses: ReturnType<typeof listRegisteredAgentHarnesses>;
@@ -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,