openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
468 lines (467 loc) • 22.7 kB
JavaScript
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import { n as disposeNodeSqliteDependents } from "./kysely-sync-cache-state-DbAMR_DD.mjs";
import { d as readSqliteDataVersion, h as runSqliteReadOperationSync } from "./sqlite-schema-facts-BVf0E_0z.mjs";
import { f as readExistingAgentSchemaMeta } from "./sqlite-readonly-location-DEj0l7LR.mjs";
import { i as setSqliteBusyTimeout } from "./sqlite-busy-timeout-D3v1uB5d.mjs";
import { s as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-BYv0nUQr.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { r as hasSqlitePostCommitScope, t as deferSqlitePostCommitPublication } from "./sqlite-post-commit-DENvzR6w.mjs";
import { a as runSqliteImmediateTransactionSync } from "./sqlite-transaction-DBRLvCDM.mjs";
import { a as registerSqliteCacheExitClose, o as runInSqliteMaintenanceContext } from "./sqlite-wal-DfKCrDTQ.mjs";
import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-CmKkMYdo.mjs";
import { m as observeOpenClawDatabaseMaintenanceResource, u as getOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-DwFy2ZTk.mjs";
import { t as openSqliteReadOnlyDatabase } from "./sqlite-snapshot-source-ChTpAJ4b.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import "./session-key-DY3MOX4N.mjs";
import { a as readOpenClawAgentDatabaseIdentity, i as isOpenClawAgentDatabasePathCurrent, r as findOpenClawAgentDatabaseIdentity } from "./openclaw-agent-db-identity-BlmER9-X.mjs";
import { t as SQLITE_IDLE_HANDLE_TTL_MS } from "./sqlite-handle-lifecycle-dWd9h3ii.mjs";
import { i as assertSupportedAgentSchemaVersion } from "./openclaw-agent-db-schema-read-y-s2h2dU.mjs";
import { a as matchesAgentDatabaseClose, g as releaseAgentCreationClaimHandle, l as revokeAgentDatabaseResources, r as drainAgentDatabaseResources, u as withAgentDatabaseCloseFence } from "./openclaw-agent-db-resources-Cm-sH7kg.mjs";
import { c as getOpenClawAgentDatabaseValidation, h as invalidateOpenClawAgentDatabaseValidation, s as clearOpenClawAgentDatabaseValidationCache } from "./openclaw-agent-db-validation-cache-CHrtutWN.mjs";
import { n as clearOpenClawAgentIntegrityVerification } from "./openclaw-quarantine-store-CUJxC7wy.mjs";
import { D as createSqliteTerminalOpenLatch, S as retainOpenClawStateDatabaseForIdle, y as registerOpenClawStateDatabaseLifecycleListener } from "./openclaw-state-db-cache-CmYLFRSt.mjs";
import { w as releaseAgentDeletionDatabaseCleanup } from "./agent-deletion-journal-DkfGlzu8.mjs";
import "./openclaw-state-db-DJZ7EKoI.mjs";
import { u as getGatewayShutdownCleanupSignal } from "./gateway-work-admission-DAV5kkgV.mjs";
import { g as releaseOpenClawAgentDatabaseLease, m as recordOpenClawAgentDatabaseAdmission, p as readOpenClawAgentDatabaseWorkerLeaseReceiptFromClaim } from "./openclaw-agent-db-lease-zP0JO4hP.mjs";
import "./openclaw-agent-db-schema-helpers-CXpPV9Zn.mjs";
import path from "node:path";
import { isMainThread, threadId } from "node:worker_threads";
import { performance } from "node:perf_hooks";
import { addAbortListener } from "node:events";
//#region src/state/openclaw-agent-db-lifecycle.ts
const agentDbLog = createSubsystemLogger("state/agent-db");
const OPENCLAW_AGENT_DB_SLOW_OPEN_MS = 1e3;
const cache = resolveGlobalSingleton(Symbol.for("openclaw.agentDatabaseLifecycle"), () => ({
databases: /* @__PURE__ */ new Map(),
borrowers: /* @__PURE__ */ new WeakMap(),
idleTimers: /* @__PURE__ */ new WeakMap(),
incognito: /* @__PURE__ */ new WeakSet(),
generation: 0,
failures: /* @__PURE__ */ new Map(),
leases: /* @__PURE__ */ new Map(),
terminal: createSqliteTerminalOpenLatch({ closeByPath: (pathname) => closeOpenClawAgentDatabaseByPath(pathname) }),
unregisterExitClose: null,
pending: /* @__PURE__ */ new Map(),
activePending: /* @__PURE__ */ new Set(),
retainedCloses: /* @__PURE__ */ new Set()
}));
function registerAgentDatabaseHandle(database, leaseId, env, deferred) {
const validation = deferred ? getOpenClawAgentDatabaseValidation(database) : void 0;
cache.leases.set(database.path, {
leaseId,
env,
...validation ? { verification: {
dataVersion: readSqliteDataVersion(database.db),
validation
} } : {}
});
cache.databases.set(database.path, database);
}
/** Only the original writer can promote its background scan into durable proof. */
function recordOpenClawAgentDatabaseBackgroundVerification(database, beforePublication) {
const lease = cache.leases.get(database.path);
const witness = lease?.verification;
if (!lease || !witness || cache.databases.get(database.path) !== database) return false;
lease.verification = void 0;
return runSqliteImmediateTransactionSync(database.db, () => {
beforePublication();
if (Atomics.load(new Int32Array(witness.validation.valid), 0) !== 1 || !isOpenClawAgentDatabasePathCurrent(database) || readSqliteDataVersion(database.db) !== witness.dataVersion) return false;
return recordOpenClawAgentDatabaseAdmission(lease.leaseId, {
agentId: database.agentId,
path: database.path,
env: lease.env
}, witness.validation.identity, true);
});
}
/** Queue a non-throwing runtime publication on the outer database commit edge. */
function deferOpenClawAgentPostCommitPublication(database, publish) {
if (!hasSqlitePostCommitScope(database.db)) return false;
const lease = cache.leases.get(database.path);
if (cache.databases.get(database.path) !== database || !lease && !cache.incognito.has(database)) throw new Error("Agent post-commit publication requires its admitted database owner");
const options = {
agentId: database.agentId,
path: database.path,
...lease ? { env: { ...lease.env } } : {}
};
return deferSqlitePostCommitPublication(database.db, () => publish(options));
}
function logResourceCloseFailure(pathname, error) {
agentDbLog.warn("Agent database resource close failed", {
path: pathname,
error
});
}
function unregisterUnusedAgentDatabaseExitClose() {
if (cache.databases.size === 0 && cache.retainedCloses.size === 0) {
cache.unregisterExitClose?.();
cache.unregisterExitClose = null;
}
}
/** Each physical-open generator owns these checkpoints across any integrity await. */
function startAgentDatabaseOpenTiming(agentId, pathname, admissionMode, diagnostics) {
const startedAt = performance.now();
let elapsedMs = 0;
const phaseDurationsMs = {
open: 0,
validation: 0,
configuration: 0,
schema: 0,
registration: 0
};
return (phase) => {
const completedMs = Math.floor(performance.now() - startedAt);
phaseDurationsMs[phase] = completedMs - elapsedMs;
elapsedMs = completedMs;
if (phase === "validation" && diagnostics.integrityGateReason) agentDbLog.info("agent database integrity gate", {
agentId,
path: pathname,
pid: process.pid,
threadId,
isMainThread,
admissionMode,
...diagnostics
});
if (phase === "registration" && elapsedMs >= OPENCLAW_AGENT_DB_SLOW_OPEN_MS) agentDbLog.warn("slow OpenClaw agent database open", {
agentId,
elapsedMs,
path: pathname,
pid: process.pid,
threadId,
isMainThread,
admissionMode,
phaseDurationsMs,
...diagnostics,
thresholdMs: OPENCLAW_AGENT_DB_SLOW_OPEN_MS
});
};
}
function retainFailedAgentDatabaseClose(agentId, pathname, close) {
const retained = {
agentId,
path: pathname,
close: () => {
close();
cache.retainedCloses.delete(retained);
}
};
cache.retainedCloses.add(retained);
getOpenClawDatabaseMaintenanceScope()?.own(retained, "agent-handles", retained.close);
cache.unregisterExitClose ??= registerSqliteCacheExitClose(closeOpenClawAgentDatabases);
}
function revokePendingAgentDatabaseOpen(pathname, expectedAgentId) {
for (const pending of cache.activePending) if (pending.path === pathname && (expectedAgentId === void 0 || pending.agentId === expectedAgentId)) pending.controller.abort(/* @__PURE__ */ new Error(`Agent database open was revoked: ${pathname}`));
}
function retainAgentDatabase(db) {
observeOpenClawDatabaseMaintenanceResource(db);
const borrowers = cache.borrowers.get(db) ?? /* @__PURE__ */ new Set();
const borrower = {};
borrowers.add(borrower);
cache.borrowers.set(db, borrowers);
return () => {
if (borrowers.delete(borrower) && borrowers.size === 0) cache.idleTimers.get(db)?.refresh();
};
}
/** Keep live deletion-fence reads warm without creating shared state or preventing explicit close. */
function retainIncognitoSharedState(env) {
const statePath = path.resolve(resolveOpenClawStateSqlitePath(env));
let releaseIdle;
const unsubscribe = registerOpenClawStateDatabaseLifecycleListener((event) => {
if (event.kind === "opened" && event.database.path === statePath) {
releaseIdle?.();
releaseIdle = retainOpenClawStateDatabaseForIdle(event.database);
}
});
return () => {
unsubscribe();
releaseIdle?.();
releaseIdle = void 0;
};
}
/** Activity and final borrower release use the same idle or post-grace eviction. */
function refreshAgentDatabaseIdleTimer(database) {
if (cache.incognito.has(database)) return;
const existing = cache.idleTimers.get(database.db);
if (existing) {
existing.refresh();
return;
}
const cleanupSignal = getGatewayShutdownCleanupSignal();
const closeIdle = () => {
if (cache.databases.get(database.path) !== database) {
cache.idleTimers.get(database.db)?.[Symbol.dispose]();
cache.idleTimers.delete(database.db);
return;
}
if (database.db.isOpen && cache.borrowers.get(database.db)?.size) return;
if (database.db.isOpen && database.db.isTransaction) {
timer.refresh();
return;
}
try {
closeCachedOpenClawAgentDatabase(database, { eviction: true });
cache.databases.delete(database.path);
cache.failures.delete(database.path);
unregisterUnusedAgentDatabaseExitClose();
} catch (error) {
logResourceCloseFailure(database.path, error);
timer.refresh();
}
};
const refresh = () => {
if (cleanupSignal.aborted) runInSqliteMaintenanceContext(() => queueMicrotask(closeIdle));
else timer.refresh();
};
const timer = runInSqliteMaintenanceContext(() => setTimeout(closeIdle, SQLITE_IDLE_HANDLE_TTL_MS));
const cleanupListener = addAbortListener(cleanupSignal, refresh);
timer.unref();
cache.idleTimers.set(database.db, {
refresh,
[Symbol.dispose]() {
clearTimeout(timer);
cleanupListener[Symbol.dispose]();
}
});
}
/** Dispose only this publication; a later admission at the same path is independent. */
async function closeMaintenanceAgentDatabase(database) {
await database.walMaintenance.stop();
if (cache.databases.get(database.path) !== database) return;
closeCachedOpenClawAgentDatabase(database);
cache.databases.delete(database.path);
cache.failures.delete(database.path);
if (cache.incognito.has(database)) cache.generation += 1;
}
function closeCachedOpenClawAgentDatabase(database, options = {}) {
const lease = cache.leases.get(database.path);
const alreadyClosed = !database.db.isOpen;
const priorCheckpointError = database.walMaintenance.health?.state === "error";
let clean;
let retainRuntimeProof;
try {
disposeNodeSqliteDependents(database.db);
if (database.walMaintenance.close(options.eviction ? { checkpointMode: "PASSIVE" } : void 0) && !cache.failures.has(database.path) && isOpenClawAgentDatabasePathCurrent(database)) {
const { identity } = readOpenClawAgentDatabaseIdentity(database);
if (typeof identity === "string") clean = {
path: database.path,
identity
};
}
retainRuntimeProof = !cache.failures.has(database.path) && (alreadyClosed ? !priorCheckpointError : database.walMaintenance.health?.state === "blocked" && isOpenClawAgentDatabasePathCurrent(database));
if (database.db.isOpen) database.db.close();
} catch (error) {
if (lease) clearOpenClawAgentIntegrityVerification(database.path, lease.env);
throw error;
}
if (lease) {
releaseOpenClawAgentDatabaseLease(lease.leaseId, {
env: lease.env,
initializationAgentPaths: [database.path]
}, clean ?? (retainRuntimeProof ? "uncheckpointed" : void 0));
cache.leases.delete(database.path);
}
releaseAgentDeletionDatabaseCleanup(database);
releaseAgentCreationClaimHandle(database);
cache.idleTimers.get(database.db)?.[Symbol.dispose]();
cache.idleTimers.delete(database.db);
}
/** A lifecycle scope closes its exact connection and retains its borrow until disposal succeeds. */
function createAgentDatabaseScopeOwnedClose(database, owner) {
const release = retainAgentDatabase(database.db);
return async () => {
if (cache.databases.get(database.path) === database) await closeOpenClawAgentDatabaseByPathAsync(database.path, database.agentId);
else if (database.db.isOpen) throw new Error(`${owner} lost its database close owner.`);
release();
};
}
/** Close one cached agent database identified by its exact resolved pathname. */
function closeOpenClawAgentDatabaseByPath(pathname, expectedAgentId) {
const resolvedPath = path.resolve(pathname);
revokeAgentDatabaseResources({
path: resolvedPath,
agentId: expectedAgentId
}, logResourceCloseFailure);
revokePendingAgentDatabaseOpen(resolvedPath, expectedAgentId);
for (const retained of cache.retainedCloses) if (retained.path === resolvedPath && (expectedAgentId === void 0 || retained.agentId === expectedAgentId)) retained.close();
const database = cache.databases.get(resolvedPath);
if (!database || expectedAgentId !== void 0 && database.agentId !== expectedAgentId) return false;
const incognito = cache.incognito.has(database);
closeCachedOpenClawAgentDatabase(database);
cache.databases.delete(resolvedPath);
cache.failures.delete(resolvedPath);
if (incognito) cache.generation += 1;
unregisterUnusedAgentDatabaseExitClose();
return true;
}
/** Capture only the exact claim belonging to this admitted Worker connection. */
function readOpenClawAgentDatabaseWorkerLeaseReceipt(pathname) {
const resolvedPath = path.resolve(pathname);
const database = cache.databases.get(resolvedPath);
const lease = cache.leases.get(resolvedPath);
if (!database?.db.isOpen || !lease || cache.failures.has(resolvedPath)) throw new Error(`Agent database Worker has no admitted lease: ${resolvedPath}`);
return readOpenClawAgentDatabaseWorkerLeaseReceiptFromClaim(lease.leaseId, {
agentId: database.agentId,
path: database.path,
env: lease.env
});
}
/**
* Converge a terminating worker's cached handle and durable lease without
* turning an already committed worker result into an operation failure.
* Callers own a bounded retry policy and must surface an unsettled result.
*/
function settleOpenClawAgentDatabaseWorkerClose(pathname) {
const resolvedPath = path.resolve(pathname);
const errors = [];
const database = cache.databases.get(resolvedPath);
if (database) {
try {
closeCachedOpenClawAgentDatabase(database);
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
if (database.db.isOpen) try {
database.db.close();
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
if (!database.db.isOpen) {
cache.idleTimers.get(database.db)?.[Symbol.dispose]();
cache.idleTimers.delete(database.db);
const incognito = cache.incognito.has(database);
cache.databases.delete(resolvedPath);
cache.failures.delete(resolvedPath);
if (incognito) cache.generation += 1;
unregisterUnusedAgentDatabaseExitClose();
}
}
if (!cache.databases.get(resolvedPath)?.db.isOpen) {
const lease = cache.leases.get(resolvedPath);
if (lease) try {
releaseOpenClawAgentDatabaseLease(lease.leaseId, {
env: lease.env,
initializationAgentPaths: [resolvedPath]
});
cache.leases.delete(resolvedPath);
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
return {
errors,
settled: !cache.databases.get(resolvedPath)?.db.isOpen && !cache.leases.has(resolvedPath)
};
}
/** Commit receipts invalidate every current handle of the captured physical database. */
function invalidateOpenClawAgentWritableProjections(databaseIdentity, invalidate) {
for (const database of cache.databases.values()) if (findOpenClawAgentDatabaseIdentity(database)?.identity === databaseIdentity) invalidate(database.db);
}
function closeOpenClawAgentDatabases(rootPath) {
revokeAgentDatabaseResources({ rootPath }, logResourceCloseFailure);
for (const pathname of cache.pending.keys()) if (rootPath === void 0 || isPathInside(rootPath, pathname)) revokePendingAgentDatabaseOpen(pathname);
for (const retained of cache.retainedCloses) if (rootPath === void 0 || isPathInside(rootPath, retained.path)) retained.close();
for (const pathname of cache.databases.keys()) if (rootPath === void 0 || isPathInside(rootPath, pathname)) closeOpenClawAgentDatabaseByPath(pathname);
}
/** Latch background verification damage so later opens fail without rescanning. */
function recordOpenClawAgentDatabaseOpenFailure(pathname, error, generation) {
const recorded = cache.terminal.record(pathname, error, generation);
if (recorded) invalidateOpenClawAgentDatabaseValidation(pathname);
return recorded;
}
/** Release fixture handles and pathname trust before a test root is recreated. */
function closeOpenClawAgentDatabasesForTest(rootPath) {
closeOpenClawAgentDatabases(rootPath);
clearOpenClawAgentDatabaseValidationCache(rootPath);
cache.terminal.clearAll(rootPath);
}
async function drainPendingAgentDatabaseOpens(selection) {
while (true) {
const pending = [...cache.activePending].filter((owner) => matchesAgentDatabaseClose(selection, owner));
if (pending.length === 0) return;
for (const owner of pending) revokePendingAgentDatabaseOpen(owner.path, selection.agentId);
await Promise.allSettled(pending.map((owner) => owner.promise));
}
}
/** Drain native opens before a lifecycle owner releases shared state or removes its root. */
async function closeOpenClawAgentDatabasesAsync(rootPath) {
const selection = { rootPath };
await withAgentDatabaseCloseFence(selection, async (resourcePaths) => {
const nativePaths = new Set([
...cache.databases.values(),
...cache.activePending,
...cache.retainedCloses
].filter((owner) => matchesAgentDatabaseClose(selection, owner)).map((owner) => owner.path));
const paths = /* @__PURE__ */ new Set([...nativePaths, ...resourcePaths]);
const errors = (await Promise.allSettled([...paths].map((pathname) => nativePaths.has(pathname) ? closeOpenClawAgentDatabaseByPathAsync(pathname) : drainAgentDatabaseResources({
...selection,
path: pathname
}, async () => {})))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
if (errors.length > 0) throw new AggregateError(errors, "Agent database close failed");
});
}
/** Drain the exact retained owner before deletion, quarantine, or file replacement. */
async function closeOpenClawAgentDatabaseByPathAsync(pathname, expectedAgentId) {
const selection = {
path: path.resolve(pathname),
agentId: expectedAgentId
};
revokePendingAgentDatabaseOpen(selection.path, expectedAgentId);
return drainAgentDatabaseResources(selection, async () => {
await drainPendingAgentDatabaseOpens(selection);
const database = cache.databases.get(selection.path);
if (database && (expectedAgentId === void 0 || database.agentId === expectedAgentId)) await database.walMaintenance.stop();
return closeOpenClawAgentDatabaseByPath(selection.path, expectedAgentId);
});
}
/** Read a database's durable role and agent owner without mutating it. */
function inspectOpenClawAgentDatabaseOwner(pathname) {
let db;
try {
const resolvedPath = path.resolve(pathname);
const opened = cache.databases.get(resolvedPath);
if (opened?.db.isOpen && !cache.failures.has(resolvedPath)) {
runSqliteReadOperationSync(opened.db, () => assertSupportedAgentSchemaVersion(opened.db, pathname), "fresh");
refreshAgentDatabaseIdleTimer(opened);
return {
status: "owned",
agentId: opened.agentId
};
}
db = openSqliteReadOnlyDatabase(pathname, { readOnly: true });
setSqliteBusyTimeout(db, OPENCLAW_SQLITE_BUSY_TIMEOUT_MS);
assertSupportedAgentSchemaVersion(db, pathname);
const existing = readExistingAgentSchemaMeta(db);
if (!existing) return { status: "unowned" };
if (existing.role !== "agent" || !existing.agentId) return { status: "unreadable" };
return {
status: "owned",
agentId: normalizeAgentId(existing.agentId)
};
} catch {
return { status: "unreadable" };
} finally {
db?.close();
}
}
/** Lists process-held incognito databases without opening new sentinel handles. */
function listOpenIncognitoAgentDatabases() {
return [...cache.databases.values()].filter((database) => database.db.isOpen && cache.incognito.has(database)).map((database) => ({
agentId: database.agentId,
storePath: database.path
})).toSorted((left, right) => left.agentId.localeCompare(right.agentId) || left.storePath.localeCompare(right.storePath));
}
/** Borrow committed process-held facts without opening or querying a private store. */
function getOpenIncognitoAgentDatabase(agentId, pathname) {
const database = cache.databases.get(path.resolve(pathname));
return database?.db.isOpen && database.agentId === normalizeAgentId(agentId) && cache.incognito.has(database) ? database : void 0;
}
/** Return the generation of process-held incognito database membership. */
function readOpenIncognitoAgentDatabaseGeneration() {
return cache.generation;
}
function isIncognitoOpenClawAgentDatabase(database) {
return cache.incognito.has(database);
}
//#endregion
export { retainFailedAgentDatabaseClose as C, startAgentDatabaseOpenTiming as D, settleOpenClawAgentDatabaseWorkerClose as E, retainAgentDatabase as S, revokePendingAgentDatabaseOpen as T, readOpenIncognitoAgentDatabaseGeneration as _, closeOpenClawAgentDatabaseByPathAsync as a, refreshAgentDatabaseIdleTimer as b, closeOpenClawAgentDatabasesForTest as c, getOpenIncognitoAgentDatabase as d, inspectOpenClawAgentDatabaseOwner as f, readOpenClawAgentDatabaseWorkerLeaseReceipt as g, listOpenIncognitoAgentDatabases as h, closeOpenClawAgentDatabaseByPath as i, createAgentDatabaseScopeOwnedClose as l, isIncognitoOpenClawAgentDatabase as m, closeCachedOpenClawAgentDatabase as n, closeOpenClawAgentDatabases as o, invalidateOpenClawAgentWritableProjections as p, closeMaintenanceAgentDatabase as r, closeOpenClawAgentDatabasesAsync as s, cache as t, deferOpenClawAgentPostCommitPublication as u, recordOpenClawAgentDatabaseBackgroundVerification as v, retainIncognitoSharedState as w, registerAgentDatabaseHandle as x, recordOpenClawAgentDatabaseOpenFailure as y };