openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
810 lines (809 loc) • 40.8 kB
JavaScript
import { u as toStringifiedError } from "./error-coercion-D_-xJ90S.js";
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { O as parseAgentSessionKey } from "./session-key-BnWWjqNc.js";
import { c as sqliteStringSet, i as executeSqliteQueryTakeFirstSync, o as iterateSqliteQuerySync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { t as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-CEMkwAJ_.js";
import { r as resolveRuntimeWorkerUrl } from "./runtime-worker-url-CpdriB1D.js";
import { _t as coerceRequiredSqliteNumber } from "./openclaw-state-db-BRTnL-D8.js";
import { _ as readSessionArchiveContentSync, h as encodeSessionArchiveContent, n as formatSessionArchiveTimestamp, p as SESSION_ARCHIVE_ZSTD_SUFFIX, s as isSessionArchiveArtifactName } from "./artifacts-DMDO9PHy.js";
import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js";
import { u as syncDirectoryBestEffortSync } from "./directory-durability-CINgXRM4.js";
import { k as ensureSessionTranscriptArchiveSchema } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import { g as runOpenClawAgentWriteTransaction, l as isIncognitoOpenClawAgentDatabase, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js";
import { Q as sqliteSessionEntriesEqual, S as collectSessionStateIdsForEntry, c as readExactSessionEntryRow, i as deleteSessionEntryRows, s as readExactSessionEntryJson, y as readSessionEntryStore } from "./session-accessor.sqlite-entry-store-BxYl0nro.js";
import { f as resolveSqliteTranscriptScope, i as getSessionKysely, m as toDatabaseOptions, p as runExclusiveSqliteSessionWrite, t as cloneSessionEntry, u as resolveSqliteTranscriptArchiveDirectory } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { E as sessionEntryMetadataJson, S as parseSessionEntryJson } from "./session-accessor.sqlite-transcript-state-DwF2owZS.js";
import { n as sqliteSessionStateDeleteSnapshotsEqual, t as readSessionStateDeleteSnapshot } from "./session-accessor.sqlite-delete-snapshot-Dc3XCL8u.js";
import { n as emitSessionTranscriptUpdate } from "./transcript-events-wgPr4ILk.js";
import { a as deleteSessionTranscriptIndexInTransaction } from "./session-transcript-index-DmCW_GtW.js";
import fs from "node:fs";
import { toUSVString } from "node:util";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { Worker } from "node:worker_threads";
//#region src/config/sessions/session-accessor.sqlite-archive.ts
const MAX_MATERIALIZED_ARCHIVE_BATCH_BYTES = 268435456;
const MAX_REGISTERED_ARCHIVE_SESSION_ID_BYTES = 96;
function resolveRegisteredArchiveSessionIdComponent(sessionId) {
if (Buffer.byteLength(sessionId, "utf8") <= MAX_REGISTERED_ARCHIVE_SESSION_ID_BYTES) return sessionId;
return `session-${createHash("sha256").update(sessionId).digest("hex")}`;
}
function resolveSqliteTranscriptArchivePath(params) {
const archiveDirectory = path.resolve(params.archiveDirectory);
const generationSuffix = params.generation ? `.${params.generation}` : "";
const sessionIdComponent = params.identityOwner === "registry" ? resolveRegisteredArchiveSessionIdComponent(params.sessionId) : params.sessionId;
const archivePath = path.resolve(archiveDirectory, `${sessionIdComponent}.jsonl.${params.reason}.${formatSessionArchiveTimestamp(params.nowMs)}${generationSuffix}`);
if (path.dirname(archivePath) !== archiveDirectory) throw new Error(`Cannot archive SQLite transcript outside ${archiveDirectory}`);
return archivePath;
}
function resolveRegisteredSqliteTranscriptArchiveName(params) {
return path.basename(`${resolveSqliteTranscriptArchivePath({
archiveDirectory: ".",
generation: params.generation,
identityOwner: "registry",
reason: params.reason,
sessionId: params.sessionId,
nowMs: params.createdAt
})}${params.encoding === "zstd" ? SESSION_ARCHIVE_ZSTD_SUFFIX : ""}`);
}
function findMatchingSqliteTranscriptArchive(params) {
let entries;
try {
entries = fs.readdirSync(params.archiveDirectory);
} catch {
return null;
}
const prefix = `${params.sessionId}.jsonl.${params.reason}.`;
for (const entry of entries) {
if (!entry.startsWith(prefix) || !isSessionArchiveArtifactName(entry)) continue;
const archivePath = path.join(params.archiveDirectory, entry);
const compressed = entry.endsWith(SESSION_ARCHIVE_ZSTD_SUFFIX);
try {
const stat = fs.statSync(archivePath);
if (!stat.isFile()) continue;
if (!compressed && stat.size !== Buffer.byteLength(params.content, "utf8")) continue;
if (readSessionArchiveContentSync(archivePath) === params.content) return archivePath;
} catch {
continue;
}
}
return null;
}
/** Writes or reuses a transcript archive and returns its durable path. */
function writeTranscriptArchive(params) {
fs.mkdirSync(params.archiveDirectory, { recursive: true });
const existing = findMatchingSqliteTranscriptArchive(params);
if (existing) return existing;
const encoded = encodeSessionArchiveContent(params.content);
for (let attempt = 0; attempt < 10; attempt += 1) {
const archivePath = `${resolveSqliteTranscriptArchivePath({
archiveDirectory: params.archiveDirectory,
identityOwner: "filename",
reason: params.reason,
sessionId: params.sessionId,
nowMs: Date.now() + attempt
})}${encoded.suffix}`;
if (fs.existsSync(archivePath)) continue;
const tempPath = `${archivePath}.${randomUUID()}.tmp`;
try {
writeDurableFileExclusive(tempPath, encoded.bytes);
fs.renameSync(tempPath, archivePath);
syncDirectoryBestEffortSync(params.archiveDirectory);
if (readSessionArchiveContentSync(archivePath) !== params.content) {
fs.rmSync(archivePath, { force: true });
throw new Error(`SQLite transcript archive verification failed for ${params.sessionId}`);
}
return archivePath;
} catch (error) {
fs.rmSync(tempPath, { force: true });
if (error?.code === "EEXIST") continue;
throw error;
}
}
throw new Error(`Could not create SQLite transcript archive for ${params.sessionId}`);
}
function writeDurableFileExclusive(filePath, content) {
const fd = fs.openSync(filePath, "wx", 384);
try {
fs.writeFileSync(fd, content);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
}
function hashSessionArchiveBytes(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
/** Publishes one exact canonical archive without directory scans or replacement. */
function publishEncodedSessionTranscriptArchive(params) {
const archiveDirectory = path.resolve(params.archiveDirectory);
const archivePath = path.resolve(archiveDirectory, params.archiveName);
if (path.dirname(archivePath) !== archiveDirectory || path.basename(archivePath) !== params.archiveName) throw new Error(`Cannot publish SQLite transcript archive outside ${archiveDirectory}`);
fs.mkdirSync(archiveDirectory, {
recursive: true,
mode: 448
});
if (fs.existsSync(archivePath)) {
if (hashSessionArchiveBytes(fs.readFileSync(archivePath)) !== params.sha256) throw new Error(`SQLite transcript archive collision for ${params.archiveName}`);
return archivePath;
}
const tempPath = `${archivePath}.${randomUUID()}.tmp`;
writeDurableFileExclusive(tempPath, Buffer.from(params.bytes));
try {
fs.linkSync(tempPath, archivePath);
} catch (error) {
if (error.code !== "EEXIST") throw error;
} finally {
fs.rmSync(tempPath, { force: true });
}
syncDirectoryBestEffortSync(archiveDirectory);
if (hashSessionArchiveBytes(fs.readFileSync(archivePath)) !== params.sha256) throw new Error(`SQLite transcript archive verification failed for ${params.archiveName}`);
return archivePath;
}
function resolveSourceWorkerExecArgv() {
const tsxApiUrl = import.meta.resolve("tsx/esm/api");
const registerTsx = `import { register } from ${JSON.stringify(tsxApiUrl)}; register();`;
return ["--import", `data:text/javascript,${encodeURIComponent(registerTsx)}`];
}
function spawnSqliteTranscriptArchiveWorkerOperation(params) {
const workerUrl = resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.sessionTranscriptArchive);
let worker;
try {
const sourceWorkerExecArgv = workerUrl.pathname.endsWith(".ts") ? resolveSourceWorkerExecArgv() : void 0;
worker = new Worker(workerUrl, {
workerData: params.workerData,
execArgv: sourceWorkerExecArgv,
transferList: params.transferList
});
} catch (error) {
return Promise.reject(toStringifiedError(error));
}
return new Promise((resolve, reject) => {
let results;
let workerError;
worker.on("message", (message) => {
if (message.type === "commit-request") try {
params.onCommitRequest?.();
} catch (error) {
workerError = toStringifiedError(error);
}
else if (message.type === params.expectedMessageType) (results ??= []).push(...message.results);
});
worker.once("error", (error) => {
workerError ??= toStringifiedError(error);
});
worker.once("exit", (code) => {
worker.removeAllListeners();
if (workerError) {
reject(workerError);
return;
}
if (code !== 0) {
reject(/* @__PURE__ */ new Error(`SQLite transcript archive worker exited with code ${code}`));
return;
}
if (!results) {
reject(/* @__PURE__ */ new Error("SQLite transcript archive worker exited without results"));
return;
}
resolve(results);
});
});
}
const sqliteTranscriptArchiveWorkerQueue = new KeyedAsyncQueue();
const SQLITE_TRANSCRIPT_ARCHIVE_WORKER_QUEUE_KEY = "lifecycle-archive";
function runSqliteTranscriptArchiveWorkerOperation(params) {
return sqliteTranscriptArchiveWorkerQueue.enqueue(SQLITE_TRANSCRIPT_ARCHIVE_WORKER_QUEUE_KEY, () => spawnSqliteTranscriptArchiveWorkerOperation(params));
}
function runSqliteTranscriptArchiveWorker(plans) {
return runSqliteTranscriptArchiveWorkerOperation({
expectedMessageType: "done",
workerData: {
operation: "materialize",
type: "sqlite-transcript-archive-v2",
plans
}
});
}
function runSqliteTranscriptArchivePublishWorker(plans) {
return runSqliteTranscriptArchiveWorkerOperation({
expectedMessageType: "published",
workerData: {
operation: "publish",
type: "sqlite-transcript-archive-v2",
plans
}
});
}
function validateEmptyTranscriptArchivePlan(plan) {
const opened = withOpenClawAgentDatabaseReadOnly((database) => readSessionStateDeleteSnapshot(database.db, plan.sessionId), {
agentId: plan.agentId,
path: plan.databasePath
});
if (!opened.found) throw new Error(`Cannot archive SQLite transcript ${plan.sessionId}: ${opened.reason.replaceAll("-", " ")}`);
if (!sqliteSessionStateDeleteSnapshotsEqual(opened.value, plan.snapshot)) throw new Error(`SQLite session state changed before archive materialization for ${plan.sessionId}`);
}
async function materializeSessionStateDeletePlans(plans) {
const deduped = dedupeSqliteSessionStateDeletePlans(plans);
const workerResults = [];
const workerPlans = [];
for (const archivePlan of deduped.filter((plan) => plan.archiveTranscript)) {
if (archivePlan.snapshot.lastSeq === null) {
validateEmptyTranscriptArchivePlan(archivePlan);
workerResults.push({
archive: null,
sessionId: archivePlan.sessionId
});
continue;
}
workerPlans.push(archivePlan);
}
if (workerPlans.length > 0) workerResults.push(...await runSqliteTranscriptArchiveWorker(workerPlans));
const resultBySessionId = new Map(workerResults.map((result) => [result.sessionId, result]));
return deduped.map((plan) => {
if (!plan.archiveTranscript) return Object.assign({}, plan, {
archive: null,
archivedTranscript: null
});
const result = resultBySessionId.get(plan.sessionId);
if (!result) throw new Error(`SQLite transcript archive worker omitted ${plan.sessionId}`);
const generation = plan.snapshot.generation;
if (result.archive && !generation) throw new Error(`Cannot archive SQLite transcript without a generation for ${plan.sessionId}`);
const archivedTranscript = result.archive && generation ? {
generation,
sessionId: plan.sessionId,
archivedPath: path.join(plan.archiveDirectory, result.archive.archiveName),
sourcePath: path.join(plan.archiveDirectory, `${plan.sessionId}.jsonl`)
} : null;
return Object.assign({}, plan, {
archive: result.archive,
archivedTranscript
});
});
}
function dedupeSqliteSessionStateDeletePlans(plans) {
const deduped = /* @__PURE__ */ new Map();
for (const plan of plans) {
const existing = deduped.get(plan.sessionId);
if (!existing) {
deduped.set(plan.sessionId, plan);
continue;
}
if (existing.agentId !== plan.agentId || existing.archiveDirectory !== plan.archiveDirectory || existing.databasePath !== plan.databasePath || existing.reason !== plan.reason || !sqliteSessionStateDeleteSnapshotsEqual(existing.snapshot, plan.snapshot)) throw new Error(`Conflicting SQLite transcript archive plans for ${plan.sessionId}`);
if (!existing.archiveTranscript && plan.archiveTranscript) deduped.set(plan.sessionId, {
...existing,
archiveTranscript: true
});
}
return [...deduped.values()];
}
//#endregion
//#region src/config/sessions/session-accessor.sqlite-events.ts
function emitArchivedTranscriptUpdates(archivedTranscripts) {
for (const archived of archivedTranscripts) emitSessionTranscriptUpdate({ sessionFile: archived.archivedPath });
}
async function publishTranscriptUpdate(scope, update = {}) {
const resolved = resolveSqliteTranscriptScope(scope);
emitSessionTranscriptUpdate({
...update,
agentId: resolved.agentId,
sessionKey: resolved.sessionKey,
sessionId: resolved.sessionId,
target: {
agentId: resolved.agentId,
sessionId: resolved.sessionId,
sessionKey: resolved.sessionKey,
storePath: resolved.path
}
});
}
//#endregion
//#region src/config/sessions/session-accessor.sqlite-archive-store.ts
/** Inserts the canonical archive row inside the lifecycle deletion transaction. */
function persistSessionTranscriptArchive(database, plan) {
const archive = plan.archive;
const generation = plan.snapshot.generation;
const sessionKey = plan.snapshot.sessionKey;
if (!archive || !generation || !sessionKey) throw new Error(`Cannot persist SQLite transcript archive without an owner generation for ${plan.sessionId}`);
ensureSessionTranscriptArchiveSchema(database.db);
const db = getSessionKysely(database.db);
executeSqliteQuerySync(database.db, db.insertInto("session_transcript_archives").values({
archive_blob: archive.bytes,
archive_name: archive.archiveName,
archive_sha256: archive.sha256,
created_at: archive.createdAt,
encoding: archive.encoding,
generation,
last_publish_attempt_at: null,
last_publish_error: null,
published_at: null,
reason: plan.reason,
session_id: plan.sessionId,
session_key: sessionKey
}).onConflict((conflict) => conflict.columns(["session_id", "generation"]).doNothing()));
const persisted = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("session_transcript_archives").select([
"archive_blob",
"archive_name",
"archive_sha256",
"created_at",
"encoding",
"reason",
"session_key"
]).where("session_id", "=", plan.sessionId).where("generation", "=", generation));
if (!persisted || persisted.archive_name !== archive.archiveName || persisted.archive_sha256 !== archive.sha256 || persisted.created_at !== archive.createdAt || persisted.encoding !== archive.encoding || persisted.reason !== plan.reason || persisted.session_key !== sessionKey || !Buffer.from(persisted.archive_blob).equals(Buffer.from(archive.bytes))) throw new Error(`Conflicting SQLite transcript archive for ${plan.sessionId}`);
}
const PENDING_ARCHIVE_PUBLISH_BATCH_SIZE = 4;
function transcriptArchiveIdentityKey(sessionId, generation) {
return `${sessionId}\u0000${generation}`;
}
function uniqueTranscriptArchives(archives) {
return [...new Map(archives.map((archive) => [transcriptArchiveIdentityKey(archive.sessionId, archive.generation), archive])).values()];
}
/** Publishes derived archive files after their canonical rows and deletions commit. */
async function publishSessionStateArchives(scope, requested) {
const requestedArchives = uniqueTranscriptArchives(requested);
const requestedIdentitySet = new Set(requestedArchives.map((archive) => transcriptArchiveIdentityKey(archive.sessionId, archive.generation)));
let includeRequested = true;
while (true) {
const plans = await runExclusiveSqliteSessionWrite(scope, async () => {
const database = openOpenClawAgentDatabase(toDatabaseOptions(scope));
const db = getSessionKysely(database.db);
if (includeRequested && requestedArchives.length > 0) ensureSessionTranscriptArchiveSchema(database.db);
else if (!executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("sqlite_schema").select("name").where("type", "=", "table").where("name", "=", "session_transcript_archives"))) return [];
const pendingArchives = executeSqliteQuerySync(database.db, db.selectFrom("session_transcript_archives").select([
"archive_name",
"created_at",
"encoding",
"generation",
"reason",
"session_id"
]).where("published_at", "is", null).orderBy("created_at", "asc").orderBy("session_id", "asc").orderBy("generation", "asc").limit(PENDING_ARCHIVE_PUBLISH_BATCH_SIZE)).rows;
for (const archive of pendingArchives) {
if (archive.encoding !== "identity" && archive.encoding !== "zstd" || archive.reason !== "deleted" && archive.reason !== "reset") throw new Error(`Invalid pending SQLite transcript archive for ${archive.session_id}`);
const archiveName = resolveRegisteredSqliteTranscriptArchiveName({
createdAt: archive.created_at,
encoding: archive.encoding,
generation: archive.generation,
reason: archive.reason,
sessionId: archive.session_id
});
if (archiveName === archive.archive_name) continue;
executeSqliteQuerySync(database.db, db.updateTable("session_transcript_archives").set({ archive_name: archiveName }).where("session_id", "=", archive.session_id).where("generation", "=", archive.generation).where("published_at", "is", null));
}
const archives = uniqueTranscriptArchives([...includeRequested ? requestedArchives : [], ...pendingArchives.map((archive) => ({
generation: archive.generation,
sessionId: archive.session_id
}))]);
const archiveDirectory = resolveSqliteTranscriptArchiveDirectory(scope);
return archives.map((archive) => ({
agentId: database.agentId,
archiveDirectory,
databasePath: database.path,
generation: archive.generation,
sessionId: archive.sessionId
}));
});
includeRequested = false;
if (plans.length === 0) break;
const results = await runSqliteTranscriptArchivePublishWorker(plans);
await runExclusiveSqliteSessionWrite(scope, async () => {
const now = Date.now();
runOpenClawAgentWriteTransaction((transactionDb) => {
ensureSessionTranscriptArchiveSchema(transactionDb.db);
const db = getSessionKysely(transactionDb.db);
for (const result of results) executeSqliteQuerySync(transactionDb.db, db.updateTable("session_transcript_archives").set((eb) => ({
last_publish_attempt_at: now,
last_publish_error: result.error?.slice(0, 1024) ?? null,
publish_attempts: eb("publish_attempts", "+", 1),
...result.archivedPath ? { published_at: now } : {}
})).where("session_id", "=", result.sessionId).where("generation", "=", result.generation));
}, toDatabaseOptions(scope));
});
const planByIdentity = new Map(plans.map((plan) => [transcriptArchiveIdentityKey(plan.sessionId, plan.generation), plan]));
emitArchivedTranscriptUpdates(results.flatMap((result) => {
const identity = transcriptArchiveIdentityKey(result.sessionId, result.generation);
if (!result.archivedPath || requestedIdentitySet.has(identity)) return [];
const plan = planByIdentity.get(identity);
return plan ? [{
archivedPath: result.archivedPath,
generation: result.generation,
sessionId: result.sessionId,
sourcePath: path.join(plan.archiveDirectory, `${result.sessionId}.jsonl`)
}] : [];
}));
const failedIds = results.flatMap((result) => result.archivedPath ? [] : [result.sessionId]);
if (failedIds.length > 0) throw new Error(`Session deletion committed, but ${failedIds.length} transcript archive file export(s) remain pending in SQLite; retry the operation to publish them.`);
}
return [...requested];
}
const ARCHIVE_RETENTION_BATCH_SIZE = 256;
/** Removes canonical rows only after retention has removed their derived files. */
async function prunePublishedSessionArchivesByRetention(params) {
const rules = new Map(params.rules.filter((rule) => Number.isFinite(rule.olderThanMs) && rule.olderThanMs >= 0).map((rule) => [rule.reason, rule.olderThanMs]));
if (rules.size === 0) return 0;
const candidates = await runExclusiveSqliteSessionWrite(params.scope, async () => {
const database = openOpenClawAgentDatabase(toDatabaseOptions(params.scope));
const db = getSessionKysely(database.db);
if (!executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("sqlite_schema").select("name").where("type", "=", "table").where("name", "=", "session_transcript_archives"))) return [];
return executeSqliteQuerySync(database.db, db.selectFrom("session_transcript_archives").select([
"archive_name",
"created_at",
"generation",
"published_at",
"reason",
"session_id"
]).where("published_at", "is not", null).orderBy("created_at", "asc").orderBy("session_id", "asc").orderBy("generation", "asc").limit(ARCHIVE_RETENTION_BATCH_SIZE)).rows;
});
const now = params.nowMs ?? Date.now();
const archiveDirectory = resolveSqliteTranscriptArchiveDirectory(params.scope);
const removable = candidates.filter((row) => {
const olderThanMs = rules.get(row.reason);
if (olderThanMs === void 0 || now - row.created_at <= olderThanMs) return false;
const archivePath = path.resolve(archiveDirectory, row.archive_name);
return path.dirname(archivePath) === path.resolve(archiveDirectory) && path.basename(archivePath) === row.archive_name && !fs.existsSync(archivePath);
});
if (removable.length === 0) return 0;
return await runExclusiveSqliteSessionWrite(params.scope, async () => {
let removed = 0;
runOpenClawAgentWriteTransaction((transactionDb) => {
const db = getSessionKysely(transactionDb.db);
for (const row of removable) {
const result = executeSqliteQuerySync(transactionDb.db, db.deleteFrom("session_transcript_archives").where("session_id", "=", row.session_id).where("generation", "=", row.generation).where("archive_name", "=", row.archive_name).where("created_at", "=", row.created_at).where("published_at", "=", row.published_at));
removed += Number(result.numAffectedRows ?? 0n);
}
}, toDatabaseOptions(params.scope));
return removed;
});
}
//#endregion
//#region src/config/sessions/session-accessor.sqlite-lifecycle-state.ts
function shouldRemoveSessionEntry(entry, removal) {
if (!entry) return false;
if (removal.expectedEntry !== void 0 && !sqliteSessionEntriesEqual(entry, removal.expectedEntry)) return false;
if (removal.expectedSessionId !== void 0 && (removal.expectedSessionId === null ? entry.sessionId !== void 0 : entry.sessionId !== removal.expectedSessionId)) return false;
if (removal.expectedLifecycleRevision !== void 0 && entry.lifecycleRevision !== removal.expectedLifecycleRevision) return false;
if (removal.expectedUpdatedAt !== void 0 && entry.updatedAt !== removal.expectedUpdatedAt) return false;
return true;
}
function sessionKeySegmentStartsWith(sessionKey, prefix) {
const firstSeparator = sessionKey.indexOf(":");
if (firstSeparator < 0) return sessionKey.startsWith(prefix);
const secondSeparator = sessionKey.indexOf(":", firstSeparator + 1);
return (secondSeparator < 0 ? sessionKey : sessionKey.slice(secondSeparator + 1)).startsWith(prefix);
}
function sessionKeyBelongsToAgent(sessionKey, agentId) {
if (agentId === void 0) return true;
const parsed = parseAgentSessionKey(sessionKey);
return parsed !== null && normalizeAgentId(parsed.agentId) === normalizeAgentId(agentId);
}
function readSessionTranscriptUpdatedAt(database, sessionId) {
const db = getSessionKysely(database.db);
const row = executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("transcript_events").select((eb) => eb.fn.max("created_at").as("updated_at")).where("session_id", "=", sessionId));
if (row?.updated_at === null || row?.updated_at === void 0) return;
return coerceRequiredSqliteNumber(row.updated_at);
}
function sqliteTranscriptStateIsReclaimable(params) {
const transcriptUpdatedAt = readSessionTranscriptUpdatedAt(params.database, params.sessionId);
const updatedAt = params.sessionUpdatedAt === void 0 ? transcriptUpdatedAt : Math.max(params.sessionUpdatedAt, transcriptUpdatedAt ?? params.sessionUpdatedAt);
return updatedAt === void 0 || params.nowMs - updatedAt >= params.orphanTranscriptMinAgeMs;
}
function sqliteTranscriptStateHasMarker(params) {
const db = getSessionKysely(params.database.db);
const rows = iterateSqliteQuerySync(params.database.db, db.selectFrom("transcript_events").select("event_json").where("session_id", "=", params.sessionId).orderBy("seq", "asc"));
let hasMarker = false;
for (const row of rows) hasMarker ||= row.event_json.includes(params.transcriptContentMarker);
return hasMarker;
}
/** Session ids protected by live node state. */
function readReferencedSessionIds(database, excludedSessionKeys = /* @__PURE__ */ new Set()) {
const db = getSessionKysely(database.db);
const excludedKeys = [...excludedSessionKeys].filter((key) => toUSVString(key) === key && !key.includes("\0") && !/[\uFFFE\uFFFF]/u.test(key));
const rows = iterateSqliteQuerySync(database.db, db.selectFrom("session_nodes").select([
sessionEntryMetadataJson,
"current_session_id",
"session_key"
]).$if(excludedKeys.length > 0, (query) => query.where("session_key", "not in", sqliteStringSet(excludedKeys))));
const sessionIds = /* @__PURE__ */ new Set();
for (const row of rows) {
if (excludedSessionKeys.has(row.session_key)) continue;
sessionIds.add(row.current_session_id);
const entry = parseSessionEntryJson(row);
if (!entry) continue;
for (const sessionId of collectSessionStateIdsForEntry(entry)) sessionIds.add(sessionId);
}
return sessionIds;
}
function readReferencedSessionIdsAfterTargetMutation(database, target, nextEntry) {
const sessionIds = readReferencedSessionIds(database, new Set(uniqueStrings([target.canonicalKey, ...target.storeKeys].map((key) => key.trim()))));
if (nextEntry) for (const sessionId of collectSessionStateIdsForEntry(nextEntry)) sessionIds.add(sessionId);
return sessionIds;
}
function planSessionStateDeleteIfUnreferenced(params) {
if (params.referencedSessionIds.has(params.sessionId)) return null;
return {
agentId: params.database.agentId,
archiveDirectory: params.archiveDirectory,
archiveTranscript: params.archiveTranscript !== false && !isIncognitoOpenClawAgentDatabase(params.database),
databasePath: params.database.path,
reason: params.reason ?? "deleted",
sessionId: params.sessionId,
snapshot: readSessionStateDeleteSnapshot(params.database.db, params.sessionId)
};
}
function deleteMaterializedSessionStatePlans(database, plans, protectedSessionIds, excludedSessionKeys, onDeleted) {
if (plans.length === 0) return [];
const archivedTranscripts = [];
const referencedSessionIds = readReferencedSessionIds(database, excludedSessionKeys);
for (const sessionId of protectedSessionIds ?? []) referencedSessionIds.add(sessionId);
for (const plan of plans) {
if (referencedSessionIds.has(plan.sessionId)) continue;
const currentSnapshot = readSessionStateDeleteSnapshot(database.db, plan.sessionId);
if (!sqliteSessionStateDeleteSnapshotsEqual(currentSnapshot, plan.snapshot)) throw new Error(`SQLite session state changed before deletion for ${plan.sessionId}`);
if (plan.archive) persistSessionTranscriptArchive(database, plan);
if (deleteSqliteSessionStateRows(database, plan.sessionId)) onDeleted?.();
if (plan.snapshot.lastSeq !== null && plan.archivedTranscript) archivedTranscripts.push(plan.archivedTranscript);
}
return archivedTranscripts;
}
function planSessionStateAfterEntryRemoval(params) {
const referencedSessionIds = params.referencedSessionIds ?? readReferencedSessionIds(params.database);
const plans = [];
for (const sessionId of collectSessionStateIdsForEntry(params.entry)) {
const plan = planSessionStateDeleteIfUnreferenced({
archiveTranscript: params.archiveTranscript,
archiveDirectory: params.archiveDirectory,
database: params.database,
reason: params.reason,
referencedSessionIds,
sessionId
});
if (plan) plans.push(plan);
}
return plans;
}
/** Ids of every persisted generation owned by the given logical session keys. */
function readSessionGenerationIdsForKeys(database, keys, options = {}) {
const sessionKeys = [...keys].map((key) => options.exactStoredKeys ? key : key.trim());
const db = getSessionKysely(database.db);
return executeSqliteQuerySync(database.db, db.selectFrom("session_windows").select("session_id").where("session_key", "in", sqliteStringSet(sessionKeys))).rows.map((row) => row.session_id);
}
async function projectSessionEntryLifecycleMutation(databaseOptions, params) {
const removalDatabase = openOpenClawAgentDatabase(databaseOptions);
const store = readSessionEntryStore(removalDatabase, {
allowCanonicalRepair: params.allowCanonicalRepair === true,
sessionKeys: [...params.removals.map((removal) => removal.exactStoredKey ? removal.sessionKey : removal.sessionKey.trim()), ...params.upserts.map((upsert) => upsert.sessionKey.trim())]
});
const removedKeysToArchive = /* @__PURE__ */ new Set();
const changedSessionKeys = /* @__PURE__ */ new Set();
const projectedRemovals = [];
for (const removal of params.removals) {
const sessionKey = removal.exactStoredKey ? removal.sessionKey : removal.sessionKey.trim();
let entry = removal.exactStoredKey || sessionKey ? store[sessionKey] : void 0;
if (removal.expectedRawEntryJson !== void 0) {
if (readExactSessionEntryJson(removalDatabase, sessionKey) !== removal.expectedRawEntryJson) throw new Error(`SQLite session entry changed before raw lifecycle removal for ${sessionKey}`);
entry = removal.expectedEntry ? cloneSessionEntry(removal.expectedEntry) : void 0;
}
if (!shouldRemoveSessionEntry(entry, removal)) continue;
if (removal.expectedTranscriptSnapshot) {
const sessionId = entry.sessionId;
if (!sessionId || !sqliteSessionStateDeleteSnapshotsEqual(readSessionStateDeleteSnapshot(removalDatabase.db, sessionId), removal.expectedTranscriptSnapshot)) continue;
}
projectedRemovals.push({
archiveTranscript: removal.archiveRemovedTranscript === true,
expectedEntry: cloneSessionEntry(entry),
removal,
sessionKey
});
if (removal.archiveRemovedTranscript === true) removedKeysToArchive.add(sessionKey);
changedSessionKeys.add(sessionKey);
delete store[sessionKey];
}
const upsertedEntries = [];
for (const upsert of params.upserts) {
const sessionKey = upsert.sessionKey.trim();
if (!sessionKey) continue;
if (upsert.requiresRemovalSessionKey && !projectedRemovals.some((removal) => removal.sessionKey === upsert.requiresRemovalSessionKey?.trim())) continue;
const expectedEntry = store[sessionKey] ? cloneSessionEntry(store[sessionKey]) : void 0;
if (upsert.resetBoundary && !expectedEntry) throw new Error(`Cannot append reset boundary without an existing session row: ${sessionKey}`);
const entry = upsert.buildEntry === void 0 ? upsert.entry : await upsert.buildEntry({
currentEntry: expectedEntry ? cloneSessionEntry(expectedEntry) : void 0,
sessionKey
});
if (!entry) continue;
const cloned = cloneSessionEntry(entry);
store[sessionKey] = cloned;
changedSessionKeys.add(sessionKey);
upsertedEntries.push({
expectedEntry,
sessionKey,
entry: cloned,
...upsert.routeContext !== void 0 ? { routeContext: upsert.routeContext } : {},
...upsert.resetBoundary ? { resetBoundary: upsert.resetBoundary } : {}
});
}
if (projectedRemovals.length === 0) return {
deletePlans: [],
removals: projectedRemovals,
upsertedEntries
};
const database = openOpenClawAgentDatabase(databaseOptions);
const referencedSessionIds = collectProjectedReferencedSessionIds({
database,
excludedSessionKeys: changedSessionKeys,
projectedStore: store
});
const deletePlans = projectedRemovals.flatMap(({ archiveTranscript, expectedEntry: entry }) => planSessionStateAfterEntryRemoval({
archiveDirectory: params.archiveDirectory,
archiveTranscript,
database,
entry,
reason: "deleted",
referencedSessionIds
}));
const observedSnapshotsBySessionId = new Map(projectedRemovals.flatMap(({ expectedEntry, removal }) => expectedEntry.sessionId && removal.expectedTranscriptSnapshot ? [[expectedEntry.sessionId, removal.expectedTranscriptSnapshot]] : []));
for (const plan of deletePlans) {
const observedSnapshot = observedSnapshotsBySessionId.get(plan.sessionId);
if (observedSnapshot) plan.snapshot = observedSnapshot;
}
const plannedIds = new Set(deletePlans.map((plan) => plan.sessionId));
for (const sessionId of readSessionGenerationIdsForKeys(database, removedKeysToArchive)) {
if (plannedIds.has(sessionId)) continue;
const plan = planSessionStateDeleteIfUnreferenced({
archiveDirectory: params.archiveDirectory,
archiveTranscript: true,
database,
reason: "deleted",
referencedSessionIds,
sessionId
});
if (plan) {
deletePlans.push(plan);
plannedIds.add(sessionId);
}
}
return {
deletePlans,
removals: projectedRemovals,
upsertedEntries
};
}
function collectProjectedReferencedSessionIds(params) {
const excludedSessionKeys = new Set(params.excludedSessionKeys);
const sessionIds = readReferencedSessionIds(params.database, excludedSessionKeys);
for (const entry of Object.values(params.projectedStore)) for (const sessionId of collectSessionStateIdsForEntry(entry)) sessionIds.add(sessionId);
return sessionIds;
}
function deleteSqliteSessionStateRows(database, sessionId) {
const db = getSessionKysely(database.db);
deleteSessionTranscriptIndexInTransaction(database.db, sessionId);
const deleted = executeSqliteQuerySync(database.db, db.deleteFrom("session_windows").where("session_id", "=", sessionId));
return Number(deleted.numAffectedRows ?? 0n) > 0;
}
function planSqliteOrphanLifecycleTranscriptStateDeletes(params) {
const db = getSessionKysely(params.database.db);
const rows = executeSqliteQuerySync(params.database.db, db.selectFrom("session_windows").select([
"session_id",
"session_key",
"plugin_owner_id"
]).orderBy("session_id", "asc")).rows;
const deletePlans = [];
for (const row of rows) {
if (!sessionKeyBelongsToAgent(row.session_key, params.agentId) || params.referencedSessionIds.has(row.session_id) || params.excludedSessionIds?.has(row.session_id) || params.pluginOwnerId && row.plugin_owner_id && row.plugin_owner_id !== params.pluginOwnerId) continue;
if (!sqliteTranscriptStateIsReclaimable({
database: params.database,
sessionId: row.session_id,
nowMs: params.nowMs,
orphanTranscriptMinAgeMs: params.orphanTranscriptMinAgeMs
}) || !sqliteTranscriptStateHasMarker({
database: params.database,
sessionId: row.session_id,
transcriptContentMarker: params.transcriptContentMarker
})) continue;
const plan = planSessionStateDeleteIfUnreferenced({
archiveTranscript: params.archiveRemovedEntryTranscripts,
archiveDirectory: params.archiveDirectory,
database: params.database,
reason: "deleted",
referencedSessionIds: params.referencedSessionIds,
sessionId: row.session_id
});
if (plan) deletePlans.push(plan);
}
return deletePlans;
}
function planSessionLifecycleArtifactCleanup(database, params) {
const db = getSessionKysely(database.db);
const rows = executeSqliteQuerySync(database.db, db.selectFrom("session_nodes").select([
"entry_json",
"session_key",
"current_session_id",
"updated_at"
]).orderBy("session_key", "asc")).rows;
const removedSessionIds = /* @__PURE__ */ new Set();
const entries = [];
const projectedStore = readSessionEntryStore(database);
const foreignOwnedSessionIds = params.pluginOwnerId ? new Set(executeSqliteQuerySync(database.db, db.selectFrom("session_windows").select("session_id").where("plugin_owner_id", "is not", null).where("plugin_owner_id", "!=", params.pluginOwnerId)).rows.map((row) => row.session_id)) : void 0;
for (const row of rows) {
if (!sessionKeyBelongsToAgent(row.session_key, params.agentId) || !sessionKeySegmentStartsWith(row.session_key, params.sessionKeySegmentPrefix)) continue;
const entry = projectedStore[row.session_key];
const sessionIds = uniqueStrings([row.current_session_id, ...entry ? collectSessionStateIdsForEntry(entry) : []]);
if (params.pluginOwnerId && entry?.pluginOwnerId && entry.pluginOwnerId !== params.pluginOwnerId || sessionIds.some((sessionId) => foreignOwnedSessionIds?.has(sessionId))) continue;
if (!sqliteTranscriptStateIsReclaimable({
database,
sessionUpdatedAt: coerceRequiredSqliteNumber(row.updated_at),
sessionId: row.current_session_id,
nowMs: params.nowMs,
orphanTranscriptMinAgeMs: params.orphanTranscriptMinAgeMs
})) continue;
for (const sessionId of sessionIds) removedSessionIds.add(sessionId);
entries.push({
expectedEntry: entry ? cloneSessionEntry(entry) : void 0,
sessionKey: row.session_key
});
delete projectedStore[row.session_key];
}
const referencedSessionIds = collectProjectedReferencedSessionIds({
database,
excludedSessionKeys: entries.map((entry) => entry.sessionKey),
projectedStore
});
const deletePlans = [];
for (const sessionId of removedSessionIds) {
const plan = planSessionStateDeleteIfUnreferenced({
archiveTranscript: params.archiveRemovedEntryTranscripts,
archiveDirectory: params.archiveDirectory,
database,
referencedSessionIds,
sessionId
});
if (plan) deletePlans.push(plan);
}
deletePlans.push(...planSqliteOrphanLifecycleTranscriptStateDeletes({
...params.agentId ? { agentId: params.agentId } : {},
archiveRemovedEntryTranscripts: params.archiveRemovedEntryTranscripts,
archiveDirectory: params.archiveDirectory,
database,
excludedSessionIds: removedSessionIds,
...params.pluginOwnerId ? { pluginOwnerId: params.pluginOwnerId } : {},
referencedSessionIds,
transcriptContentMarker: params.transcriptContentMarker,
orphanTranscriptMinAgeMs: params.orphanTranscriptMinAgeMs,
nowMs: params.nowMs
}));
return {
deletePlans,
entries
};
}
function deletePlannedLifecycleArtifactEntries(database, entries) {
assertPlannedLifecycleArtifactEntriesUnchanged(database, entries);
let removedEntries = 0;
for (const planned of entries) {
deleteSessionEntryRows(database, planned.sessionKey);
removedEntries += 1;
}
return removedEntries;
}
function assertPlannedLifecycleArtifactEntriesUnchanged(database, entries) {
for (const planned of entries) {
const current = readExactSessionEntryRow(database, planned.sessionKey)?.entry;
if (!sqliteSessionEntriesEqual(current, planned.expectedEntry)) throw new Error(`SQLite lifecycle cleanup entry changed for ${planned.sessionKey}`);
}
}
/** Partition only optimistic entry conflicts; database and parse failures stay fatal. */
function partitionUnchangedPlannedLifecycleArtifactEntries(database, entries) {
const changed = [];
const unchanged = [];
for (const planned of entries) {
const current = readExactSessionEntryRow(database, planned.sessionKey)?.entry;
(sqliteSessionEntriesEqual(current, planned.expectedEntry) ? unchanged : changed).push(planned);
}
return {
changed,
unchanged
};
}
//#endregion
export { runSqliteTranscriptArchiveWorkerOperation as C, resolveSqliteTranscriptArchivePath as S, publishTranscriptUpdate as _, partitionUnchangedPlannedLifecycleArtifactEntries as a, materializeSessionStateDeletePlans as b, planSessionStateDeleteIfUnreferenced as c, readReferencedSessionIdsAfterTargetMutation as d, readSessionGenerationIdsForKeys as f, emitArchivedTranscriptUpdates as g, publishSessionStateArchives as h, deletePlannedLifecycleArtifactEntries as i, projectSessionEntryLifecycleMutation as l, prunePublishedSessionArchivesByRetention as m, collectProjectedReferencedSessionIds as n, planSessionLifecycleArtifactCleanup as o, shouldRemoveSessionEntry as p, deleteMaterializedSessionStatePlans as r, planSessionStateAfterEntryRemoval as s, assertPlannedLifecycleArtifactEntriesUnchanged as t, readReferencedSessionIds as u, MAX_MATERIALIZED_ARCHIVE_BATCH_BYTES as v, writeTranscriptArchive as w, publishEncodedSessionTranscriptArchive as x, hashSessionArchiveBytes as y };