// Stores interactive plugin state and dedupe caches. import { createDedupeCache, resolveGlobalDedupeCache } from "../infra/dedupe.js"; import type { DedupeCache } from "../infra/dedupe.js"; import type { PluginInteractiveHandlerRegistration } from "./types.js"; /** Registered interactive handler with owning plugin metadata. */ export type RegisteredInteractiveHandler = PluginInteractiveHandlerRegistration & { pluginId: string; pluginName?: string; pluginRoot?: string; registryOwned?: true; }; type InteractiveState = { interactiveHandlers: Map; callbackDedupe: ReturnType; inflightCallbackDedupe: Set; }; const PLUGIN_INTERACTIVE_STATE_KEY = Symbol.for("openclaw.pluginInteractiveState"); const PLUGIN_INTERACTIVE_CALLBACK_DEDUPE_KEY = Symbol.for( "openclaw.pluginInteractiveCallbackDedupe", ); function createInteractiveCallbackDedupe(): DedupeCache { return resolveGlobalDedupeCache(PLUGIN_INTERACTIVE_CALLBACK_DEDUPE_KEY, { ttlMs: 5 * 60_000, maxSize: 4096, }); } function createInteractiveState(): InteractiveState { return { interactiveHandlers: new Map(), callbackDedupe: createInteractiveCallbackDedupe(), inflightCallbackDedupe: new Set(), }; } function hydrateInteractiveState(value: unknown): InteractiveState { const state = typeof value === "object" && value !== null ? (value as Partial) : ({} as Partial); return { interactiveHandlers: state.interactiveHandlers instanceof Map ? state.interactiveHandlers : new Map(), callbackDedupe: createInteractiveCallbackDedupe(), inflightCallbackDedupe: state.inflightCallbackDedupe instanceof Set ? state.inflightCallbackDedupe : new Set(), }; } function getState() { const globalStore = globalThis as Record; const existing = globalStore[PLUGIN_INTERACTIVE_STATE_KEY]; if (existing !== undefined) { const hydrated = hydrateInteractiveState(existing); globalStore[PLUGIN_INTERACTIVE_STATE_KEY] = hydrated; return hydrated; } const created = createInteractiveState(); globalStore[PLUGIN_INTERACTIVE_STATE_KEY] = created; return created; } /** Returns the process-global plugin interactive handler registry. */ export function getPluginInteractiveHandlersState() { return getState().interactiveHandlers; } function getPluginInteractiveCallbackDedupeState() { return getState().callbackDedupe; } /** Claims an interactive callback dedupe key while the callback is in flight. */ export function claimPluginInteractiveCallbackDedupe( dedupeKey: string | undefined, now = Date.now(), ): boolean { if (!dedupeKey) { return true; } const state = getState(); if (state.inflightCallbackDedupe.has(dedupeKey) || state.callbackDedupe.peek(dedupeKey, now)) { return false; } state.inflightCallbackDedupe.add(dedupeKey); return true; } /** Commits an interactive callback dedupe key after successful handling. */ export function commitPluginInteractiveCallbackDedupe( dedupeKey: string | undefined, now = Date.now(), ): void { if (!dedupeKey) { return; } const state = getState(); state.inflightCallbackDedupe.delete(dedupeKey); state.callbackDedupe.check(dedupeKey, now); } /** Releases an in-flight interactive callback dedupe claim without committing it. */ export function releasePluginInteractiveCallbackDedupe(dedupeKey: string | undefined): void { if (!dedupeKey) { return; } getState().inflightCallbackDedupe.delete(dedupeKey); } /** Clears plugin interactive handlers and callback dedupe state. */ export function clearPluginInteractiveHandlersState(): void { clearPluginInteractiveHandlerRegistrationsState(); getPluginInteractiveCallbackDedupeState().clear(); getState().inflightCallbackDedupe.clear(); } /** Clears only plugin interactive handler registrations. */ export function clearPluginInteractiveHandlerRegistrationsState(): void { getPluginInteractiveHandlersState().clear(); }