UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

336 lines (335 loc) 15.1 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js"; import "./config-C9RxTsn1.js"; import { A as makePluginSessionSchedulerJobKey, E as clearPluginRunContext, N as withPluginHostCleanupTimeout, T as cleanupPluginSessionSchedulerJobs, o as getActivePluginRegistry } from "./runtime-B2ROiq5l.js"; import { l as normalizeSessionEntrySlotKey } from "./store-load-C4uq6Lrq.js"; import { d as updateSessionStore } from "./store-Qsgtu-0y.js"; import { r as resolveAllAgentSessionStoreTargetsSync } from "./targets-BrMDn2yj.js"; import fs from "node:fs"; //#region src/plugins/host-hook-cleanup.ts /** Runs plugin cleanup callbacks and clears host-side plugin session/runtime state. */ function shouldCleanPlugin(pluginId, filterPluginId) { return !filterPluginId || pluginId === filterPluginId; } function collectStoredSessionEntrySlotKeys(entry, pluginId) { const slotKeys = /* @__PURE__ */ new Set(); const storedSlotKeys = entry.pluginExtensionSlotKeys; if (!storedSlotKeys) return slotKeys; const records = pluginId === void 0 ? Object.values(storedSlotKeys) : storedSlotKeys[pluginId] ? [storedSlotKeys[pluginId]] : []; for (const record of records) for (const slotKey of Object.values(record)) { const normalized = normalizeSessionEntrySlotKey(slotKey); if (normalized.ok) slotKeys.add(normalized.key); } return slotKeys; } function collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys) { const slotKeys = collectStoredSessionEntrySlotKeys(entry, pluginId); for (const slotKey of sessionEntrySlotKeys ?? []) slotKeys.add(slotKey); return slotKeys; } function clearPromotedSessionEntrySlots(entry, pluginId, sessionEntrySlotKeys, options = {}) { const slotKeys = options.includeStoredSlotKeys === false && sessionEntrySlotKeys ? new Set(sessionEntrySlotKeys) : collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); const entryRecord = entry; for (const slotKey of slotKeys) delete entryRecord[slotKey]; if (!options.pruneSlotOwnership || !entry.pluginExtensionSlotKeys) return; const pruneRecord = (record) => { for (const [namespace, slotKey] of Object.entries(record)) { const normalized = normalizeSessionEntrySlotKey(slotKey); if (normalized.ok && slotKeys.has(normalized.key)) delete record[namespace]; } }; if (pluginId) { const record = entry.pluginExtensionSlotKeys[pluginId]; if (record) { pruneRecord(record); if (Object.keys(record).length === 0) delete entry.pluginExtensionSlotKeys[pluginId]; } } else { for (const record of Object.values(entry.pluginExtensionSlotKeys)) pruneRecord(record); for (const [ownerPluginId, record] of Object.entries(entry.pluginExtensionSlotKeys)) if (Object.keys(record).length === 0) delete entry.pluginExtensionSlotKeys[ownerPluginId]; } if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) delete entry.pluginExtensionSlotKeys; } /** Clears plugin-owned extension state from one session entry. */ function clearPluginOwnedSessionState(entry, pluginId, sessionEntrySlotKeys) { clearPromotedSessionEntrySlots(entry, pluginId, sessionEntrySlotKeys); if (!pluginId) { delete entry.pluginExtensions; delete entry.pluginExtensionSlotKeys; delete entry.pluginNextTurnInjections; return; } if (entry.pluginExtensions) { delete entry.pluginExtensions[pluginId]; if (Object.keys(entry.pluginExtensions).length === 0) delete entry.pluginExtensions; } if (entry.pluginExtensionSlotKeys) { delete entry.pluginExtensionSlotKeys[pluginId]; if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) delete entry.pluginExtensionSlotKeys; } if (entry.pluginNextTurnInjections) { delete entry.pluginNextTurnInjections[pluginId]; if (Object.keys(entry.pluginNextTurnInjections).length === 0) delete entry.pluginNextTurnInjections; } } function hasPromotedSessionEntrySlot(entry, pluginId, sessionEntrySlotKeys) { const slotKeys = collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); if (slotKeys.size === 0) return false; const entryRecord = entry; for (const slotKey of slotKeys) if (Object.hasOwn(entryRecord, slotKey)) return true; return false; } function hasPluginOwnedSessionState(entry, pluginId, sessionEntrySlotKeys) { if (hasPromotedSessionEntrySlot(entry, pluginId, sessionEntrySlotKeys)) return true; if (!pluginId) return Boolean(entry.pluginExtensions || entry.pluginExtensionSlotKeys || entry.pluginNextTurnInjections); return Boolean(entry.pluginExtensions?.[pluginId] || entry.pluginExtensionSlotKeys?.[pluginId] || entry.pluginNextTurnInjections?.[pluginId]); } function matchesCleanupSession(entryKey, entry, sessionKey) { const normalizedSessionKey = normalizeLowercaseStringOrEmpty(sessionKey); if (!normalizedSessionKey) return true; return normalizeLowercaseStringOrEmpty(entryKey) === normalizedSessionKey || normalizeLowercaseStringOrEmpty(entry.sessionId) === normalizedSessionKey; } function resolveExistingSessionStorePaths(cfg) { return [...new Set(resolveAllAgentSessionStoreTargetsSync(cfg).map((target) => target.storePath).filter((storePath) => fs.existsSync(storePath)))]; } function createMemoizedCleanupSessionStorePathResolver(cfg) { let paths; return () => { paths ??= resolveExistingSessionStorePaths(cfg); return paths; }; } function resolveCleanupSessionStorePaths(params) { return params.storePaths ?? params.resolveStorePaths?.() ?? resolveExistingSessionStorePaths(params.cfg); } async function clearPluginOwnedSessionStores(params) { if (!params.pluginId && !params.sessionKey) return 0; const storePaths = resolveCleanupSessionStorePaths(params); let cleared = 0; for (const storePath of storePaths) cleared += await updateSessionStore(storePath, (store) => { let clearedInStore = 0; const now = Date.now(); for (const [entryKey, entry] of Object.entries(store)) { if (!matchesCleanupSession(entryKey, entry, params.sessionKey) || !hasPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys)) continue; clearPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); entry.updatedAt = now; clearedInStore += 1; } return clearedInStore; }, { skipSaveWhenResult: (clearedInStore) => clearedInStore === 0, takeCacheOwnership: true }); return cleared; } async function clearPromotedSessionEntrySlotStores(params) { if (!params.pluginId && !params.sessionKey || params.sessionEntrySlotKeys.size === 0) return 0; const storePaths = resolveCleanupSessionStorePaths(params); let cleared = 0; for (const storePath of storePaths) cleared += await updateSessionStore(storePath, (store) => { let clearedInStore = 0; const now = Date.now(); for (const [entryKey, entry] of Object.entries(store)) { if (!matchesCleanupSession(entryKey, entry, params.sessionKey) || !hasPromotedSessionEntrySlot(entry, params.pluginId, params.sessionEntrySlotKeys)) continue; clearPromotedSessionEntrySlots(entry, params.pluginId, params.sessionEntrySlotKeys, { includeStoredSlotKeys: false, pruneSlotOwnership: true }); entry.updatedAt = now; clearedInStore += 1; } return clearedInStore; }, { skipSaveWhenResult: (clearedInStore) => clearedInStore === 0, takeCacheOwnership: true }); return cleared; } function collectSessionEntrySlotKeys(registry, pluginId) { const slotKeys = /* @__PURE__ */ new Set(); for (const registration of registry?.sessionExtensions ?? []) { if (!shouldCleanPlugin(registration.pluginId, pluginId)) continue; const slotKey = registration.extension.sessionEntrySlotKey; if (slotKey === void 0) continue; const normalized = normalizeSessionEntrySlotKey(slotKey); if (normalized.ok) slotKeys.add(normalized.key); } return slotKeys; } /** Runs persistent and in-memory cleanup for a plugin, session, or host lifecycle event. */ /** Runs cleanup callbacks for one plugin and returns failures instead of throwing. */ async function runPluginHostCleanup(params) { const failures = []; const shouldCleanup = params.shouldCleanup ?? (() => true); if (!shouldCleanup()) return { cleanupCount: 0, failures }; const registry = params.registry; const sessionEntrySlotKeys = collectSessionEntrySlotKeys(registry ?? getActivePluginRegistry(), params.pluginId); const restartPromotedSessionEntrySlotKeys = params.restartPromotedSessionEntrySlotKeys ?? sessionEntrySlotKeys; let persistentCleanupCount = 0; if (shouldCleanup()) try { persistentCleanupCount = params.reason === "restart" ? await clearPromotedSessionEntrySlotStores({ cfg: params.cfg ?? getRuntimeConfig(), pluginId: params.pluginId, sessionKey: params.sessionKey, sessionEntrySlotKeys: restartPromotedSessionEntrySlotKeys, storePaths: params.sessionStorePaths, resolveStorePaths: params.resolveSessionStorePaths }) : await clearPluginOwnedSessionStores({ cfg: params.cfg ?? getRuntimeConfig(), pluginId: params.pluginId, sessionKey: params.sessionKey, sessionEntrySlotKeys, storePaths: params.sessionStorePaths, resolveStorePaths: params.resolveSessionStorePaths }); } catch (error) { failures.push({ pluginId: params.pluginId ?? "plugin-host", hookId: "session-store", error }); } let cleanupCount = persistentCleanupCount; if (registry) { for (const registration of registry.sessionExtensions ?? []) { if (!shouldCleanup()) return { cleanupCount, failures }; if (!shouldCleanPlugin(registration.pluginId, params.pluginId)) continue; const cleanup = registration.extension.cleanup; if (!cleanup) continue; const hookId = `session:${registration.extension.namespace}`; try { await withPluginHostCleanupTimeout(hookId, () => cleanup({ reason: params.reason, sessionKey: params.sessionKey })); cleanupCount += 1; } catch (error) { failures.push({ pluginId: registration.pluginId, hookId, error }); } } for (const registration of registry.runtimeLifecycles ?? []) { if (!shouldCleanup()) return { cleanupCount, failures }; if (!shouldCleanPlugin(registration.pluginId, params.pluginId)) continue; const cleanup = registration.lifecycle.cleanup; if (!cleanup) continue; const hookId = `runtime:${registration.lifecycle.id}`; try { await withPluginHostCleanupTimeout(hookId, () => cleanup({ reason: params.reason, sessionKey: params.sessionKey, runId: params.runId })); cleanupCount += 1; } catch (error) { failures.push({ pluginId: registration.pluginId, hookId, error }); } } const schedulerFailures = await cleanupPluginSessionSchedulerJobs({ pluginId: params.pluginId, reason: params.reason, sessionKey: params.sessionKey, records: registry.sessionSchedulerJobs, preserveJobIds: params.preserveSchedulerJobIds, preserveOwnerRegistry: params.preserveSchedulerOwnerRegistry, shouldCleanup }); for (const failure of schedulerFailures) failures.push(failure); } if (params.reason !== "restart" && shouldCleanup()) { const registrySchedulerJobKeys = new Set((registry?.sessionSchedulerJobs ?? []).filter((record) => !params.pluginId || record.pluginId === params.pluginId).map((record) => ({ pluginId: record.pluginId, jobId: typeof record.job.id === "string" ? record.job.id.trim() : "" })).filter(({ jobId }) => jobId.length > 0).map(({ pluginId, jobId }) => makePluginSessionSchedulerJobKey(pluginId, jobId))); const runtimeSchedulerFailures = await cleanupPluginSessionSchedulerJobs({ pluginId: params.pluginId, reason: params.reason, sessionKey: params.sessionKey, preserveJobIds: params.preserveSchedulerJobIds, excludeJobKeys: registrySchedulerJobKeys, shouldCleanup }); for (const failure of runtimeSchedulerFailures) failures.push(failure); } if (shouldCleanup() && (params.pluginId || params.runId) && (params.reason !== "restart" || params.runId)) clearPluginRunContext({ pluginId: params.pluginId, runId: params.runId }); return { cleanupCount, failures }; } function collectHostHookPluginIds(registry) { const ids = /* @__PURE__ */ new Set(); for (const registration of registry.sessionExtensions ?? []) ids.add(registration.pluginId); for (const registration of registry.runtimeLifecycles ?? []) ids.add(registration.pluginId); for (const registration of registry.agentEventSubscriptions ?? []) ids.add(registration.pluginId); for (const registration of registry.sessionSchedulerJobs ?? []) ids.add(registration.pluginId); return ids; } function collectLoadedPluginIds(registry) { return new Set(registry.plugins.filter((plugin) => plugin.status === "loaded").map((plugin) => plugin.id)); } function collectSchedulerJobIds(registry, pluginId) { return new Set((registry?.sessionSchedulerJobs ?? []).filter((registration) => registration.pluginId === pluginId).map((registration) => typeof registration.job.id === "string" ? registration.job.id.trim() : "").filter(Boolean)); } function collectRestartPromotedSessionEntrySlotKeys(previousRegistry, nextRegistry, pluginId) { const staleSlotKeys = collectSessionEntrySlotKeys(previousRegistry, pluginId); const preservedSlotKeys = collectSessionEntrySlotKeys(nextRegistry, pluginId); for (const slotKey of preservedSlotKeys) staleSlotKeys.delete(slotKey); return staleSlotKeys; } /** Cleans up plugin host state when a registry snapshot is replaced. */ async function cleanupReplacedPluginHostRegistry(params) { const previousRegistry = params.previousRegistry; const shouldCleanup = params.shouldCleanup ?? (() => true); if (!previousRegistry || previousRegistry === params.nextRegistry || !shouldCleanup()) return { cleanupCount: 0, failures: [] }; const nextPluginIds = params.nextRegistry ? collectLoadedPluginIds(params.nextRegistry) : /* @__PURE__ */ new Set(); const previousPluginIds = new Set([...collectLoadedPluginIds(previousRegistry), ...collectHostHookPluginIds(previousRegistry)]); const resolveSessionStorePaths = createMemoizedCleanupSessionStorePathResolver(params.cfg); const failures = []; let cleanupCount = 0; for (const pluginId of previousPluginIds) { if (!shouldCleanup()) break; const restarted = nextPluginIds.has(pluginId); const result = await runPluginHostCleanup({ cfg: params.cfg, registry: previousRegistry, pluginId, reason: restarted ? "restart" : "disable", preserveSchedulerJobIds: restarted ? collectSchedulerJobIds(params.nextRegistry, pluginId) : void 0, shouldCleanup, restartPromotedSessionEntrySlotKeys: restarted ? collectRestartPromotedSessionEntrySlotKeys(previousRegistry, params.nextRegistry, pluginId) : void 0, preserveSchedulerOwnerRegistry: restarted ? params.nextRegistry : void 0, resolveSessionStorePaths }); cleanupCount += result.cleanupCount; failures.push(...result.failures); } return { cleanupCount, failures }; } //#endregion export { clearPluginOwnedSessionState as n, runPluginHostCleanup as r, cleanupReplacedPluginHostRegistry as t };