UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

565 lines (564 loc) 24.9 kB
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-DItp3I4t.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { v as writeTextAtomic } from "./json-files-Bq1lIlQB.js"; import { t as isPluginJsonValue } from "./host-hook-json-BdcyDerH.js"; import { r as isInternalNonDeliveryChannel } from "./message-channel-constants-2zSoJXQC.js"; import { V as migrateLegacySessionCreator } from "./openclaw-agent-db-maintenance-wTIy-jt-.js"; import { a as isCanonicalSessionDeliveryState, c as normalizeDeliveryContext, l as normalizeSessionDeliveryState, o as mergeDeliveryContext, s as normalizeDeliveryChannelRoute, t as deliveryContextFromChannelRoute } from "./delivery-context.shared-CXmRgetN.js"; import { t as normalizeSessionEntrySlotKey } from "./session-entry-slot-keys-DWTKggcI.js"; import { a as hydrateSessionStoreSkillPromptRefs, i as ensureSessionStorePromptBlobsForPersistence, l as stripRuntimeOnlySessionSkillsFields, o as projectSessionStoreForPersistence, s as normalizePersistedSessionEntryShape } from "./store-writer-state-C4OG_EQ4.js"; import { b as runExclusiveSessionStoreWrite } from "./session-lifecycle-admission-CS8v45tk.js"; import { C as collectSessionMaintenancePreserveKeysForStore, b as shouldRunModelRunPrune, c as archiveStaleDashboardEntries, d as getActiveSessionMaintenanceWarning, g as pruneStaleModelRunEntries, h as pruneStaleEntries, l as capEntryCount, m as normalizeResolvedMaintenanceConfigInput, s as resolveMaintenanceConfig, t as enforceSessionDiskBudget, u as countUnarchivedSessionEntries, x as shouldRunSessionEntryMaintenance } from "./disk-budget-BsDSCUGD.js"; import { a as normalizeSessionRuntimeModelFields } from "./types-BHV0IaPK.js"; import { c as isValidAgentHarnessSessionStoreEntry, f as resolveAgentHarnessSessionStoreError, p as resolveAgentHarnessSessionStoreTransitionError } from "./agent-harness-session-key-BOz3yx0-.js"; import { s as normalizeRestartRecoveryEntryFields } from "./restart-recovery-state-DDdTIiD6.js"; import { a as readSessionStoreJson5 } from "./state-migrations.fs-DyoUSxwp.js"; import fs from "node:fs"; import path from "node:path"; //#region src/config/sessions/store-maintenance-operations.ts function resolveMaintenanceForOperation(params) { return params.maintenanceConfig ? { ...normalizeResolvedMaintenanceConfigInput(params.maintenanceConfig), ...params.maintenanceOverride } : { ...resolveMaintenanceConfig(), ...params.maintenanceOverride }; } function collectReferencedSessionIds(store) { return new Set(Object.values(store).map((entry) => entry?.sessionId).filter((id) => Boolean(id))); } function rememberRemovedSessionFile(removedSessionFiles, entry) { if (!removedSessionFiles.has(entry.sessionId)) removedSessionFiles.set(entry.sessionId, void 0); } async function applyWarnOnlyMaintenance(params) { const activeSessionKey = params.operation.activeSessionKey?.trim(); if (activeSessionKey && params.shouldRunEntryMaintenance) { const warning = getActiveSessionMaintenanceWarning({ store: params.operation.store, activeSessionKey, pruneAfterMs: params.maintenance.pruneAfterMs, maxEntries: params.maintenance.maxEntries, preserveKeys: params.preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs }); if (warning) { const outcome = warning.wouldPrune || warning.capOutcome === "remove" ? "remove" : "archive"; params.operation.log.warn(`session maintenance would ${outcome} active session; skipping enforcement`, { activeSessionKey: warning.activeSessionKey, wouldPrune: warning.wouldPrune, wouldCap: warning.wouldCap, capOutcome: warning.capOutcome, pruneAfterMs: warning.pruneAfterMs, maxEntries: warning.maxEntries }); await params.operation.onWarn?.(warning); } } const diskBudget = await enforceSessionDiskBudget({ store: params.operation.store, storePath: params.operation.storePath, activeSessionKey: params.operation.activeSessionKey, maintenance: params.maintenance, warnOnly: true, log: params.operation.log }); await params.operation.onMaintenanceApplied?.({ mode: params.maintenance.mode, beforeCount: params.beforeCount, afterCount: Object.keys(params.operation.store).length, archived: 0, capArchived: 0, modelRunPruned: 0, pruned: 0, capped: 0, diskBudget }); } async function cleanupRemovedSessionArtifacts(params) { const archivedDirs = await params.operation.artifacts.archiveRemovedSessionTranscripts({ removedSessionFiles: params.removedSessionFiles, referencedSessionIds: params.referencedSessionIds, storePath: params.operation.storePath, reason: "deleted", restrictToStoreDir: true }); if (params.removedSessionFiles.size > 0) await params.operation.artifacts.removeRemovedSessionTrajectoryArtifacts({ removedSessionFiles: params.removedSessionFiles, referencedSessionIds: params.referencedSessionIds, storePath: params.operation.storePath, restrictToStoreDir: true }); if (params.maintenance.resetArchiveRetentionMs == null) return; const targetDirs = archivedDirs.size > 0 ? [...archivedDirs] : [path.dirname(path.resolve(params.operation.storePath))]; await params.operation.artifacts.cleanupArchivedSessionTranscripts({ directories: targetDirs, rules: [{ reason: "deleted", olderThanMs: params.maintenance.resetArchiveRetentionMs }, { reason: "reset", olderThanMs: params.maintenance.resetArchiveRetentionMs }] }).catch((error) => { params.operation.log.warn("session transcript archive retention cleanup failed", { error: String(error) }); }); } async function applyEnforcedMaintenance(params) { const removedSessionFiles = /* @__PURE__ */ new Map(); const modelRunPruned = shouldRunModelRunPrune({ maintenance: params.maintenance, entryCount: countUnarchivedSessionEntries(params.operation.store), force: params.forceMaintenance }) ? pruneStaleModelRunEntries(params.operation.store, params.maintenance.modelRunPruneAfterMs, { onPruned: ({ entry }) => { rememberRemovedSessionFile(removedSessionFiles, entry); }, preserveKeys: params.preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs }) : 0; let archived = archiveStaleDashboardEntries(params.operation.store, params.maintenance.archiveDashboardAfterMs, { preserveKeys: params.preserveSessionKeys }); const pruned = pruneStaleEntries(params.operation.store, params.maintenance.pruneAfterMs, { onPruned: ({ entry }) => { rememberRemovedSessionFile(removedSessionFiles, entry); }, preserveKeys: params.preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs }); const countAfterPrune = countUnarchivedSessionEntries(params.operation.store); const shouldRunCapMaintenance = params.forceMaintenance || shouldRunSessionEntryMaintenance({ entryCount: countAfterPrune, maxEntries: params.maintenance.maxEntries }); let capArchived = 0; const capped = shouldRunCapMaintenance ? capEntryCount(params.operation.store, params.maintenance.maxEntries, { onArchived: () => { archived += 1; capArchived += 1; }, onRemoved: ({ entry }) => { rememberRemovedSessionFile(removedSessionFiles, entry); }, preserveKeys: params.preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs }) : 0; const referencedSessionIds = collectReferencedSessionIds(params.operation.store); await cleanupRemovedSessionArtifacts({ operation: params.operation, maintenance: params.maintenance, removedSessionFiles, referencedSessionIds }); const diskBudget = await enforceSessionDiskBudget({ store: params.operation.store, storePath: params.operation.storePath, activeSessionKey: params.operation.activeSessionKey, preserveKeys: params.preserveSessionKeys, maintenance: params.maintenance, warnOnly: false, log: params.operation.log, commitEvictedIndex: params.operation.commitReducedStore }); await params.operation.onMaintenanceApplied?.({ mode: params.maintenance.mode, beforeCount: params.beforeCount, afterCount: Object.keys(params.operation.store).length, archived, capArchived, modelRunPruned, pruned, capped, diskBudget }); return { changedStore: archived > 0 || modelRunPruned > 0 || pruned > 0 || capped > 0 || (diskBudget?.removedEntries ?? 0) > 0 }; } /** * Applies automatic session-store maintenance to the in-memory file-store image. * * Future SQLite adapters should map this into named boundaries: entry retention, * removed-session artifact cleanup, disk-budget eviction, and archive retention cleanup. */ async function applyFileBackedSessionStoreMaintenance(params) { const maintenance = resolveMaintenanceForOperation(params); const beforeCount = Object.keys(params.store).length; const beforeUnarchivedCount = countUnarchivedSessionEntries(params.store); const forceMaintenance = params.maintenanceOverride !== void 0; const preserveSessionKeys = collectSessionMaintenancePreserveKeysForStore({ storePath: params.storePath, store: params.store, baseKeys: [params.activeSessionKey] }); const shouldRunEntryMaintenance = shouldRunSessionEntryMaintenance({ entryCount: beforeUnarchivedCount, maxEntries: maintenance.maxEntries, force: forceMaintenance }); if (maintenance.mode === "warn") { await applyWarnOnlyMaintenance({ operation: params, maintenance, beforeCount, shouldRunEntryMaintenance, preserveSessionKeys }); return { changedStore: false }; } return await applyEnforcedMaintenance({ operation: params, maintenance, beforeCount, forceMaintenance, preserveSessionKeys }); } //#endregion //#region src/config/sessions/store-migrations.ts /** Applies best-effort in-place migrations for legacy session store entry fields. */ function applySessionStoreMigrations(store) { let changed = false; for (const entry of Object.values(store)) { if (!entry || typeof entry !== "object") continue; const rec = asOptionalRecord(entry); if (!rec) continue; if (typeof rec["channel"] !== "string" && typeof rec["provider"] === "string") { rec["channel"] = rec["provider"]; delete rec["provider"]; changed = true; } if (typeof rec["lastChannel"] !== "string" && typeof rec["lastProvider"] === "string") { rec["lastChannel"] = rec["lastProvider"]; delete rec["lastProvider"]; changed = true; } if (typeof rec.groupChannel !== "string" && typeof rec["room"] === "string") { rec.groupChannel = rec["room"]; delete rec["room"]; changed = true; } else if ("room" in rec) { delete rec["room"]; changed = true; } } return changed; } //#endregion //#region src/infra/state-migrations.legacy-session-store.ts const log = createSubsystemLogger("sessions/legacy-importer"); const loadSessionArchiveRuntime = createLazyRuntimeModule(() => import("./session-archive.runtime.js")); const loadTrajectoryCleanupRuntime = createLazyRuntimeModule(() => import("./cleanup-CksDDRp8.js")); function normalizeRecordKey(value) { const key = value.trim(); return key.length > 0 ? key : void 0; } function normalizeOptionalDeliveryContext(value) { if (!isRecord(value)) return; const normalized = normalizeDeliveryContext({ channel: typeof value.channel === "string" ? value.channel : void 0, to: typeof value.to === "string" ? value.to : void 0, accountId: typeof value.accountId === "string" ? value.accountId : void 0, threadId: typeof value.threadId === "string" || typeof value.threadId === "number" ? value.threadId : void 0 }); return normalized?.channel && normalized.to ? normalized : void 0; } function sameDeliveryContext(left, right) { return (left?.channel ?? void 0) === (right?.channel ?? void 0) && (left?.to ?? void 0) === (right?.to ?? void 0) && (left?.accountId ?? void 0) === (right?.accountId ?? void 0) && (left?.threadId ?? void 0) === (right?.threadId ?? void 0); } function normalizeRestartRecoveryFields(entry) { let next = entry; const assign = (key, value) => { if (entry[key] === value) return; if (next === entry) next = { ...entry }; if (value === void 0) delete next[key]; else next[key] = value; }; const restartContext = normalizeOptionalDeliveryContext(entry.restartRecoveryDeliveryContext); if (!sameDeliveryContext(entry.restartRecoveryDeliveryContext, restartContext)) assign("restartRecoveryDeliveryContext", restartContext); normalizeRestartRecoveryEntryFields(entry, assign); return next; } function normalizeLegacyPluginState(entry, key, normalizeValue) { const state = entry[key]; if (state === void 0) return entry; if (!isRecord(state)) { const next = { ...entry }; delete next[key]; return next; } let changed = false; const normalizedState = {}; for (const [rawPluginId, rawPluginState] of Object.entries(state)) { const pluginId = normalizeRecordKey(rawPluginId); if (!pluginId || !isRecord(rawPluginState)) { changed = true; continue; } changed ||= pluginId !== rawPluginId; const normalizedPluginState = {}; for (const [rawNamespace, rawValue] of Object.entries(rawPluginState)) { const namespace = normalizeRecordKey(rawNamespace); const value = normalizeValue(rawValue); if (!namespace || value === void 0) { changed = true; continue; } changed ||= namespace !== rawNamespace || value !== rawValue; normalizedPluginState[namespace] = value; } if (Object.keys(normalizedPluginState).length === 0) { changed = true; continue; } normalizedState[pluginId] = normalizedPluginState; } if (!changed) return entry; const next = { ...entry }; if (Object.keys(normalizedState).length > 0) Object.assign(next, { [key]: normalizedState }); else delete next[key]; return next; } function normalizePluginExtensions(entry) { return normalizeLegacyPluginState(entry, "pluginExtensions", (value) => isPluginJsonValue(value) ? value : void 0); } function normalizePluginExtensionSlotKeys(entry) { return normalizeLegacyPluginState(entry, "pluginExtensionSlotKeys", (value) => { const slotKey = normalizeSessionEntrySlotKey(value); return slotKey.ok ? slotKey.key : void 0; }); } function normalizeLegacySessionStore(store) { applySessionStoreMigrations(store); for (const [key, entry] of Object.entries(store)) { const modelSelectionLocked = isRecord(entry) && entry.modelSelectionLocked === true; const shaped = normalizePersistedSessionEntryShape(entry, { sessionKey: key }); if (!shaped) { if (modelSelectionLocked) throw new Error(`Invalid model-selection-locked session entry: ${key}`); delete store[key]; continue; } const runtimeFields = normalizeSessionRuntimeModelFields(shaped); if (modelSelectionLocked && runtimeFields !== shaped) throw new Error(`Invalid model-selection-locked session entry: ${key}`); store[key] = stripRuntimeOnlySessionSkillsFields(normalizePluginExtensionSlotKeys(normalizePluginExtensions(normalizeRestartRecoveryFields(normalizeLegacySessionEntryDelivery(migrateLegacySessionCreator(modelSelectionLocked ? shaped : runtimeFields)))))); } const harnessError = resolveAgentHarnessSessionStoreError(store); if (harnessError) throw new Error(harnessError); } function loadLegacySessionStore(storePath, options = {}) { const { store } = readSessionStoreJson5(storePath); if (options.hydrateSkillPromptRefs !== false) hydrateSessionStoreSkillPromptRefs({ storePath, store }); const sessionStore = store; normalizeLegacySessionStore(sessionStore); if (options.runMaintenance) { const maintenance = options.maintenanceConfig ?? resolveMaintenanceConfig(); const beforeCount = countUnarchivedSessionEntries(sessionStore); if (maintenance.mode === "enforce") { const preserveSessionKeys = collectSessionMaintenancePreserveKeysForStore({ storePath, store: sessionStore }); archiveStaleDashboardEntries(sessionStore, maintenance.archiveDashboardAfterMs, { log: false, preserveKeys: preserveSessionKeys }); if (shouldRunModelRunPrune({ maintenance, entryCount: beforeCount })) pruneStaleModelRunEntries(sessionStore, maintenance.modelRunPruneAfterMs, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: maintenance.preserveRecentMs }); if (countUnarchivedSessionEntries(sessionStore) > maintenance.maxEntries) { pruneStaleEntries(sessionStore, maintenance.pruneAfterMs, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: maintenance.preserveRecentMs }); if (shouldRunSessionEntryMaintenance({ entryCount: countUnarchivedSessionEntries(sessionStore), maxEntries: maintenance.maxEntries })) capEntryCount(sessionStore, maintenance.maxEntries, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: maintenance.preserveRecentMs }); } } } return sessionStore; } function snapshotLockedEntries(store) { return new Map(Object.entries(store).flatMap(([sessionKey, entry]) => isValidAgentHarnessSessionStoreEntry(sessionKey, entry) ? [[sessionKey, structuredClone(entry)]] : [])); } function assertLegacySessionStoreWriteIsValid(params) { const transitionError = resolveAgentHarnessSessionStoreTransitionError({ before: params.lockedEntriesBefore, store: params.store }); if (transitionError) throw new Error(transitionError); const storeError = resolveAgentHarnessSessionStoreError(params.store); if (storeError) throw new Error(storeError); } async function archiveRemovedSessionTranscripts(params) { const { archiveSessionTranscripts } = await loadSessionArchiveRuntime(); const archivedDirs = /* @__PURE__ */ new Set(); for (const [sessionId, sessionFile] of params.removedSessionFiles) { if (params.referencedSessionIds.has(sessionId)) continue; const archived = archiveSessionTranscripts({ sessionId, storePath: params.storePath, sessionFile, reason: params.reason, restrictToStoreDir: params.restrictToStoreDir }); for (const archivedPath of archived) archivedDirs.add(path.dirname(archivedPath)); } return archivedDirs; } async function persistLegacySessionStore(storePath, store) { const persisted = projectSessionStoreForPersistence({ storePath, store }); await fs.promises.mkdir(path.dirname(storePath), { recursive: true }); await writeTextAtomic(storePath, JSON.stringify(persisted.store, null, 2), { beforeRename: async () => { await ensureSessionStorePromptBlobsForPersistence({ storePath, promptBlobs: persisted.promptBlobs.values() }); }, durable: true, mode: 384, tempPrefix: path.basename(storePath), trailingNewline: true }); } async function writeLegacySessionStoreUnlocked(storePath, store, lockedEntriesBefore, options) { normalizeLegacySessionStore(store); assertLegacySessionStoreWriteIsValid({ lockedEntriesBefore, store }); if (!options.skipMaintenance) await applyFileBackedSessionStoreMaintenance({ storePath, store, activeSessionKey: options.activeSessionKey, onWarn: options.onWarn, onMaintenanceApplied: options.onMaintenanceApplied, maintenanceOverride: options.maintenanceOverride, maintenanceConfig: options.maintenanceConfig, log, commitReducedStore: () => persistLegacySessionStore(storePath, store), artifacts: { archiveRemovedSessionTranscripts, removeRemovedSessionTrajectoryArtifacts: async (params) => { const { removeRemovedSessionTrajectoryArtifacts } = await loadTrajectoryCleanupRuntime(); await removeRemovedSessionTrajectoryArtifacts(params); }, cleanupArchivedSessionTranscripts: async (params) => { const { cleanupArchivedSessionTranscripts } = await loadSessionArchiveRuntime(); await cleanupArchivedSessionTranscripts(params); } } }); assertLegacySessionStoreWriteIsValid({ lockedEntriesBefore, store }); await persistLegacySessionStore(storePath, store); } async function saveLegacySessionStore(storePath, store, options = {}) { await runExclusiveSessionStoreWrite(storePath, async () => { await writeLegacySessionStoreUnlocked(storePath, store, snapshotLockedEntries(loadLegacySessionStore(storePath)), options); }); } async function updateLegacySessionStore(storePath, mutator, options = {}) { return await runExclusiveSessionStoreWrite(storePath, async () => { const store = loadLegacySessionStore(storePath); const lockedEntriesBefore = snapshotLockedEntries(store); const result = await mutator(store); if (!options.skipSaveWhenResult?.(result)) await writeLegacySessionStoreUnlocked(storePath, store, lockedEntriesBefore, options); return result; }, { reentrant: options.reentrant }); } const LEGACY_SESSION_DELIVERY_KEYS = [ "route", "deliveryContext", "origin", "channel", "lastChannel", "lastTo", "lastAccountId", "lastThreadId" ]; function isInternalContext(context) { return Boolean(context?.channel && (context.channel === "webchat" || isInternalNonDeliveryChannel(context.channel))); } function hasExternalTarget(context) { return Boolean(context?.channel && context.channel !== "webchat" && !isInternalNonDeliveryChannel(context.channel) && context.to); } function mergeExternalOverInternal(external, internal) { return normalizeDeliveryContext({ channel: external?.channel, to: external?.to, accountId: external?.accountId ?? internal?.accountId, threadId: external?.threadId ?? internal?.threadId }); } /** Canonicalizes file-era delivery fields before doctor imports a row into SQLite. */ function normalizeLegacySessionEntryDelivery(entry) { const legacy = entry; const hasLegacyFields = LEGACY_SESSION_DELIVERY_KEYS.some((key) => key in legacy); if (isCanonicalSessionDeliveryState(entry.delivery) && !hasLegacyFields) return entry; const route = normalizeDeliveryChannelRoute(legacy.route); const routeContext = deliveryContextFromChannelRoute(route); const explicitContext = normalizeDeliveryContext(legacy.deliveryContext); const lastChannel = normalizeDeliveryContext({ channel: legacy.lastChannel })?.channel; const storedChannel = normalizeDeliveryContext({ channel: legacy.channel })?.channel; const originChannel = normalizeDeliveryContext({ channel: legacy.origin?.provider })?.channel; const normalizedLastFields = normalizeDeliveryContext({ to: legacy.lastTo, accountId: legacy.lastAccountId, threadId: legacy.lastThreadId }); const hasNormalizedLastFields = Boolean(normalizedLastFields?.to || normalizedLastFields?.accountId || normalizedLastFields?.threadId != null); const lastCandidate = normalizeDeliveryContext({ channel: lastChannel ?? (hasNormalizedLastFields ? storedChannel ?? originChannel : void 0), to: normalizedLastFields?.to, accountId: normalizedLastFields?.accountId, threadId: normalizedLastFields?.threadId }); const lastContext = isInternalContext(lastCandidate) || hasExternalTarget(lastCandidate) || lastChannel != null && lastCandidate?.channel != null || lastCandidate?.channel && lastCandidate.channel === explicitContext?.channel ? lastCandidate : void 0; const channelContext = normalizeDeliveryContext({ channel: legacy.channel }); const originContext = normalizeDeliveryContext({ channel: legacy.origin?.provider, to: legacy.origin?.to, accountId: legacy.origin?.accountId, threadId: legacy.origin?.threadId }); const fallbackContext = mergeDeliveryContext(lastContext, mergeDeliveryContext(explicitContext, mergeDeliveryContext(channelContext, originContext))); const internalFallbackContext = isInternalContext(routeContext) ? mergeDeliveryContext(routeContext, lastContext) : isInternalContext(lastContext) ? lastContext : isInternalContext(channelContext) ? mergeDeliveryContext(channelContext, lastContext) : void 0; const hasInternalFallback = internalFallbackContext !== void 0 && hasExternalTarget(explicitContext); const context = hasInternalFallback ? mergeExternalOverInternal(explicitContext, internalFallbackContext) : mergeDeliveryContext(routeContext, fallbackContext); const migratedDelivery = normalizeSessionDeliveryState({ route: hasInternalFallback ? void 0 : route, context, origin: legacy.origin }); const recoverLegacyDelivery = isCanonicalSessionDeliveryState(entry.delivery) && (entry.delivery.kind === "none" && migratedDelivery.kind !== "none" || entry.delivery.kind === "internal" && migratedDelivery.kind === "external"); const delivery = isCanonicalSessionDeliveryState(entry.delivery) && !recoverLegacyDelivery ? entry.delivery : migratedDelivery; const next = { ...entry, delivery }; const legacyChatType = legacy.origin?.chatType; if (next.chatType == null && (legacyChatType === "direct" || legacyChatType === "group" || legacyChatType === "channel")) next.chatType = legacyChatType; for (const key of LEGACY_SESSION_DELIVERY_KEYS) delete next[key]; return next; } //#endregion export { updateLegacySessionStore as i, normalizeLegacySessionEntryDelivery as n, saveLegacySessionStore as r, loadLegacySessionStore as t };