UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

425 lines (424 loc) 18.8 kB
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js"; import { E as normalizeSessionKeyPreservingOpaquePeerIds, O as parseAgentSessionKey, f as resolveAgentIdFromSessionKey } from "./session-key-BnWWjqNc.js"; import { t as isIncognitoSessionKey } from "./incognito-session-key-BwpD1Lwd.js"; import { l as runSqliteDeferredTransactionSync } from "./node-sqlite-BpQX3W0e.js"; import { i as executeSqliteQueryTakeFirstSync, o as iterateSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { o as resolveSessionStorePathCore } from "./paths-CXdaYWF_.js"; import { a as normalizeOptionalAgentRuntimeId } from "./agent-runtime-id-BpfvnoN1.js"; import { O as resolveIncognitoOpenClawAgentSqlitePath } from "./openclaw-agent-db-lease-Djvd6LWN.js"; import "./openclaw-agent-db-CWtDoRbC.js"; import { t as normalizeSessionEntrySlotKey } from "./session-entry-slot-keys-DWTKggcI.js"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js"; import { A as isInternalSessionEffectsKey, E as resolveSessionStorePathForScope, S as loadExactSessionEntryCandidates, _ as resolveSessionEntry, a as listSessionEntriesReadOnly, d as patchSessionEntryCore, s as listSessionEntryRows, w as loadExactSessionEntryReadOnly, x as loadExactSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js"; import { a as normalizeStoreSessionKey, o as resolveDeliveryProvenCanonicalSessionKey } from "./store-entry-CzRELcpv.js"; import { c as resolveSqliteScope, i as getSessionKysely, m as toDatabaseOptions } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { O as projectSqliteSessionParticipants, S as parseSessionEntryJson, m as canonicalSessionKeyMigrationRequiredError, p as assertCanonicalSqliteSessionKeysCurrent } from "./session-accessor.sqlite-transcript-state-DwF2owZS.js"; import { r as resolveAgentMainSessionKey } from "./main-session-Br0F9dzh.js"; import { r as resolveSessionStoreIdentity } from "./session-store-key-8xEjWSNi.js"; import "./session-accessor.sqlite-canonical-repair-ChKhq4_W.js"; import { o as resolveAllAgentSessionStoreTargetsSync } from "./targets-Cknmo7YZ.js"; //#region src/config/sessions/plugin-host-cleanup.ts 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); for (const slotKey of slotKeys) Reflect.deleteProperty(entry, 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; for (const slotKey of slotKeys) if (Object.hasOwn(entry, 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 matchesPluginHostCleanupSession(entryKey, entry, sessionKey) { const normalizedSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(sessionKey); if (!normalizedSessionKey) return true; return normalizeSessionKeyPreservingOpaquePeerIds(entryKey) === normalizedSessionKey || entry.sessionId.trim().toLowerCase() === sessionKey?.trim().toLowerCase(); } function shouldSkipPluginHostCleanupStore(params) { if (!params.pluginId && !params.sessionKey) return true; return params.mode === "promoted-slots" && (params.sessionEntrySlotKeys?.size ?? 0) === 0; } function hasPluginHostCleanupTarget(entry, params) { if (params.mode === "promoted-slots") return hasPromotedSessionEntrySlot(entry, params.pluginId, params.sessionEntrySlotKeys); return hasPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); } function isLockedHarnessSessionOwnedByPlugin(entry, preserveLockedHarnessIds) { if (entry.modelSelectionLocked !== true || !preserveLockedHarnessIds?.size) return false; const harnessId = normalizeOptionalAgentRuntimeId(entry.agentHarnessId); return harnessId !== void 0 && preserveLockedHarnessIds.has(harnessId); } function clearPluginHostCleanupTarget(entry, params) { if (params.mode === "promoted-slots") { clearPromotedSessionEntrySlots(entry, params.pluginId, params.sessionEntrySlotKeys, { includeStoredSlotKeys: false, pruneSlotOwnership: true }); return; } clearPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); } //#endregion //#region src/config/sessions/session-accessor.sqlite-summary.ts /** Reads counts and bounded recent session payloads without warming the store cache. */ function readSessionStoreSummaryReadOnly(scope, options) { const resolved = resolveSqliteScope({ ...scope, sessionKey: "" }); const summary = { count: 0, recent: [], byAgent: new Map(options.agentIds.map((agentId) => [agentId, { count: 0, recent: [] }])) }; const result = withOpenClawAgentDatabaseReadOnly((database) => runSqliteDeferredTransactionSync(database.db, () => { assertCanonicalSqliteSessionKeysCurrent(database); const db = getSessionKysely(database.db); for (const row of iterateSqliteQuerySync(database.db, db.selectFrom("session_nodes").select(["session_key", "entry_valid"]).orderBy("updated_at", "desc").orderBy("session_key", "asc"))) { const owner = parseAgentSessionKey(row.session_key)?.agentId; if (!owner || isInternalSessionEffectsKey(row.session_key)) continue; const agent = summary.byAgent.get(owner); const needsRecent = summary.recent.length < options.recentLimit || agent !== void 0 && agent.recent.length < options.recentLimit; if (row.entry_valid === 1 && !needsRecent) { summary.count += 1; if (agent) agent.count += 1; continue; } const stored = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_nodes").selectAll().where("session_key", "=", row.session_key)); if (!stored) continue; const { current_session_id: _currentSessionId, ...listRow } = stored; const parsed = parseSessionEntryJson(listRow); if (!parsed) continue; const entry = projectSqliteSessionParticipants(database.db, row.session_key, parsed); const deliveryCanonicalKey = resolveDeliveryProvenCanonicalSessionKey(row.session_key, entry); if (deliveryCanonicalKey !== row.session_key) throw canonicalSessionKeyMigrationRequiredError(`non-canonical persisted row resolves to session key ${deliveryCanonicalKey}`); summary.count += 1; const selected = { sessionKey: row.session_key, entry }; if (summary.recent.length < options.recentLimit) summary.recent.push(selected); if (agent) { agent.count += 1; if (agent.recent.length < options.recentLimit) agent.recent.push(selected); } } return summary; }), toDatabaseOptions(resolved)); return result.found ? result.value : summary; } //#endregion //#region src/config/sessions/session-accessor.entry.ts /** Resolves a session directly through canonical SQLite row and alias ownership. */ function resolveSessionEntrySelection(scope, options = {}) { return resolveSessionEntry(scope, options); } function resolveAccessStorePath(scope) { return resolveSessionStorePathForScope(scope); } function isStorePathTemplate(store) { return typeof store === "string" && store.includes("{agentId}"); } function resolveLogicalSessionStoreCandidates(params) { const storeConfig = params.cfg.session?.store; const defaultTarget = { agentId: params.agentId, storePath: resolveSessionStorePathCore(storeConfig, { agentId: params.agentId, env: params.env }) }; if (!isStorePathTemplate(storeConfig)) return [defaultTarget]; const targets = /* @__PURE__ */ new Map(); targets.set(defaultTarget.storePath, defaultTarget); for (const target of resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env })) if (target.agentId === params.agentId) targets.set(target.storePath, target); return [...targets.values()]; } function buildLogicalSessionEntryCandidateKeys(params) { const targets = /* @__PURE__ */ new Set(); if (params.canonicalKey) targets.add(params.canonicalKey); if (params.requestedKey && params.requestedKey !== params.canonicalKey) targets.add(params.requestedKey); if (params.canonicalKey === "global" || params.canonicalKey === "unknown") return [...targets]; const agentMainKey = resolveAgentMainSessionKey({ cfg: params.cfg, agentId: params.agentId }); if (params.canonicalKey === agentMainKey) targets.add(`agent:${params.agentId}:main`); return [...targets]; } function findCanonicalSessionEntryMatch(scope, canonicalKey, candidateKeys, options = {}) { let selected; for (const match of loadExactSessionEntryCandidates({ ...scope, sessionKeys: candidateKeys, readOnly: options.readOnly !== false })) { if (selected) throw canonicalSessionKeyMigrationRequiredError(`duplicate rows resolve to canonical session key ${canonicalKey}`); if (match.sessionKey !== canonicalKey) throw canonicalSessionKeyMigrationRequiredError(`non-canonical persisted row resolves to session key ${canonicalKey}`); selected = match; } return selected; } /** Resolves one canonical row across the prepared configured and discovered store targets. */ function resolveSessionEntryAccessTarget(scope) { const target = resolveSessionEntryStoreTarget(scope); return { agentId: target.agentId, canonicalKey: target.canonicalKey, entry: target.entry, requestedKey: target.requestedKey, storeKey: target.storeKey }; } /** Resolves ordered candidate keys inside one agent-owned session store. */ function resolveSessionEntryCandidateTarget(scope) { const candidateKeys = uniqueStrings(scope.candidateKeys.map((key) => key.trim())); const incognitoKey = candidateKeys.find(isIncognitoSessionKey); const incognitoAgentId = incognitoKey ? resolveAgentIdFromSessionKey(incognitoKey) : void 0; const storePath = incognitoAgentId ? resolveIncognitoOpenClawAgentSqlitePath({ agentId: incognitoAgentId, env: scope.env }) : resolveSessionStorePathCore(scope.cfg.session?.store, { agentId: scope.agentId, env: scope.env }); const resolvedAgentId = incognitoAgentId ?? scope.agentId; for (const candidateKey of candidateKeys) { if (!candidateKey) continue; const resolved = resolveSessionEntrySelection({ agentId: resolvedAgentId, ...scope.env ? { env: scope.env } : {}, sessionKey: candidateKey, storePath }, { readOnly: !incognitoAgentId }); if (!resolved.existing) continue; return { agentId: resolvedAgentId, candidateKey, entry: resolved.existing, persisted: true, sessionKey: resolved.normalizedKey }; } const fallbackKey = scope.fallback?.sessionKey.trim(); if (!fallbackKey || !scope.fallback) return null; return { agentId: resolvedAgentId, candidateKey: fallbackKey, entry: structuredClone(scope.fallback.entry), persisted: false, sessionKey: fallbackKey }; } function resolveSessionEntryStoreTarget(scope) { const requestedKey = scope.sessionKey.trim(); const { agentId, canonicalKey } = resolveSessionStoreIdentity({ cfg: scope.cfg, sessionKey: requestedKey, agentId: scope.agentId }); const scanTargets = buildLogicalSessionEntryCandidateKeys({ agentId, canonicalKey, cfg: scope.cfg, requestedKey }); if (isIncognitoSessionKey(canonicalKey)) { const incognitoAgentId = resolveAgentIdFromSessionKey(canonicalKey); const storePath = resolveIncognitoOpenClawAgentSqlitePath({ agentId: incognitoAgentId, env: scope.env }); const selectedMatch = findCanonicalSessionEntryMatch({ agentId: incognitoAgentId, ...scope.env ? { env: scope.env } : {}, storePath }, canonicalKey, scanTargets, { readOnly: false }); return { agentId: incognitoAgentId, canonicalKey, entry: selectedMatch?.entry, requestedKey, storeKey: selectedMatch?.sessionKey ?? canonicalKey, storePath }; } const candidates = resolveLogicalSessionStoreCandidates({ agentId, cfg: scope.cfg, env: scope.env }); const fallback = candidates[0] ?? { agentId, storePath: resolveSessionStorePathCore(scope.cfg.session?.store, { agentId, env: scope.env }) }; let selectedStorePath = fallback.storePath; let selectedMatch = findCanonicalSessionEntryMatch({ agentId, ...scope.env ? { env: scope.env } : {}, storePath: fallback.storePath }, canonicalKey, scanTargets); for (let index = 1; index < candidates.length; index += 1) { const candidate = candidates[index]; if (!candidate) continue; const match = findCanonicalSessionEntryMatch({ agentId, ...scope.env ? { env: scope.env } : {}, storePath: candidate.storePath }, canonicalKey, scanTargets); if (match && selectedMatch) throw canonicalSessionKeyMigrationRequiredError(`duplicate rows resolve to canonical session key ${canonicalKey}`); if (match) { selectedStorePath = candidate.storePath; selectedMatch = match; } } return { agentId, canonicalKey, entry: selectedMatch?.entry, requestedKey, storeKey: selectedMatch?.sessionKey ?? canonicalKey, storePath: selectedStorePath }; } /** * Mutates the canonical logical session entry without exposing the * backing store map to callers. */ async function updateResolvedSessionEntry(scope, update) { const target = resolveSessionEntryStoreTarget(scope); if (!target.entry) return { canonicalKey: target.canonicalKey, found: false }; let updateResult; const updated = await patchSessionEntryCore({ agentId: target.agentId, sessionKey: target.storeKey, storePath: target.storePath }, async (entry) => { updateResult = await update(entry, { agentId: target.agentId, canonicalKey: target.canonicalKey, entry, requestedKey: target.requestedKey, storeKey: target.storeKey }); return entry; }, { replaceEntry: true, skipMaintenance: true }); if (!updated) return { canonicalKey: target.canonicalKey, found: false }; return { canonicalKey: target.canonicalKey, entry: structuredClone(updated), found: true, result: updateResult, storeKey: target.storeKey }; } /** Lists entries from the resolved store, preserving the persisted key for each row. */ function listSessionEntriesCore(scope = {}) { if (scope.clone === false) return openSessionEntryReadView(scope).entries(); return listSessionEntryRows(scope); } /** * Synchronous read view: `get` queries one exact persisted key without alias resolution; * `entries` caches listing metadata or loads complete entries. Rows and nested values are * borrowed: callers must not mutate them and must drop the view before any await. */ function openSessionEntryReadView(scope = {}) { return { get: (sessionKey) => (isIncognitoSessionKey(sessionKey) ? loadExactSessionEntry : loadExactSessionEntryReadOnly)({ ...scope, clone: false, sessionKey })?.entry, entries: () => listSessionEntriesReadOnly({ ...scope, clone: false }) }; } /** * Applies an atomic patch and returns the persisted key selected by the backing * store. Use when a caller must keep sidecar state keyed to the final row. */ async function patchSessionEntryWithKey(scope, update, options = {}) { const entry = await patchSessionEntryCore(scope, update, options); return entry ? { sessionKey: normalizeStoreSessionKey(scope.sessionKey), entry } : null; } //#endregion export { resolveSessionEntryAccessTarget as a, updateResolvedSessionEntry as c, clearPluginOwnedSessionState as d, hasPluginHostCleanupTarget as f, shouldSkipPluginHostCleanupStore as h, resolveAccessStorePath as i, readSessionStoreSummaryReadOnly as l, matchesPluginHostCleanupSession as m, openSessionEntryReadView as n, resolveSessionEntryCandidateTarget as o, isLockedHarnessSessionOwnedByPlugin as p, patchSessionEntryWithKey as r, resolveSessionEntrySelection as s, listSessionEntriesCore as t, clearPluginHostCleanupTarget as u };