openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
333 lines (332 loc) • 15.6 kB
JavaScript
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 };