UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

483 lines (482 loc) 19.3 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js"; import { n as resolveSessionStoreCompatibilityAgentId } from "./legacy.default-agent-owner-BGwEdQRe.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { o as getLogger } from "./logger-DK-iouVT.js"; import { o as resolveSessionStorePathCore } from "./paths-CXdaYWF_.js"; import { Wt as applySessionEntryLifecycleMutation, qt as purgeDeletedAgentSessionEntries } from "./session-accessor-YsytfDtG.js"; import { _ as inspectTranscriptEventsSync } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js"; import { r as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dp-kgpcT.js"; import { n as enforceSqliteSessionHistoryDiskBudget, r as inspectSqliteSessionHistoryDiskBudget } from "./session-history-eviction-C4srftLJ.js"; import { C as collectSessionMaintenancePreserveKeysForStore, a as resolveSessionArtifactCanonicalPathsForEntry, b as shouldRunModelRunPrune, c as archiveStaleDashboardEntries, g as pruneStaleModelRunEntries, h as pruneStaleEntries, i as pruneUnreferencedSessionArtifacts, l as capEntryCount, s as resolveMaintenanceConfig, u as countUnarchivedSessionEntries, y as shouldPreserveMaintenanceEntry } from "./disk-budget-BsDSCUGD.js"; import { t as listSessionEntriesCore } from "./session-accessor.entry-CQ2WPpfn.js"; import { u as resolveSessionStoreTargets } from "./targets-Cknmo7YZ.js"; import fs from "node:fs"; import path from "node:path"; //#region src/config/sessions/cleanup-result.ts function createSessionsCleanupFailure(target, cause, lifecycleCommitted) { return { target, message: `Session cleanup failed for agent '${target.agentId}': ${formatErrorMessage(cause)}`, lifecycleCommitted }; } var SessionsCleanupFailureError = class extends Error { constructor(failure, cause) { super(failure.message, { cause }); this.failure = failure; this.name = "SessionsCleanupFailureError"; } }; function serializeSessionCleanupResult(params) { const summaries = params.summaries.map((summary) => ({ ...summary, storePath: resolveSqliteTargetFromSessionStorePath(summary.storePath, { agentId: summary.agentId }).path })); const [summary] = summaries; if (summary && summaries.length === 1 && !params.failure) return summary; return { allAgents: true, mode: params.mode, dryRun: params.dryRun, stores: summaries, ...params.failure ? { partialError: { failingAgentId: params.failure.target.agentId, failingStorePath: resolveSqliteTargetFromSessionStorePath(params.failure.target.storePath, { agentId: params.failure.target.agentId }).path, message: params.failure.message, lifecycleCommitted: params.failure.lifecycleCommitted } } : {} }; } function isSessionCleanupSummary(value) { if (!isRecord(value) || !isRecord(value.unreferencedArtifacts)) return false; return typeof value.agentId === "string" && typeof value.storePath === "string" && (value.mode === "enforce" || value.mode === "warn") && typeof value.dryRun === "boolean" && typeof value.beforeCount === "number" && typeof value.afterCount === "number" && typeof value.missing === "number" && typeof value.dmScopeRetired === "number" && typeof value.modelRunPruned === "number" && (value.archived === void 0 || typeof value.archived === "number") && (value.capArchived === void 0 || typeof value.capArchived === "number") && typeof value.pruned === "number" && typeof value.capped === "number" && typeof value.unreferencedArtifacts.removedFiles === "number" && (value.diskBudget === null || isRecord(value.diskBudget)) && typeof value.wouldMutate === "boolean" && (value.applied === void 0 || value.applied === true) && (value.appliedCount === void 0 || typeof value.appliedCount === "number"); } function isSessionsCleanupPartialResult(value) { if (!isRecord(value) || !isRecord(value.partialError)) return false; return value.allAgents === true && (value.mode === "enforce" || value.mode === "warn") && typeof value.dryRun === "boolean" && Array.isArray(value.stores) && value.stores.every(isSessionCleanupSummary) && typeof value.partialError.failingAgentId === "string" && typeof value.partialError.failingStorePath === "string" && typeof value.partialError.message === "string" && typeof value.partialError.lifecycleCommitted === "boolean"; } //#endregion //#region src/config/sessions/cleanup-service.ts function resolveCleanupSqlitePath(target) { return resolveSqliteTargetFromSessionStorePath(target.storePath, { agentId: target.agentId }).path; } function loadCleanupSessionStore(target, options = {}) { if (options.createIfMissing !== true && !fs.existsSync(resolveCleanupSqlitePath(target))) return {}; return Object.fromEntries(listSessionEntriesCore({ agentId: target.agentId, storePath: target.storePath }).map(({ sessionKey, entry }) => [sessionKey, entry])); } function isTranscriptMessageRole(role) { return role === "user" || role === "assistant" || role === "tool" || role === "toolResult" || role === "system"; } function isTranscriptMessageRecord(entry) { if (!entry || typeof entry !== "object") return false; const record = entry; if (record.type === "message") return true; if (record.type === void 0 && record.message && typeof record.message === "object" && isTranscriptMessageRole(record.message.role)) return true; return record.type === void 0 && isTranscriptMessageRole(record.role); } function inspectConfirmedMessageFreeTranscript(params) { try { const inspection = inspectTranscriptEventsSync(params); return inspection.events.some(isTranscriptMessageRecord) ? void 0 : inspection; } catch { return; } } function isMainScopeStaleDirectSessionKey(params) { if ((params.cfg.session?.dmScope ?? "main") !== "main") return false; if (params.activeKey && params.key === params.activeKey) return false; const parsed = parseAgentSessionKey(params.key); if (!parsed || normalizeAgentId(parsed.agentId) !== normalizeAgentId(params.targetAgentId)) return false; const parts = parsed.rest.split(":"); if (parts[0] === "agent") return false; return parts.length === 2 && parts[0] === "direct" && Boolean(parts[1]) || parts.length === 3 && Boolean(parts[0]) && parts[1] === "direct" && Boolean(parts[2]) || parts.length === 4 && Boolean(parts[0]) && Boolean(parts[1]) && parts[2] === "direct" && Boolean(parts[3]); } function retireMainScopeDirectSessionEntries(params) { let retired = 0; for (const [key, entry] of Object.entries(params.store)) { if (entry.archivedAt !== void 0) continue; if (isMainScopeStaleDirectSessionKey({ cfg: params.cfg, targetAgentId: params.targetAgentId, key, activeKey: params.activeKey })) { params.onRetired?.(key, entry); delete params.store[key]; retired += 1; } } return retired; } function pruneMissingTranscriptEntries(params) { let removed = 0; for (const [key, entry] of Object.entries(params.store)) { if ((entry?.modelSelectionLocked === true || entry?.archivedAt !== void 0) && shouldPreserveMaintenanceEntry({ key, entry })) continue; const legacySessionFile = entry.sessionFile; if (parseAgentSessionKey(key) && (entry.initializationPending === true || entry.sessionId === key && (typeof legacySessionFile !== "string" || !legacySessionFile.trim()))) continue; if (!entry?.sessionId) { if (parseAgentSessionKey(key)) continue; delete params.store[key]; removed += 1; params.onPruned?.(key, entry); continue; } const inspection = inspectConfirmedMessageFreeTranscript({ ...params.target, sessionId: entry.sessionId, sessionKey: key }); if (inspection) { delete params.store[key]; removed += 1; params.onPruned?.(key, entry, inspection); } } return removed; } function addEntryArtifactPathsToSet(params) { const sessionsDir = path.dirname(params.storePath); for (const key of params.keys) { const entry = params.store[key]; if (!entry) continue; for (const artifactPath of resolveSessionArtifactCanonicalPathsForEntry({ sessionsDir, entry })) params.paths.add(artifactPath); } } async function previewStoreCleanup(params) { const beforeStore = loadCleanupSessionStore(params.target, { createIfMissing: !params.dryRun }); const previewStore = structuredClone(beforeStore); const staleKeys = /* @__PURE__ */ new Set(); const cappedKeys = /* @__PURE__ */ new Set(); const missingKeys = /* @__PURE__ */ new Set(); const modelRunPrunedKeys = /* @__PURE__ */ new Set(); const archivedKeys = /* @__PURE__ */ new Set(); const capArchivedKeys = /* @__PURE__ */ new Set(); const dmScopeRetiredKeys = /* @__PURE__ */ new Set(); const missing = params.fixMissing === true ? pruneMissingTranscriptEntries({ store: previewStore, target: params.target, onPruned: (key) => { missingKeys.add(key); } }) : 0; const dmScopeRetired = params.fixDmScope === true ? retireMainScopeDirectSessionEntries({ cfg: params.cfg, store: previewStore, targetAgentId: params.target.agentId, activeKey: params.activeKey, onRetired: (key) => { dmScopeRetiredKeys.add(key); } }) : 0; const preserveSessionKeys = collectSessionMaintenancePreserveKeysForStore({ storePath: params.target.storePath, store: previewStore, baseKeys: [params.activeKey] }); const modelRunPruned = shouldRunModelRunPrune({ maintenance: params.maintenance, entryCount: countUnarchivedSessionEntries(previewStore), force: true }) ? pruneStaleModelRunEntries(previewStore, params.maintenance.modelRunPruneAfterMs, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs, onPruned: ({ key }) => { modelRunPrunedKeys.add(key); } }) : 0; const archived = archiveStaleDashboardEntries(previewStore, params.maintenance.archiveDashboardAfterMs, { log: false, onArchived: ({ key }) => { archivedKeys.add(key); }, preserveKeys: preserveSessionKeys }); const pruned = pruneStaleEntries(previewStore, params.maintenance.pruneAfterMs, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs, onPruned: ({ key }) => { staleKeys.add(key); } }); let capArchived = 0; const capped = capEntryCount(previewStore, params.maintenance.maxEntries, { log: false, preserveKeys: preserveSessionKeys, preserveRecentMs: params.maintenance.preserveRecentMs, onArchived: ({ key }) => { capArchived += 1; capArchivedKeys.add(key); }, onRemoved: ({ key }) => { cappedKeys.add(key); } }); const entryCleanupArtifactPaths = /* @__PURE__ */ new Set(); addEntryArtifactPathsToSet({ paths: entryCleanupArtifactPaths, store: beforeStore, storePath: params.target.storePath, keys: modelRunPrunedKeys }); addEntryArtifactPathsToSet({ paths: entryCleanupArtifactPaths, store: beforeStore, storePath: params.target.storePath, keys: staleKeys }); addEntryArtifactPathsToSet({ paths: entryCleanupArtifactPaths, store: beforeStore, storePath: params.target.storePath, keys: cappedKeys }); addEntryArtifactPathsToSet({ paths: entryCleanupArtifactPaths, store: beforeStore, storePath: params.target.storePath, keys: dmScopeRetiredKeys }); const diskBudgetPreview = fs.existsSync(resolveCleanupSqlitePath(params.target)) ? await inspectSqliteSessionHistoryDiskBudget({ agentId: params.target.agentId, storePath: params.target.storePath, mode: params.mode, maintenance: params.maintenance }) : { diskBudget: null, wouldMutate: false }; const diskBudget = diskBudgetPreview.diskBudget; const unreferencedArtifacts = await pruneUnreferencedSessionArtifacts({ store: previewStore, storePath: params.target.storePath, olderThanMs: params.maintenance.pruneAfterMs, dryRun: true, excludeCanonicalPaths: entryCleanupArtifactPaths }); const beforeCount = Object.keys(beforeStore).length; const afterPreviewCount = Object.keys(previewStore).length; const wouldMutate = missing > 0 || dmScopeRetired > 0 || modelRunPruned > 0 || archived > 0 || capArchived > 0 || pruned > 0 || capped > 0 || unreferencedArtifacts.removedFiles > 0 || (diskBudget?.removedEntries ?? 0) > 0 || (diskBudget?.removedFiles ?? 0) > 0 || diskBudgetPreview.wouldMutate; return { summary: { agentId: params.target.agentId, storePath: params.target.storePath, mode: params.mode, dryRun: params.dryRun, beforeCount, afterCount: afterPreviewCount, missing, dmScopeRetired, modelRunPruned, archived, capArchived, pruned, capped, unreferencedArtifacts, diskBudget, wouldMutate }, beforeStore, missingKeys, modelRunPrunedKeys, archivedKeys, capArchivedKeys, staleKeys, cappedKeys, dmScopeRetiredKeys }; } /** Runs session cleanup preview/apply for the selected store targets. */ async function runSessionsCleanup(params) { const { cfg, opts } = params; const maintenance = resolveMaintenanceConfig(); const mode = opts.enforce ? "enforce" : maintenance.mode; const targets = params.targets ?? resolveSessionStoreTargets(cfg, { store: opts.store, agent: opts.agent, allAgents: opts.allAgents }); const previewResults = []; for (const target of targets) { const result = await previewStoreCleanup({ cfg, target, maintenance, mode, dryRun: Boolean(opts.dryRun), activeKey: opts.activeKey, fixMissing: Boolean(opts.fixMissing), fixDmScope: Boolean(opts.fixDmScope) }); previewResults.push(result); } const appliedSummaries = []; let failingTarget; let failingTargetLifecycleCommitted = false; try { if (!opts.dryRun) for (const target of targets) { failingTarget = target; failingTargetLifecycleCommitted = false; const applyStore = loadCleanupSessionStore(target, { createIfMissing: true }); const missingRemovals = []; const dmScopeRetiredRemovals = []; if (opts.fixMissing) pruneMissingTranscriptEntries({ store: applyStore, target, onPruned: (sessionKey, entry, inspection) => { missingRemovals.push({ sessionKey, expectedEntry: structuredClone(entry), archiveRemovedTranscript: true, ...inspection ? { expectedTranscriptSnapshot: inspection.snapshot } : {} }); } }); if (opts.fixDmScope) retireMainScopeDirectSessionEntries({ cfg, store: applyStore, targetAgentId: target.agentId, activeKey: opts.activeKey, onRetired: (sessionKey, entry) => { dmScopeRetiredRemovals.push({ sessionKey, expectedEntry: structuredClone(entry), archiveRemovedTranscript: true }); } }); const removals = [...missingRemovals, ...dmScopeRetiredRemovals]; const lifecycleResult = await applySessionEntryLifecycleMutation({ agentId: target.agentId, storePath: target.storePath, removals, activeSessionKey: opts.activeKey, maintenanceOverride: { ...maintenance, mode }, onLifecycleCommitted: () => { failingTargetLifecycleCommitted = true; } }); const postApplyStore = loadCleanupSessionStore(target, { createIfMissing: true }); const appliedUnreferencedArtifacts = mode === "warn" ? null : await pruneUnreferencedSessionArtifacts({ store: postApplyStore, storePath: target.storePath, olderThanMs: maintenance.pruneAfterMs, dryRun: false }); const removedSessionKeys = new Set(lifecycleResult.removedSessionKeys); const unreferencedArtifacts = mode === "warn" ? { scannedFiles: 0, removedFiles: 0, freedBytes: 0, olderThanMs: maintenance.pruneAfterMs } : appliedUnreferencedArtifacts ?? { scannedFiles: 0, removedFiles: 0, freedBytes: 0, olderThanMs: maintenance.pruneAfterMs }; const appliedDiskBudget = await enforceSqliteSessionHistoryDiskBudget({ agentId: target.agentId, storePath: target.storePath, mode, maintenance }); const finalStore = (appliedDiskBudget?.removedEntries ?? 0) > 0 ? loadCleanupSessionStore(target, { createIfMissing: true }) : postApplyStore; const finalCount = Object.keys(finalStore).length; const missing = missingRemovals.filter(({ sessionKey }) => removedSessionKeys.has(sessionKey)).length; const dmScopeRetired = dmScopeRetiredRemovals.filter(({ sessionKey }) => removedSessionKeys.has(sessionKey)).length; const maintenanceRemovedEntries = lifecycleResult.modelRunPruned + lifecycleResult.pruned + lifecycleResult.capped; const summary = { agentId: target.agentId, storePath: target.storePath, mode, dryRun: false, beforeCount: lifecycleResult.beforeCount, afterCount: finalCount, missing, dmScopeRetired, modelRunPruned: lifecycleResult.modelRunPruned, archived: lifecycleResult.archived - (lifecycleResult.capArchived ?? 0), capArchived: lifecycleResult.capArchived ?? 0, pruned: lifecycleResult.pruned, capped: lifecycleResult.capped, unreferencedArtifacts, diskBudget: appliedDiskBudget, wouldMutate: lifecycleResult.removedEntries > 0 || lifecycleResult.archived > 0 || maintenanceRemovedEntries > 0 || unreferencedArtifacts.removedFiles > 0 || (appliedDiskBudget?.removedEntries ?? 0) > 0 || (appliedDiskBudget?.removedFiles ?? 0) > 0 || appliedDiskBudget != null && appliedDiskBudget.totalBytesAfter < appliedDiskBudget.totalBytesBefore, applied: true, appliedCount: finalCount }; appliedSummaries.push(summary); } } catch (cause) { if (!failingTarget) throw cause; const failure = createSessionsCleanupFailure(failingTarget, cause, failingTargetLifecycleCommitted); if (appliedSummaries.length === 0) throw new SessionsCleanupFailureError(failure, cause); return { mode, previewResults, appliedSummaries, failure }; } return { mode, previewResults, appliedSummaries }; } /** Purge session store entries for a deleted agent (#65524). Best-effort. */ async function purgeAgentSessionStoreEntries(cfg, agentId, options = {}) { const normalizedAgentId = normalizeAgentId(agentId); let storePath = typeof cfg.session?.store === "string" ? cfg.session.store : "<default>"; try { storePath = resolveSessionStorePathCore(cfg.session?.store, { agentId: normalizedAgentId, env: options.env }); const sqliteTarget = resolveSqliteTargetFromSessionStorePath(storePath, { agentId: normalizedAgentId, defaultAgentId: resolveSessionStoreCompatibilityAgentId(cfg), env: options.env }); if (!fs.existsSync(sqliteTarget.path)) return false; const storeAgentId = sqliteTarget.agentId ?? normalizedAgentId; const purge = () => purgeDeletedAgentSessionEntries({ cfg, agentId: normalizedAgentId, storeAgentId, storePath, env: options.env }); if (options.runDatabaseCleanup) await options.runDatabaseCleanup({ agentId: storeAgentId, path: sqliteTarget.path }, purge); else await purge(); return false; } catch (error) { getLogger().warn("session store purge failed during agent deletion", { agentId: normalizedAgentId, error, storePath }); return true; } } //#endregion export { isSessionsCleanupPartialResult as a, createSessionsCleanupFailure as i, runSessionsCleanup as n, serializeSessionCleanupResult as o, SessionsCleanupFailureError as r, purgeAgentSessionStoreEntries as t };