UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

552 lines (551 loc) • 22.6 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { r as assertExistingDatabaseIdentity } from "./sqlite-worker-identity-BRNRpRyc.mjs"; import { i as getLogger } from "./logger-BhL9CiBr.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import "./session-key-DY3MOX4N.mjs"; import { h as parseAgentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs"; import { a as resolveSessionArtifactDirectory, c as resolveSessionStorePathCore, g as resolveUnsuffixedSqliteTargetFromSessionStorePath } from "./paths-Bonddxfk.mjs"; import { a as readOpenClawAgentDatabaseIdentity, n as createOpenClawAgentDatabaseClaim } from "./openclaw-agent-db-identity-BlmER9-X.mjs"; import { i as readOpenClawAgentDatabase } from "./openclaw-agent-db-readonly-open-CLVyHzjq.mjs"; import { i as isIncognitoOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs"; import { i as getOpenClawAgentDatabaseIfOpen, t as borrowOpenClawAgentDatabase } from "./openclaw-agent-db-DWI9Bgur.mjs"; import { n as resolveSessionStoreCompatibilityAgentId } from "./legacy.default-agent-owner-CMzbXvh6.mjs"; import { n as captureSessionStoreCandidateIdentities } from "./session-store-read-candidates-CXD-JYGz.mjs"; import { t as captureSessionTranscriptStorageEnvironment } from "./transcript-target-binding-DpcuSIFy.mjs"; import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dd3VSz82.mjs"; import { d as resolveSessionStoreTargets } from "./targets-kx08xomQ.mjs"; import { c as maintenanceLane } from "./session-transcript-worker-resources-cnBmyJ1M.mjs"; import { _ as countUnarchivedSessionEntries } from "./session-accessor.sqlite-entry-worker-publication-BNeFwCVE.mjs"; import { a as planSessionEntryMaintenance } from "./store-maintenance-preserve-snapshot-y0FafUMk.mjs"; import { a as resolveSessionArtifactCanonicalPathsForEntry, i as pruneUnreferencedSessionArtifacts } from "./disk-budget-DMQGiy_N.mjs"; import { t as captureSessionStoreReadCandidates } from "./session-store-target-inventory-C9ERDnSH.mjs"; import { n as enforceSqliteSessionHistoryDiskBudget, r as inspectSqliteSessionHistoryDiskBudget } from "./session-history-eviction-BC2i-jnE.mjs"; import { n as resolveMaintenanceConfig } from "./store-maintenance-runtime-CdL5JZhR.mjs"; import { n as collectSessionMaintenancePreserveKeysForStore } from "./store-maintenance-preserve-DmKlYufH.mjs"; import "./session-accessor-C3ljJ974.mjs"; import { u as withSessionStoreReaderInWorker } from "./session-entry-read-runtime-BdnQjoL6.mjs"; import { f as applySessionEntryLifecycleMutation, p as purgeDeletedAgentSessionEntries } from "./session-accessor.entry-mutation-telsi3Ig.mjs"; import fs from "node:fs"; import { setImmediate } from "node:timers/promises"; //#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-read.ts /** Retain the preview's physical owner through fresh classification and lifecycle settlement. */ function withSessionCleanupStore(target, consume, options = {}) { const env = captureSessionTranscriptStorageEnvironment(options.source?.database.env ?? process.env); const path = resolveUnsuffixedSqliteTargetFromSessionStorePath(target.storePath).path; if (isIncognitoOpenClawAgentSqlitePath(path, { agentId: target.agentId, env })) return withNativeSessionCleanupStore(target, { agentId: target.agentId, path, env }, consume, options); const identities = captureSessionStoreCandidateIdentities(captureSessionStoreReadCandidates(target.storePath)); let identity; return withSessionStoreReaderInWorker({ ...target, env }, async ({ reader, database, logicalAgentId, continuation, assertCurrent: assertReader }) => { const source = { database, identity }; const assertCurrent = () => { assertReader(); options.assertCurrent?.(); if (options.source && (database.path !== options.source.database.path || database.agentId !== options.source.database.agentId)) throw new Error("Session cleanup changed its physical store"); for (const expected of [options.source?.identity, identity]) if (expected) assertExistingDatabaseIdentity(database.path, expected.key, expected.birthtime); }; assertCurrent(); const result = await consume({ source, resolved: { agentId: logicalAgentId, databaseAgentId: database.agentId, path: database.path, env: database.env, sessionKey: "", ownerStorePath: target.storePath }, assertCurrent, read: async (fixMissing = false) => { assertCurrent(); const snapshot = await reader.readCleanup({ env: database.env, expectedIdentity: identity && { ...identity, canonicalPath: database.path }, continuation, fixMissing }); if (!identity && snapshot.source) { identity = { key: `file:${snapshot.source.databaseIdentity}`, birthtime: snapshot.source.databaseBirthtime }; source.identity = identity; } assertCurrent(); return snapshot; } }); assertCurrent(); return result; }, { lane: maintenanceLane, logical: { assertCurrent: options.assertCurrent }, prepareSource: (database, captured) => { const original = identities.get(captured.canonicalPath); if (captured.key.startsWith("file:")) { if (!original || original.key !== captured.key || original.birthtime !== captured.birthtime) throw new Error("Session cleanup changed its physical owner during discovery"); assertExistingDatabaseIdentity(database.path, original.key, original.birthtime); identity = original; } } }); } /** An explicit process-held selector keeps the same kernel on its original native owner. */ async function withNativeSessionCleanupStore(target, database, consume, options) { const source = { database, nativeIncarnation: options.source?.nativeIncarnation }; const native = {}; const assertCurrent = () => { options.assertCurrent?.(); native.claim?.assertCurrent(); const current = getOpenClawAgentDatabaseIfOpen(database); if (source.nativeIncarnation && (!current || readOpenClawAgentDatabaseIdentity(current).incarnation !== source.nativeIncarnation)) throw new Error("Session cleanup changed its process-held owner"); if (current && !native.claim) { const borrowed = borrowOpenClawAgentDatabase(database); native.claim = createOpenClawAgentDatabaseClaim(current, borrowed.release); source.nativeIncarnation = readOpenClawAgentDatabaseIdentity(current).incarnation; } return current; }; try { assertCurrent(); const { readSessionCleanupSnapshotInDatabase } = await import("./cleanup-service-read.kernel-DvZ0EgF1.mjs"); assertCurrent(); const result = await consume({ source, resolved: { ...database, sessionKey: "", ownerStorePath: target.storePath }, assertCurrent, read: async (fixMissing = false) => { const current = assertCurrent(); if (!current) return { kind: "session-cleanup", store: {}, missing: [] }; const read = readOpenClawAgentDatabase(current, (held) => readSessionCleanupSnapshotInDatabase(held, { env: database.env, fixMissing })); assertCurrent(); return read.value; } }); assertCurrent(); return result; } finally { native.claim?.release(); } } //#endregion //#region src/config/sessions/cleanup-service.ts function resolveCleanupSqlitePath(target) { return resolveSqliteTargetFromSessionStorePath(target.storePath, { agentId: target.agentId }).path; } 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 addEntryArtifactPathsToSet(params) { const sessionsDir = resolveSessionArtifactDirectory(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 = params.snapshot.store; 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 ageArchivedKeys = /* @__PURE__ */ new Set(); const dmScopeRetiredKeys = /* @__PURE__ */ new Set(); for (const { sessionKey } of params.snapshot.missing) { missingKeys.add(sessionKey); delete previewStore[sessionKey]; } const missing = missingKeys.size; 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 = await collectSessionMaintenancePreserveKeysForStore({ storePath: params.target.storePath, store: previewStore, baseKeys: [params.activeKey] }); const { modelRunPruned, archived: totalArchived, capArchived, pruned, capped } = planSessionEntryMaintenance({ maintenance: params.maintenance, initialUnarchivedCount: countUnarchivedSessionEntries(previewStore), forceMaintenance: true, readPreserveKeys: () => preserveSessionKeys, log: false, readAgeCandidates: () => previewStore, readCapCandidates: () => ({ store: previewStore, maxEntries: params.maintenance.maxEntries }), onRemoved: ({ key }, reason) => { (reason === "model-run-pruned" ? modelRunPrunedKeys : reason === "pruned" ? staleKeys : cappedKeys).add(key); }, onArchived: ({ key }, phase) => { (phase === "dashboard" ? archivedKeys : phase === "age" ? ageArchivedKeys : capArchivedKeys).add(key); } }); const archived = totalArchived - capArchived; const entryCleanupArtifactPaths = /* @__PURE__ */ new Set(); for (const keys of [ modelRunPrunedKeys, staleKeys, cappedKeys, dmScopeRetiredKeys ]) addEntryArtifactPathsToSet({ paths: entryCleanupArtifactPaths, store: beforeStore, storePath: params.target.storePath, keys }); 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, ageArchivedKeys, staleKeys, cappedKeys, dmScopeRetiredKeys }; } /** Runs session cleanup preview/apply for the selected store targets. */ async function runSessionsCleanup(params) { const { cfg, opts } = params; params.assertCurrent?.(); 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 = []; const previewSources = /* @__PURE__ */ new Map(); for (const target of targets) { const { result, source } = await withSessionCleanupStore(target, async (owner) => { const snapshot = await owner.read(Boolean(opts.fixMissing)); return { result: await previewStoreCleanup({ cfg, target, maintenance, mode, dryRun: Boolean(opts.dryRun), activeKey: opts.activeKey, fixDmScope: Boolean(opts.fixDmScope), snapshot }), source: owner.source }; }, { assertCurrent: params.assertCurrent }); previewSources.set(target, source); previewResults.push(result); } const appliedSummaries = []; let failingTarget; let failingTargetLifecycleCommitted = false; try { if (!opts.dryRun) for (const target of targets) { failingTarget = target; failingTargetLifecycleCommitted = false; await withSessionCleanupStore(target, async (owner) => { let missingRemovals = []; const dmScopeRetiredRemovals = []; if (opts.fixMissing || opts.fixDmScope) { const snapshot = await owner.read(Boolean(opts.fixMissing)); const applyStore = snapshot.store; if (opts.fixMissing) { missingRemovals = snapshot.missing; for (const { sessionKey } of missingRemovals) delete applyStore[sessionKey]; } 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]; await setImmediate(); owner.assertCurrent(); const lifecycleResult = await applySessionEntryLifecycleMutation({ agentId: target.agentId, storePath: target.storePath, removals, commitGuard: owner.assertCurrent, beforeCommitInTransaction: params.beforeCommitInTransaction, activeSessionKey: opts.activeKey, maintenanceOverride: { ...maintenance, mode }, onLifecycleCommitted: () => { failingTargetLifecycleCommitted = true; } }, owner.resolved); await setImmediate(); const postApplyStore = (await owner.read()).store; owner.assertCurrent(); const unreferencedArtifacts = mode === "warn" ? { scannedFiles: 0, removedFiles: 0, freedBytes: 0, olderThanMs: maintenance.pruneAfterMs } : await pruneUnreferencedSessionArtifacts({ store: postApplyStore, storePath: target.storePath, olderThanMs: maintenance.pruneAfterMs, dryRun: false }); owner.assertCurrent(); const removedSessionKeys = new Set(lifecycleResult.removedSessionKeys); const appliedDiskBudget = await enforceSqliteSessionHistoryDiskBudget({ agentId: target.agentId, storePath: target.storePath, mode, maintenance, reclamationMode: params.reclamationMode }); owner.assertCurrent(); const finalStore = (appliedDiskBudget?.removedEntries ?? 0) > 0 ? (await owner.read()).store : 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); }, { source: previewSources.get(target), assertCurrent: params.assertCurrent }); } } 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 };