UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

333 lines (332 loc) 15.6 kB
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { c as isSqliteLockError, m as setSqliteBusyTimeout, t as openNodeSqliteDatabase, u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js"; import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { a as resolveOpenClawStateDirForDatabasePath, s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js"; import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js"; import { D as isIncognitoOpenClawAgentSqlitePath, k as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db-lease-Djvd6LWN.js"; import { g as runOpenClawAgentWriteTransaction, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import { Q as sqliteSessionEntriesEqual, Z as sqliteLifecycleTargetSnapshotsEqual, r as deleteLifecycleTargetRows, u as readLifecycleTargetSnapshot, w as deleteSessionDeliveryArtifacts, z as runSqliteSessionDeletionTransaction } from "./session-accessor.sqlite-entry-store-BxYl0nro.js"; import { i as getSessionKysely, t as cloneSessionEntry } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { C as runSqliteTranscriptArchiveWorkerOperation, i as deletePlannedLifecycleArtifactEntries, r as deleteMaterializedSessionStatePlans, t as assertPlannedLifecycleArtifactEntriesUnchanged } from "./session-accessor.sqlite-lifecycle-state-BTh4yZ7R.js"; //#region src/config/sessions/session-accessor.sqlite-reclamation-commit.ts const COMMIT_DECISION_TIMEOUT_MS = 5e3; const WAITING = 0; const APPROVED = 1; const REJECTED = 2; const COMMITTING = 3; const SETTLED = 4; function rejectCommit(shared) { Atomics.compareExchange(shared, 0, WAITING, REJECTED); Atomics.compareExchange(shared, 0, APPROVED, REJECTED); Atomics.notify(shared, 0); } /** Called by the Worker while its deletion transaction still owns the writer lock. */ function waitForSqliteReclamationCommit(buffer, request) { const shared = new Int32Array(buffer); request(); Atomics.wait(shared, 0, WAITING, COMMIT_DECISION_TIMEOUT_MS); if (Atomics.compareExchange(shared, 0, APPROVED, COMMITTING) !== APPROVED) { rejectCommit(shared); throw new Error("SQLite session reclamation commit was not authorized"); } } /** Publish only after the transaction ended or its connection successfully closed. */ function markSqliteReclamationSettled(buffer) { if (buffer) { const shared = new Int32Array(buffer); Atomics.store(shared, 0, SETTLED); Atomics.notify(shared, 0); } } /** Keep the live parent authority current until the Worker's transaction has settled. */ function authorizeSqliteReclamationCommit(buffer, databasePath, assertCurrent) { const shared = new Int32Array(buffer); let database; const recoveredErrors = []; let settled = false; try { database = openNodeSqliteDatabase(databasePath); setSqliteBusyTimeout(database, COMMIT_DECISION_TIMEOUT_MS); assertCurrent(); if (Atomics.compareExchange(shared, 0, WAITING, APPROVED) !== WAITING) throw new Error("SQLite session reclamation commit checkpoint expired"); Atomics.notify(shared, 0); while (!settled) { if (Atomics.load(shared, 0) === SETTLED) { settled = true; break; } try { runSqliteImmediateTransactionSync(database, () => { settled = true; }); } catch (error) { if (recoveredErrors.length === 0) recoveredErrors.push(error); settled ||= database.isOpen && database.isTransaction; if (settled) break; const decision = Atomics.compareExchange(shared, 0, APPROVED, REJECTED); if (decision === SETTLED) { settled = true; break; } if (decision !== COMMITTING) { Atomics.notify(shared, 0); throw error; } if (!isSqliteLockError(error)) Atomics.wait(shared, 0, COMMITTING, 10); } } } catch (error) { rejectCommit(shared); throw error; } finally { try { if (database?.isOpen) database.close(); } catch (error) { if (settled) recoveredErrors.push(error); } } return recoveredErrors; } //#endregion //#region src/config/sessions/session-accessor.sqlite-reclamation.ts const reclamationLog = createSubsystemLogger("sessions/reclamation"); const reclamationQueue = new KeyedAsyncQueue(); /** Bounds materialized archive bytes through the matching reclamation commit. */ function runExclusiveSqliteSessionReclamation(run) { return reclamationQueue.enqueue("session-reclamation", run); } function toWorkerDatabaseOptions(options) { const sourceEnv = options.env ?? process.env; const sharedStatePath = options.database?.path ?? resolveOpenClawStateSqlitePath(sourceEnv); return { agentId: options.agentId, env: { OPENCLAW_STATE_DIR: resolveOpenClawStateDirForDatabasePath(sharedStatePath) }, path: resolveOpenClawAgentSqlitePath(options) }; } function deleteSessionBoardRows(database, sessionKeys) { const keys = [...new Set(sessionKeys)]; if (keys.length === 0) return; const db = getNodeSqliteKysely(database.db); const tables = new Set(executeSqliteQuerySync(database.db, db.selectFrom("sqlite_schema").select("name").where("type", "=", "table").where("name", "in", ["board_tabs", "board_widgets"])).rows.map((row) => row.name)); if (!tables.has("board_tabs") || !tables.has("board_widgets")) return; executeSqliteQuerySync(database.db, db.deleteFrom("board_widgets").where("session_key", "in", keys)); executeSqliteQuerySync(database.db, db.deleteFrom("board_tabs").where("session_key", "in", keys)); } function shouldDeleteSqliteSessionEntryLifecycle(database, entry, params) { if (!entry || params.expectedEntry && !sqliteSessionEntriesEqual(entry, params.expectedEntry)) return false; if (params.expectedSessionId !== void 0 && (params.expectedSessionId === null ? entry.sessionId !== void 0 : entry.sessionId !== params.expectedSessionId)) return false; if (params.expectedLifecycleRevision !== void 0 && entry.lifecycleRevision !== params.expectedLifecycleRevision || params.expectedUpdatedAt !== void 0 && entry.updatedAt !== params.expectedUpdatedAt) return false; const expectedTranscript = params.expectedTranscript; if (!expectedTranscript) return true; const rows = executeSqliteQuerySync(database.db, getSessionKysely(database.db).selectFrom("transcript_events").select("event_json").where("session_id", "=", expectedTranscript.sessionId).orderBy("seq", "asc")).rows; return entry.sessionId === expectedTranscript.sessionId && rows.length === expectedTranscript.eventJson.length && rows.every((row, index) => row.event_json === expectedTranscript.eventJson[index]); } function expectedEntryMismatchResult() { return { archivedTranscripts: [], deleted: false, expectedEntryMismatch: true }; } function reclaimSqliteSessionInTransaction(plan, callbacks = {}) { if (plan.kind === "entry") { const value = runSqliteSessionDeletionTransaction((transactionDb) => { callbacks.beforeMutation?.(); const snapshot = readLifecycleTargetSnapshot(transactionDb, plan.deleteParams.target); const entry = snapshot[0]?.entry; if (!sqliteLifecycleTargetSnapshotsEqual(plan.preparedTargetSnapshot, snapshot) || !shouldDeleteSqliteSessionEntryLifecycle(transactionDb, entry, plan.deleteParams)) return expectedEntryMismatchResult(); const sessionKeys = [ plan.deleteParams.target.canonicalKey, ...plan.deleteParams.target.storeKeys, ...snapshot.map((row) => row.sessionKey) ]; const archivedTranscripts = deleteMaterializedSessionStatePlans(transactionDb, plan.materializedPlans, void 0, new Set(sessionKeys)); deleteLifecycleTargetRows(transactionDb, plan.deleteParams.target); if (plan.deleteParams.deleteDeliveryArtifacts === true) deleteSessionDeliveryArtifacts(transactionDb, plan.deleteParams.target.canonicalKey, sessionKeys); deleteSessionBoardRows(transactionDb, sessionKeys); callbacks.onCommit?.(transactionDb); if (!entry) throw new Error("SQLite reclamation plan lost its prepared entry"); return { archivedTranscripts, deleted: true, deletedEntry: cloneSessionEntry(entry), ...entry.sessionId ? { deletedSessionId: entry.sessionId } : {} }; }, plan.databaseOptions); return { kind: plan.kind, value }; } if (plan.kind === "lifecycle-artifacts") { const value = runSqliteSessionDeletionTransaction((transactionDb) => { callbacks.beforeMutation?.(); assertPlannedLifecycleArtifactEntriesUnchanged(transactionDb, plan.entries); const archivedTranscripts = deleteMaterializedSessionStatePlans(transactionDb, plan.materializedPlans, void 0, new Set(plan.entries.map((entry) => entry.sessionKey))); const removedEntries = deletePlannedLifecycleArtifactEntries(transactionDb, plan.entries); callbacks.onCommit?.(transactionDb); return { archivedTranscripts, removedEntries }; }, plan.databaseOptions); return { kind: plan.kind, value }; } if (plan.kind === "history-eviction") { const value = runOpenClawAgentWriteTransaction((transactionDb) => { callbacks.beforeMutation?.(); const archivedTranscripts = deleteMaterializedSessionStatePlans(transactionDb, plan.materializedPlans, new Set(plan.protectedSessionIds)); const db = getSessionKysely(transactionDb.db); const deleted = executeSqliteQuerySync(transactionDb.db, db.selectFrom("session_windows").select("session_id").where("session_id", "=", plan.sessionId)).rows.length === 0; if (deleted) callbacks.onCommit?.(transactionDb); return { archivedTranscripts: deleted ? archivedTranscripts : [], deleted }; }, plan.databaseOptions); if (value.deleted) reclaimSqliteFreePagesBestEffort(plan.databaseOptions); return { kind: plan.kind, value }; } const value = runOpenClawAgentWriteTransaction((transactionDb) => { callbacks.beforeMutation?.(); const snapshot = readLifecycleTargetSnapshot(transactionDb, plan.deleteParams.target); if (!sqliteLifecycleTargetSnapshotsEqual(plan.preparedTargetSnapshot, snapshot) || !shouldDeleteSqliteSessionEntryLifecycle(transactionDb, snapshot[0]?.entry, plan.deleteParams)) return { archivedTranscripts: [], deleted: false, expectedEntryMismatch: true }; const archivedTranscripts = deleteMaterializedSessionStatePlans(transactionDb, plan.materializedPlans, new Set(plan.protectedSessionIds)); const db = getSessionKysely(transactionDb.db); const deleted = executeSqliteQuerySync(transactionDb.db, db.selectFrom("session_windows").select("session_id").where("session_id", "=", plan.sessionId)).rows.length === 0; if (deleted) callbacks.onCommit?.(transactionDb); return { archivedTranscripts: deleted ? archivedTranscripts : [], deleted }; }, plan.databaseOptions); return { kind: plan.kind, value }; } function reclaimSqliteFreePagesBestEffort(databaseOptions) { try { const database = openOpenClawAgentDatabase(databaseOptions); database.walMaintenance.checkpoint(); const row = database.db.prepare("PRAGMA freelist_count").get(); const freePages = Number(row.freelist_count); if (Number.isSafeInteger(freePages) && freePages > 0) database.db.exec(`PRAGMA incremental_vacuum(${freePages});`); database.walMaintenance.checkpoint(); } catch {} } function prepareReclamationWorkerTransferList(plan) { const buffers = /* @__PURE__ */ new Set(); for (const materializedPlan of plan.materializedPlans) { const archive = materializedPlan.archive; if (!archive) continue; const bytes = archive.bytes; let owned = bytes; let buffer; if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) buffer = bytes.buffer; else { buffer = new ArrayBuffer(bytes.byteLength); owned = new Uint8Array(buffer); owned.set(bytes); } materializedPlan.archive = { ...archive, bytes: owned }; buffers.add(buffer); } return [...buffers]; } async function runSqliteSessionReclamation(params) { if (params.forceInProcess || isIncognitoOpenClawAgentSqlitePath(params.plan.databaseOptions.path, { agentId: params.plan.databaseOptions.agentId, env: params.plan.databaseOptions.env })) return reclaimSqliteSessionInTransaction(params.plan, { beforeMutation: params.assertCommitAllowed, onCommit: params.onInProcessCommit }); const assertCommitAllowed = params.assertCommitAllowed; const commitGate = assertCommitAllowed ? new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT) : void 0; const recoveredCommitErrors = []; const [workerResult] = await runSqliteTranscriptArchiveWorkerOperation({ expectedMessageType: "reclaimed", onCommitRequest: commitGate && assertCommitAllowed ? () => { recoveredCommitErrors.push(...authorizeSqliteReclamationCommit(commitGate, params.plan.databaseOptions.path, assertCommitAllowed)); } : void 0, transferList: prepareReclamationWorkerTransferList(params.plan), workerData: { commitGate, operation: "reclaim", plan: params.plan, type: "sqlite-transcript-archive-v2" } }); if (!workerResult) throw new Error("SQLite session reclamation Worker returned no result"); if (recoveredCommitErrors.length > 0) reclamationLog.warn("SQLite session reclamation recovered commit settlement errors", { errors: recoveredCommitErrors.map(String), path: params.plan.databaseOptions.path }); if (workerResult.cleanupIncomplete) reclamationLog.error("SQLite session reclamation committed but Worker cleanup is incomplete", { errors: workerResult.cleanupWarnings ?? [], path: params.plan.databaseOptions.path, recovery: "restart OpenClaw before deleting the owning agent" }); else if (workerResult.cleanupWarnings?.length) reclamationLog.warn("SQLite session reclamation Worker recovered cleanup failures", { errors: workerResult.cleanupWarnings, path: params.plan.databaseOptions.path }); return workerResult.result; } function prepareReclamationDeleteParams({ commitGuard: _commitGuard, ...params }) { return params; } function createSessionEntryReclamationPlan(params) { return { databaseOptions: toWorkerDatabaseOptions(params.databaseOptions), deleteParams: prepareReclamationDeleteParams(params.deleteParams), kind: "entry", materializedPlans: params.materializedPlans, preparedTargetSnapshot: params.preparedTargetSnapshot }; } function createLifecycleArtifactReclamationPlan(params) { return { databaseOptions: toWorkerDatabaseOptions(params.databaseOptions), entries: params.entries, kind: "lifecycle-artifacts", materializedPlans: params.materializedPlans }; } function createHistoryEvictionReclamationPlan(params) { return { databaseOptions: toWorkerDatabaseOptions(params.databaseOptions), kind: "history-eviction", materializedPlans: params.materializedPlans, protectedSessionIds: [...params.protectedSessionIds], sessionId: params.sessionId }; } function createHistoricalGenerationReclamationPlan(params) { return { databaseOptions: toWorkerDatabaseOptions(params.databaseOptions), deleteParams: prepareReclamationDeleteParams(params.deleteParams), kind: "historical-generation", materializedPlans: params.materializedPlans, preparedTargetSnapshot: params.preparedTargetSnapshot, protectedSessionIds: [...params.protectedSessionIds], sessionId: params.sessionId }; } //#endregion export { reclaimSqliteSessionInTransaction as a, shouldDeleteSqliteSessionEntryLifecycle as c, createSessionEntryReclamationPlan as i, markSqliteReclamationSettled as l, createHistoryEvictionReclamationPlan as n, runExclusiveSqliteSessionReclamation as o, createLifecycleArtifactReclamationPlan as r, runSqliteSessionReclamation as s, createHistoricalGenerationReclamationPlan as t, waitForSqliteReclamationCommit as u };