openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
336 lines (335 loc) • 15.1 kB
JavaScript
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 };