openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
656 lines (655 loc) • 29.8 kB
JavaScript
import { y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { r as isCompactionCheckpointTranscriptFileName } from "./artifacts-DMDO9PHy.js";
import { g as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-CWtDoRbC.js";
import { i as captureOwnedTranscriptWriteAssertion } from "./transcript-write-context-mhuh2Sos.js";
import { i as migrateSessionEntries } from "./session-manager-codec-DIXSw5vP.js";
import "./session-accessor-YsytfDtG.js";
import { S as loadTranscriptEventsSync, f as createSessionTranscriptHeader, k as readTranscriptIdentityByEventId, n as appendTranscriptEventsInTransaction } from "./session-accessor.sqlite-transcript-store-B0zw3fAU.js";
import { d as patchSessionEntryCore, l as loadSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js";
import { d as readSessionEntryRow, g as writeSessionEntry, p as readSessionIdentitySnapshot } from "./session-accessor.sqlite-entry-store-BxYl0nro.js";
import { n as parseSqliteSessionFileMarker } from "./legacy-sqlite-marker-COPKCuIN.js";
import { t as collectSessionEntryLookupKeys } from "./store-entry-CzRELcpv.js";
import { a as normalizeSqliteSessionKey, c as resolveSqliteScope, i as getSessionKysely, m as toDatabaseOptions, p as runExclusiveSqliteSessionWrite, r as formatSqliteSessionReferenceForScope } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { n as buildSessionCreationStamp } from "./session-entry-provenance-jzrCUpdQ.js";
import { u as emitCommittedSessionIdentityDiff } from "./session-history-eviction-C4srftLJ.js";
import { c as scanSessionTranscriptTree } from "./transcript-tree-BH69pMSi.js";
import "./types-BHV0IaPK.js";
import "./session-manager-DO0bDxav.js";
import "./sessions-9nxpeTwt.js";
import { r as readFileRangeAsync, t as streamSessionTranscriptLines } from "./transcript-stream-DIGsiEca.js";
import { d as resolveGatewaySessionStoreTarget } from "./session-utils-store-CInT2loy.js";
import "./session-utils-Cai0_C6U.js";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/config/sessions/session-accessor.sqlite-checkpoint.ts
async function branchCompactionCheckpointSession(params) {
return await applySqliteCompactionCheckpointSessionOperation({
...params,
kind: "branch"
});
}
async function restoreCompactionCheckpointSession(params) {
return await applySqliteCompactionCheckpointSessionOperation({
...params,
kind: "restore"
});
}
async function applySqliteCompactionCheckpointSessionOperation(operation) {
const sourceKey = normalizeSqliteSessionKey(operation.kind === "branch" ? operation.sourceStoreKey ?? operation.sourceKey : operation.sessionStoreKey ?? operation.sessionKey);
const targetKey = normalizeSqliteSessionKey(operation.kind === "branch" ? operation.nextKey : operation.sessionKey);
const resolved = resolveSqliteScope({
...operation.agentId ? { agentId: operation.agentId } : {},
...operation.env ? { env: operation.env } : {},
sessionKey: sourceKey,
...operation.storePath ? { storePath: operation.storePath } : {}
});
return await runExclusiveSqliteSessionWrite(resolved, async () => {
const committed = runOpenClawAgentWriteTransaction((database) => {
const identityKeys = uniqueStrings([...collectSessionEntryLookupKeys(database, sourceKey), ...collectSessionEntryLookupKeys(database, targetKey)]);
const previousIdentity = readSessionIdentitySnapshot(database, identityKeys);
const result = applySqliteCompactionCheckpointSessionOperationInTransaction(database, resolved, operation, sourceKey, targetKey);
return {
previousIdentity,
currentIdentity: readSessionIdentitySnapshot(database, identityKeys),
result
};
}, toDatabaseOptions(resolved));
emitCommittedSessionIdentityDiff(committed.previousIdentity, committed.currentIdentity);
return committed.result;
});
}
function applySqliteCompactionCheckpointSessionOperationInTransaction(database, resolved, operation, sourceKey, targetKey) {
const currentEntry = readSessionEntryRow(database, sourceKey)?.entry;
if (!currentEntry?.sessionId) return { status: "missing-session" };
if (currentEntry.sessionId !== operation.expectedState.sessionId || currentEntry.lifecycleRevision !== operation.expectedState.lifecycleRevision) return { status: "conflict" };
if (currentEntry.modelSelectionLocked === true) return { status: "model-selection-locked" };
const checkpoint = readSessionCompactionCheckpoint(currentEntry, operation.checkpointId);
if (!checkpoint) return { status: "missing-checkpoint" };
const forked = forkSqliteCheckpointTranscriptInTransaction(database, resolved, {
checkpoint,
legacySource: operation.legacySource,
targetSessionKey: targetKey
});
if (forked.status !== "created") return forked;
const nextEntry = operation.kind === "branch" ? cloneSqliteCheckpointSessionEntry({
currentEntry,
creation: operation.creation,
label: currentEntry.label?.trim() ? `${currentEntry.label.trim()} (checkpoint)` : "Checkpoint branch",
nextSessionId: forked.sessionId,
parentSessionKey: normalizeSqliteSessionKey(operation.sourceKey),
totalTokens: forked.totalTokens
}) : cloneSqliteCheckpointSessionEntry({
currentEntry,
nextSessionId: forked.sessionId,
preserveCompactionCheckpoints: true,
totalTokens: forked.totalTokens
});
writeSessionEntry(database, targetKey, nextEntry);
return {
status: "created",
key: targetKey,
checkpoint,
entry: nextEntry
};
}
function forkSqliteCheckpointTranscriptInTransaction(database, resolved, params) {
const sources = resolveSqliteCheckpointTranscriptForkSources(params.checkpoint);
if (sources.length === 0) return { status: "missing-boundary" };
let lastFailure = { status: "missing-boundary" };
let selected;
for (const source of sources) {
const rows = readSqliteTranscriptRowsForFork(database, source);
if (rows.status === "created") {
selected = {
source,
rows: rows.events
};
break;
}
lastFailure = rows;
}
const legacySource = selected ? void 0 : resolvePreparedLegacyCheckpointSource(params.checkpoint, params.legacySource);
if (!selected && !legacySource) return lastFailure;
const sessionId = randomUUID();
const targetScope = {
...resolved,
sessionId,
sessionKey: params.targetSessionKey
};
const sessionFile = formatSqliteSessionReferenceForScope(targetScope);
const selectedEvents = selected?.rows ?? legacySource?.events ?? [];
const totalTokens = selected?.source.totalTokens ?? legacySource?.totalTokens;
appendTranscriptEventsInTransaction(database, targetScope, [createSessionTranscriptHeader({
cwd: readTranscriptHeaderCwd(selectedEvents),
sessionId
}), ...selectedEvents.filter((event) => !isSessionTranscriptHeader(event))]);
return {
status: "created",
sessionId,
sessionFile,
...typeof totalTokens === "number" ? { totalTokens } : {}
};
}
function resolvePreparedLegacyCheckpointSource(checkpoint, source) {
if (!source || source.checkpointId !== checkpoint.checkpointId || source.events.length === 0) return;
return [checkpoint.preCompaction, checkpoint.postCompaction].some((position) => {
const sessionFile = position.sessionFile?.trim();
const sourceLeafId = position.entryId?.trim() || position.leafId?.trim() || void 0;
return sessionFile === source.sessionFile && sourceLeafId === source.sourceLeafId;
}) ? source : void 0;
}
function resolveSqliteCheckpointTranscriptForkSources(checkpoint) {
const sources = [];
const checkpointTokensTrusted = checkpoint.tokensVersion === 1;
if (checkpoint.preCompaction.sessionId) {
const preLeafId = checkpoint.preCompaction.entryId ?? checkpoint.preCompaction.leafId;
sources.push({
sessionId: checkpoint.preCompaction.sessionId,
...preLeafId ? { leafId: preLeafId } : {},
...checkpointTokensTrusted && typeof checkpoint.tokensBefore === "number" ? { totalTokens: checkpoint.tokensBefore } : {}
});
}
const postLeafId = checkpoint.postCompaction.entryId ?? checkpoint.postCompaction.leafId;
if (checkpoint.postCompaction.sessionId && postLeafId) sources.push({
sessionId: checkpoint.postCompaction.sessionId,
leafId: postLeafId,
...checkpointTokensTrusted && typeof checkpoint.tokensAfter === "number" ? { totalTokens: checkpoint.tokensAfter } : {}
});
return sources;
}
function readSqliteTranscriptRowsForFork(database, source) {
const boundarySeq = source.leafId ? readTranscriptIdentityByEventId(database, source.sessionId, source.leafId)?.seq : void 0;
if (source.leafId && boundarySeq === void 0) return { status: "missing-boundary" };
const query = getSessionKysely(database.db).selectFrom("transcript_events").select(["event_json", "seq"]).where("session_id", "=", source.sessionId).orderBy("seq", "asc");
const rows = executeSqliteQuerySync(database.db, boundarySeq === void 0 ? query : query.where("seq", "<=", boundarySeq)).rows;
if (rows.length === 0) return { status: "failed" };
try {
return {
status: "created",
events: rows.map((row) => JSON.parse(row.event_json))
};
} catch {
return { status: "failed" };
}
}
function readSessionCompactionCheckpoint(entry, checkpointId) {
const normalizedCheckpointId = checkpointId.trim();
if (!normalizedCheckpointId || !Array.isArray(entry.compactionCheckpoints)) return;
return entry.compactionCheckpoints.find((checkpoint) => checkpoint.checkpointId === normalizedCheckpointId);
}
function cloneSqliteCheckpointSessionEntry(params) {
const hasTotalTokens = typeof params.totalTokens === "number" && Number.isFinite(params.totalTokens);
return {
...params.currentEntry,
...params.creation ? {
...buildSessionCreationStamp(params.creation),
createdActor: params.creation.actor,
sandbox: params.creation.sandbox
} : {},
sessionId: params.nextSessionId,
updatedAt: Date.now(),
systemSent: false,
abortedLastRun: false,
lifecycleRunId: void 0,
lastRunId: void 0,
startedAt: void 0,
endedAt: void 0,
runtimeMs: void 0,
status: void 0,
inputTokens: void 0,
outputTokens: void 0,
cacheRead: void 0,
cacheWrite: void 0,
estimatedCostUsd: void 0,
transcriptByteCompactionLatch: void 0,
totalTokens: hasTotalTokens ? params.totalTokens : void 0,
totalTokensFresh: hasTotalTokens ? true : void 0,
totalTokensVersion: hasTotalTokens ? 1 : void 0,
label: params.label ?? params.currentEntry.label,
parentSessionKey: params.parentSessionKey ?? params.currentEntry.parentSessionKey,
compactionCheckpoints: params.preserveCompactionCheckpoints ? params.currentEntry.compactionCheckpoints : void 0
};
}
function readTranscriptHeaderCwd(events) {
const header = events.find(isSessionTranscriptHeader);
return typeof header?.cwd === "string" && header.cwd.trim() ? header.cwd : void 0;
}
function isSessionTranscriptHeader(event) {
return Boolean(event && typeof event === "object" && !Array.isArray(event) && event.type === "session");
}
//#endregion
//#region src/gateway/session-compaction-checkpoints.ts
const log = createSubsystemLogger("gateway/session-compaction-checkpoints");
const MAX_COMPACTION_CHECKPOINT_LEAF_SCAN_BYTES = 67108864;
const MAX_COMPACTION_CHECKPOINT_RETAINED_BYTES_PER_SESSION = 134217728;
function resolveCompactionCheckpointTranscriptPosition(params) {
const leafId = params.preferredLeafId ?? params.transcriptState?.leafId ?? void 0;
const entryId = params.transcriptState?.entryId ?? leafId;
return {
...leafId ? { leafId } : {},
...entryId ? { entryId } : {}
};
}
function checkpointSnapshotPath(checkpoint) {
return checkpoint.preCompaction.sessionFile?.trim() || void 0;
}
function checkpointSnapshotBytes(checkpoint, snapshotBytesByPath) {
const sessionFile = checkpointSnapshotPath(checkpoint);
if (!sessionFile) return 0;
const bytes = snapshotBytesByPath.get(sessionFile);
return typeof bytes === "number" && Number.isFinite(bytes) && bytes > 0 ? bytes : 0;
}
function trimSessionCheckpoints(checkpoints, snapshotBytesByPath = /* @__PURE__ */ new Map()) {
if (!Array.isArray(checkpoints) || checkpoints.length === 0) return {
kept: void 0,
removed: []
};
const countTrimmed = checkpoints.slice(-25);
const countRemoved = checkpoints.slice(0, Math.max(0, checkpoints.length - countTrimmed.length));
const keptNewestFirst = [];
const byteRemovedNewestFirst = [];
let retainedBytes = 0;
for (let index = countTrimmed.length - 1; index >= 0; index -= 1) {
const checkpoint = countTrimmed[index];
if (!checkpoint) continue;
const checkpointBytes = checkpointSnapshotBytes(checkpoint, snapshotBytesByPath);
if (keptNewestFirst.length === 0 || retainedBytes + checkpointBytes <= MAX_COMPACTION_CHECKPOINT_RETAINED_BYTES_PER_SESSION) {
keptNewestFirst.push(checkpoint);
retainedBytes += checkpointBytes;
} else byteRemovedNewestFirst.push(checkpoint);
}
const kept = keptNewestFirst.toReversed();
return {
kept: kept.length > 0 ? kept : void 0,
removed: [...countRemoved, ...byteRemovedNewestFirst.toReversed()]
};
}
function sessionStoreCheckpoints(entry) {
return Array.isArray(entry?.compactionCheckpoints) ? [...entry.compactionCheckpoints] : [];
}
async function statCheckpointSnapshotBytes(checkpoints) {
const bytesByPath = /* @__PURE__ */ new Map();
await Promise.all(checkpoints.map(async (checkpoint) => {
const sessionFile = checkpointSnapshotPath(checkpoint);
if (!sessionFile || bytesByPath.has(sessionFile)) return;
try {
const stat = await fs.stat(sessionFile);
bytesByPath.set(sessionFile, stat.isFile() ? stat.size : 0);
} catch {
bytesByPath.set(sessionFile, 0);
}
}));
return bytesByPath;
}
/** Resolve the stored checkpoint reason from compaction trigger state. */
function resolveSessionCompactionCheckpointReason(params) {
if (params.trigger === "manual") return "manual";
if (params.timedOut) return "timeout-retry";
if (params.trigger === "overflow") return "overflow-retry";
return "auto-threshold";
}
const SESSION_HEADER_READ_MAX_BYTES = 65536;
const SESSION_TAIL_READ_INITIAL_BYTES = 65536;
async function readSessionHeaderFromTranscriptAsync(sessionFile) {
let fileHandle;
try {
fileHandle = await fs.open(sessionFile, "r");
const buffer = await readFileRangeAsync(fileHandle, 0, SESSION_HEADER_READ_MAX_BYTES);
if (buffer.length <= 0) return null;
const firstLine = buffer.toString("utf-8").split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
if (!firstLine) return null;
const parsed = JSON.parse(firstLine);
if (parsed.type !== "session" || typeof parsed.id !== "string" || !parsed.id.trim()) return null;
return {
id: parsed.id.trim(),
...typeof parsed.cwd === "string" && parsed.cwd.trim() ? { cwd: parsed.cwd } : {}
};
} catch {
return null;
} finally {
if (fileHandle) await fileHandle.close().catch(() => void 0);
}
}
async function readSessionIdFromTranscriptHeaderAsync(sessionFile) {
return (await readSessionHeaderFromTranscriptAsync(sessionFile))?.id ?? null;
}
function parseTranscriptLine(line) {
try {
const parsed = JSON.parse(line);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
async function readTranscriptEntriesForForkAsync(params) {
const entries = [];
const stopAfterEntryId = params.stopAfterEntryId?.trim();
let foundStopEntry = false;
try {
for await (const line of streamSessionTranscriptLines(params.sessionFile)) try {
const parsed = JSON.parse(line);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
entries.push(parsed);
if (stopAfterEntryId && parsed.type !== "session" && parsed.id === stopAfterEntryId) {
foundStopEntry = true;
break;
}
} catch {}
} catch {
return null;
}
const firstEntry = entries[0];
if (firstEntry?.type !== "session" || typeof firstEntry.id !== "string") return null;
if (stopAfterEntryId && !foundStopEntry) return null;
return entries;
}
function trimTranscriptEntriesThroughLeaf(entries, leafId) {
const normalizedLeafId = leafId?.trim();
if (!normalizedLeafId) return entries;
const leafIndex = entries.findIndex((entry, index) => index > 0 && entry.id === normalizedLeafId);
if (leafIndex < 1) return null;
return entries.slice(0, leafIndex + 1);
}
async function readSessionLeafStateFromTranscriptAsync(sessionFile, maxBytes = MAX_COMPACTION_CHECKPOINT_LEAF_SCAN_BYTES) {
if (typeof sessionFile !== "string") return readSessionLeafStateFromRecords(loadTranscriptEventsSync(sessionFile).filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)));
const sqliteMarker = parseSqliteSessionFileMarker(sessionFile);
if (sqliteMarker) return readSessionLeafStateFromRecords(loadTranscriptEventsSync(sqliteMarker).filter((event) => Boolean(event) && typeof event === "object" && !Array.isArray(event)));
let fileHandle;
try {
fileHandle = await fs.open(sessionFile, "r");
const stat = await fileHandle.stat();
if (!stat.isFile() || stat.size <= 0) return null;
const requestedMaxBytes = Number.isFinite(maxBytes) ? Math.max(1024, Math.floor(maxBytes)) : MAX_COMPACTION_CHECKPOINT_LEAF_SCAN_BYTES;
const maxReadableBytes = Math.min(stat.size, requestedMaxBytes);
let readLength = Math.min(maxReadableBytes, SESSION_TAIL_READ_INITIAL_BYTES);
while (readLength > 0) {
const readStart = Math.max(0, stat.size - readLength);
const lines = (await readFileRangeAsync(fileHandle, readStart, readLength)).toString("utf-8").split(/\r?\n/);
const candidateLines = readStart > 0 ? lines.slice(1) : lines;
const records = [];
let latestEntryId;
for (const candidateLine of candidateLines) {
const line = candidateLine.trim();
if (!line) continue;
const parsed = parseTranscriptLine(line);
if (!parsed) continue;
records.push(parsed);
if (parsed.type === "session") continue;
const entryId = typeof parsed.id === "string" ? parsed.id.trim() : "";
if (entryId) latestEntryId = entryId;
}
const tree = scanSessionTranscriptTree(records);
if (latestEntryId && tree.hasLeafUpdate && (!tree.hasInvalidLeafControl || readStart === 0)) return {
entryId: latestEntryId,
leafId: tree.leafId
};
if (readStart === 0) return null;
const nextReadLength = Math.min(maxReadableBytes, readLength * 2);
if (nextReadLength === readLength) return latestEntryId ? {
entryId: latestEntryId,
leafId: latestEntryId
} : null;
readLength = nextReadLength;
}
} catch {
return null;
} finally {
if (fileHandle) await fileHandle.close().catch(() => void 0);
}
return null;
}
function readSessionLeafStateFromRecords(records) {
let latestEntryId;
for (const record of records) {
if (record.type === "session") continue;
const entryId = typeof record.id === "string" ? record.id.trim() : "";
if (entryId) latestEntryId = entryId;
}
if (!latestEntryId) return null;
const tree = scanSessionTranscriptTree(records);
return {
entryId: latestEntryId,
leafId: tree.leafId
};
}
function resolveCheckpointTranscriptForkSource(checkpoint) {
const checkpointTokensTrusted = checkpoint.tokensVersion === 1;
const preCompactionFile = checkpoint.preCompaction.sessionFile?.trim();
if (preCompactionFile) return {
sourceFile: preCompactionFile,
sourceLeafId: checkpoint.preCompaction.entryId ?? checkpoint.preCompaction.leafId,
totalTokens: checkpointTokensTrusted ? checkpoint.tokensBefore : void 0
};
const postCompactionFile = checkpoint.postCompaction.sessionFile?.trim();
if (!postCompactionFile) return null;
const postCompactionLeafId = checkpoint.postCompaction.entryId ?? checkpoint.postCompaction.leafId;
if (!postCompactionLeafId) return null;
return {
sourceFile: postCompactionFile,
sourceLeafId: postCompactionLeafId,
totalTokens: checkpointTokensTrusted ? checkpoint.tokensAfter : void 0
};
}
async function prepareLegacyCheckpointSource(checkpoint) {
if (!checkpoint) return;
const forkSource = resolveCheckpointTranscriptForkSource(checkpoint);
if (!forkSource) return;
const entries = await readTranscriptEntriesForForkAsync({
sessionFile: forkSource.sourceFile,
stopAfterEntryId: forkSource.sourceLeafId
});
if (!entries) return;
migrateSessionEntries(entries);
const events = trimTranscriptEntriesThroughLeaf(entries, forkSource.sourceLeafId);
if (!events) return;
return {
checkpointId: checkpoint.checkpointId,
events,
sessionFile: forkSource.sourceFile,
...forkSource.sourceLeafId ? { sourceLeafId: forkSource.sourceLeafId } : {},
...typeof forkSource.totalTokens === "number" ? { totalTokens: forkSource.totalTokens } : {}
};
}
function findCheckpoint(entry, checkpointId) {
return entry?.compactionCheckpoints?.find((checkpoint) => checkpoint.checkpointId === checkpointId);
}
async function branchCheckpointSessionFromStoredBoundary(params) {
const legacySource = await prepareLegacyCheckpointSource(findCheckpoint(loadSessionEntry({
...params.agentId ? { agentId: params.agentId } : {},
storePath: params.storePath,
sessionKey: params.sourceStoreKey ?? params.sourceKey
}), params.checkpointId));
return await branchCompactionCheckpointSession({
...params.agentId ? { agentId: params.agentId } : {},
storePath: params.storePath,
sourceKey: params.sourceKey,
nextKey: params.nextKey,
checkpointId: params.checkpointId,
expectedState: params.expectedState,
creation: params.creation,
...params.sourceStoreKey ? { sourceStoreKey: params.sourceStoreKey } : {},
...legacySource ? { legacySource } : {}
});
}
async function restoreCheckpointSessionFromStoredBoundary(params) {
const legacySource = await prepareLegacyCheckpointSource(findCheckpoint(loadSessionEntry({
...params.agentId ? { agentId: params.agentId } : {},
storePath: params.storePath,
sessionKey: params.sessionStoreKey ?? params.sessionKey
}), params.checkpointId));
return await restoreCompactionCheckpointSession({
...params.agentId ? { agentId: params.agentId } : {},
storePath: params.storePath,
sessionKey: params.sessionKey,
checkpointId: params.checkpointId,
expectedState: params.expectedState,
...params.sessionStoreKey ? { sessionStoreKey: params.sessionStoreKey } : {},
...legacySource ? { legacySource } : {}
});
}
/**
* Creates the current file-backed compaction checkpoint domain store.
*
* The branch/restore operations own the transcript fork plus session entry
* update so a SQLite implementation can copy transcript rows and update
* `session_nodes.entry_json` inside one write transaction.
*/
function createFileBackedCompactionCheckpointStore() {
return {
captureSnapshot: captureCompactionCheckpointSnapshotAsync,
persistCheckpoint: persistSessionCompactionCheckpoint,
cleanupSnapshot: cleanupCompactionCheckpointSnapshot,
branchCheckpointSession: branchCheckpointSessionFromStoredBoundary,
restoreCheckpointSession: restoreCheckpointSessionFromStoredBoundary
};
}
/**
* Capture the stable pre-compaction identity without duplicating the transcript.
* Branch/restore uses the compacted successor transcript, while legacy
* checkpoints that already have a snapshot file keep working.
*/
async function captureCompactionCheckpointSnapshotAsync(params) {
const getLeafId = params.sessionManager && typeof params.sessionManager.getLeafId === "function" ? params.sessionManager.getLeafId.bind(params.sessionManager) : null;
const sessionFile = params.sessionFile.trim();
if (!sessionFile || params.sessionManager && !getLeafId) return null;
const liveLeafId = getLeafId ? getLeafId() : void 0;
if (getLeafId && !liveLeafId) return null;
const maxBytes = params.maxBytes ?? MAX_COMPACTION_CHECKPOINT_LEAF_SCAN_BYTES;
const sqliteTarget = params.sessionTarget ?? parseSqliteSessionFileMarker(sessionFile);
if (sqliteTarget) {
if (typeof params.sessionManager?.getEntries !== "function") return null;
const position = resolveCompactionCheckpointTranscriptPosition({
preferredLeafId: liveLeafId,
transcriptState: readSessionLeafStateFromRecords(params.sessionManager.getEntries())
});
const leafId = position.leafId;
if (!leafId) return null;
return {
sessionId: typeof params.sessionManager.getSessionId === "function" ? params.sessionManager.getSessionId() : sqliteTarget.sessionId,
leafId,
...position.entryId ? { entryId: position.entryId } : {}
};
}
const sessionId = await readSessionIdFromTranscriptHeaderAsync(sessionFile);
const position = resolveCompactionCheckpointTranscriptPosition({
preferredLeafId: liveLeafId,
transcriptState: await readSessionLeafStateFromTranscriptAsync(sessionFile, maxBytes)
});
const leafId = position.leafId;
if (!sessionId || !leafId) return null;
return {
sessionId,
leafId,
...position.entryId ? { entryId: position.entryId } : {}
};
}
async function cleanupCompactionCheckpointSnapshot(snapshot) {
if (!snapshot?.sessionFile) return;
try {
await fs.unlink(snapshot.sessionFile);
} catch {}
}
async function cleanupTrimmedCompactionCheckpointFiles(params) {
if (params.removed.length === 0 || !params.artifactDir) return;
const artifactDir = path.resolve(params.artifactDir);
const retainedPaths = new Set((params.retained ?? []).map((checkpoint) => checkpoint.preCompaction.sessionFile?.trim()).filter((filePath) => Boolean(filePath)));
for (const checkpoint of params.removed) {
const sessionFile = checkpoint.preCompaction.sessionFile?.trim();
if (!sessionFile || retainedPaths.has(sessionFile)) continue;
const resolvedSessionFile = path.resolve(sessionFile);
if (path.dirname(resolvedSessionFile) !== artifactDir || !isCompactionCheckpointTranscriptFileName(path.basename(resolvedSessionFile))) continue;
try {
await fs.unlink(resolvedSessionFile);
} catch {}
}
}
async function persistSessionCompactionCheckpoint(params) {
const snapshotSessionFile = params.snapshot.sessionFile?.trim();
const postSessionFile = params.postSessionFile?.trim();
const snapshotSqliteMarker = parseSqliteSessionFileMarker(snapshotSessionFile);
const postSqliteMarker = parseSqliteSessionFileMarker(postSessionFile);
const snapshotArtifactFile = snapshotSqliteMarker ? void 0 : snapshotSessionFile;
const postArtifactFile = postSqliteMarker ? void 0 : postSessionFile;
const postSourceLeafId = params.postEntryId?.trim() || params.postLeafId?.trim();
if (!snapshotArtifactFile && !postSourceLeafId) {
log.warn("skipping compaction checkpoint persist: missing stable fork source", { sessionKey: params.sessionKey });
return null;
}
const target = resolveGatewaySessionStoreTarget({
cfg: params.cfg,
key: params.sessionKey,
...params.agentId ? { agentId: params.agentId } : {}
});
const assertCommitAllowed = captureOwnedTranscriptWriteAssertion({
agentId: target.agentId,
sessionId: params.sessionId,
sessionKey: target.canonicalKey,
storePath: target.storePath
});
const createdAt = params.createdAt ?? Date.now();
const checkpoint = {
checkpointId: randomUUID(),
sessionKey: target.canonicalKey,
sessionId: params.sessionId,
createdAt,
reason: params.reason,
tokensVersion: 1,
...typeof params.tokensBefore === "number" ? { tokensBefore: params.tokensBefore } : {},
...typeof params.tokensAfter === "number" ? { tokensAfter: params.tokensAfter } : {},
...params.summary?.trim() ? { summary: params.summary.trim() } : {},
...params.firstKeptEntryId?.trim() ? { firstKeptEntryId: params.firstKeptEntryId.trim() } : {},
preCompaction: {
sessionId: params.snapshot.sessionId,
...snapshotArtifactFile ? { sessionFile: snapshotArtifactFile } : {},
leafId: params.snapshot.leafId,
...params.snapshot.entryId?.trim() ? { entryId: params.snapshot.entryId.trim() } : {}
},
postCompaction: {
sessionId: params.sessionId,
...postArtifactFile ? { sessionFile: postArtifactFile } : {},
...params.postLeafId?.trim() ? { leafId: params.postLeafId.trim() } : {},
...params.postEntryId?.trim() ? { entryId: params.postEntryId.trim() } : {}
}
};
let trimmedCheckpoints;
let stored = false;
if (!await patchSessionEntryCore({
agentId: target.agentId,
storePath: target.storePath,
sessionKey: target.canonicalKey
}, async (existing) => {
if (!existing.sessionId) return null;
const checkpoints = sessionStoreCheckpoints(existing);
checkpoints.push(checkpoint);
trimmedCheckpoints = trimSessionCheckpoints(checkpoints, await statCheckpointSnapshotBytes(checkpoints));
stored = true;
return {
updatedAt: Math.max(existing.updatedAt ?? 0, createdAt),
compactionCheckpoints: trimmedCheckpoints.kept
};
}, { assertCommitAllowed }) || !stored) {
log.warn("skipping compaction checkpoint persist: session not found", { sessionKey: params.sessionKey });
return null;
}
const checkpointArtifactFile = snapshotArtifactFile || postArtifactFile || "";
await cleanupTrimmedCompactionCheckpointFiles({
removed: trimmedCheckpoints?.removed ?? [],
retained: trimmedCheckpoints?.kept,
...checkpointArtifactFile ? { artifactDir: path.dirname(checkpointArtifactFile) } : {}
});
return checkpoint;
}
function listSessionCompactionCheckpoints(entry) {
return sessionStoreCheckpoints(entry).toSorted((a, b) => b.createdAt - a.createdAt);
}
function getSessionCompactionCheckpoint(params) {
const checkpointId = params.checkpointId.trim();
if (!checkpointId) return;
return listSessionCompactionCheckpoints(params.entry).find((checkpoint) => checkpoint.checkpointId === checkpointId);
}
//#endregion
export { resolveCompactionCheckpointTranscriptPosition as a, readSessionLeafStateFromTranscriptAsync as i, getSessionCompactionCheckpoint as n, resolveSessionCompactionCheckpointReason as o, listSessionCompactionCheckpoints as r, createFileBackedCompactionCheckpointStore as t };