
























@@ -98,7 +98,7 @@ import {
9898resolveOwnedSessionTranscriptWriteLockRunner,
9999withOwnedSessionTranscriptWrites,
100100} from "./transcript-write-context.js";
101-import type { SessionEntry } from "./types.js";
101+import type { SessionCompactionCheckpoint, SessionEntry } from "./types.js";
102102103103/**
104104 * Session access API for callers that need entries or transcripts without
@@ -466,6 +466,81 @@ export type RestartRecoveryLifecycleUpdate<T> = {
466466replacements?: Iterable<RestartRecoveryLifecycleReplacement>;
467467};
468468469+/** File-backed checkpoint transcript fork produced by the checkpoint storage boundary. */
470+export type SessionCompactionCheckpointForkedTranscript = {
471+sessionFile: string;
472+sessionId: string;
473+totalTokens?: number;
474+};
475+476+/** Result of resolving and copying checkpoint transcript content for branch/restore. */
477+export type SessionCompactionCheckpointTranscriptForkResult =
478+| { status: "created"; transcript: SessionCompactionCheckpointForkedTranscript }
479+| { status: "missing-boundary" }
480+| { status: "failed" };
481+482+/** Result of applying a checkpoint branch or restore mutation to session storage. */
483+export type SessionCompactionCheckpointMutationResult =
484+| {
485+status: "created";
486+key: string;
487+checkpoint: SessionCompactionCheckpoint;
488+entry: SessionEntry;
489+}
490+| { status: "missing-session" }
491+| { status: "missing-checkpoint" }
492+| { status: "missing-boundary" }
493+| { status: "failed" };
494+495+export type SessionCompactionCheckpointEntryBuildContext = {
496+/** Checkpoint row selected from the current persisted session entry. */
497+checkpoint: SessionCompactionCheckpoint;
498+/** Persisted entry that owns the selected checkpoint. */
499+currentEntry: SessionEntry;
500+/** Forked transcript identity created from the stored checkpoint boundary. */
501+forkedTranscript: SessionCompactionCheckpointForkedTranscript;
502+};
503+504+export type SessionCompactionCheckpointTranscriptForker = (
505+checkpoint: SessionCompactionCheckpoint,
506+) => Promise<SessionCompactionCheckpointTranscriptForkResult>;
507+508+export type SessionCompactionCheckpointEntryBuilder = (
509+context: SessionCompactionCheckpointEntryBuildContext,
510+) => Promise<SessionEntry> | SessionEntry;
511+512+export type BranchSessionFromCompactionCheckpointParams = {
513+/** Checkpoint id stored on the source session entry. */
514+checkpointId: string;
515+/** Builds the branched session entry from the forked transcript. */
516+buildEntry: SessionCompactionCheckpointEntryBuilder;
517+/** Copies transcript content through the stored checkpoint boundary. */
518+forkTranscriptFromCheckpoint: SessionCompactionCheckpointTranscriptForker;
519+/** Persisted key for the new checkpoint branch. */
520+nextKey: string;
521+/** Canonical key used as the branch parent. */
522+sourceKey: string;
523+/** Actual persisted key to read when a legacy alias still owns the row. */
524+sourceStoreKey?: string;
525+/** Explicit store target for file-backed stores and SQLite migration adapters. */
526+storePath: string;
527+};
528+529+export type RestoreSessionFromCompactionCheckpointParams = {
530+/** Checkpoint id stored on the current session entry. */
531+checkpointId: string;
532+/** Builds the restored session entry from the forked transcript. */
533+buildEntry: SessionCompactionCheckpointEntryBuilder;
534+/** Copies transcript content through the stored checkpoint boundary. */
535+forkTranscriptFromCheckpoint: SessionCompactionCheckpointTranscriptForker;
536+/** Canonical key to replace with the restored checkpoint state. */
537+sessionKey: string;
538+/** Actual persisted key to read when a legacy alias still owns the row. */
539+sessionStoreKey?: string;
540+/** Explicit store target for file-backed stores and SQLite migration adapters. */
541+storePath: string;
542+};
543+469544export type SessionEntryCreateWithTranscriptContext = {
470545/** Current entry under the requested key before creation, if any. */
471546existingEntry?: SessionEntry;
@@ -1164,6 +1239,103 @@ function applySessionAbortCutoff(
11641239entry.abortCutoffTimestamp = cutoff?.timestamp;
11651240}
116612411242+function findSessionCompactionCheckpoint(params: {
1243+checkpointId: string;
1244+entry: SessionEntry;
1245+}): SessionCompactionCheckpoint | undefined {
1246+const checkpointId = params.checkpointId.trim();
1247+if (!checkpointId || !Array.isArray(params.entry.compactionCheckpoints)) {
1248+return undefined;
1249+}
1250+return [...params.entry.compactionCheckpoints]
1251+.toSorted((a, b) => b.createdAt - a.createdAt)
1252+.find((checkpoint) => checkpoint.checkpointId === checkpointId);
1253+}
1254+1255+type ApplySessionCompactionCheckpointMutationParams = {
1256+buildEntry: SessionCompactionCheckpointEntryBuilder;
1257+checkpointId: string;
1258+forkTranscriptFromCheckpoint: SessionCompactionCheckpointTranscriptForker;
1259+readKey: string;
1260+storePath: string;
1261+writeKey: string;
1262+};
1263+1264+async function applySessionCompactionCheckpointMutation(
1265+params: ApplySessionCompactionCheckpointMutationParams,
1266+): Promise<SessionCompactionCheckpointMutationResult> {
1267+return await updateSessionStore(
1268+params.storePath,
1269+async (store) => {
1270+const currentEntry = store[params.readKey];
1271+if (!currentEntry?.sessionId) {
1272+return { status: "missing-session" };
1273+}
1274+const checkpoint = findSessionCompactionCheckpoint({
1275+entry: currentEntry,
1276+checkpointId: params.checkpointId,
1277+});
1278+if (!checkpoint) {
1279+return { status: "missing-checkpoint" };
1280+}
1281+const forkedSession = await params.forkTranscriptFromCheckpoint(checkpoint);
1282+if (forkedSession.status !== "created") {
1283+return forkedSession;
1284+}
1285+1286+const nextEntry = await params.buildEntry({
1287+ checkpoint,
1288+ currentEntry,
1289+forkedTranscript: forkedSession.transcript,
1290+});
1291+store[params.writeKey] = nextEntry;
1292+return {
1293+status: "created",
1294+key: params.writeKey,
1295+ checkpoint,
1296+entry: nextEntry,
1297+};
1298+},
1299+{ skipSaveWhenResult: (result) => result.status !== "created" },
1300+);
1301+}
1302+1303+/**
1304+ * Forks checkpoint transcript content and persists a new branch entry in one
1305+ * storage-sized mutation. SQLite adapters implement the transcript row copy
1306+ * and `session_entries.entry_json` insert inside the same write transaction.
1307+ */
1308+export async function branchSessionFromCompactionCheckpoint(
1309+params: BranchSessionFromCompactionCheckpointParams,
1310+): Promise<SessionCompactionCheckpointMutationResult> {
1311+return await applySessionCompactionCheckpointMutation({
1312+buildEntry: params.buildEntry,
1313+checkpointId: params.checkpointId,
1314+forkTranscriptFromCheckpoint: params.forkTranscriptFromCheckpoint,
1315+readKey: params.sourceStoreKey ?? params.sourceKey,
1316+storePath: params.storePath,
1317+writeKey: params.nextKey,
1318+});
1319+}
1320+1321+/**
1322+ * Forks checkpoint transcript content and replaces the current entry in one
1323+ * storage-sized mutation. SQLite adapters implement the transcript row copy
1324+ * and `session_entries.entry_json` update inside the same write transaction.
1325+ */
1326+export async function restoreSessionFromCompactionCheckpoint(
1327+params: RestoreSessionFromCompactionCheckpointParams,
1328+): Promise<SessionCompactionCheckpointMutationResult> {
1329+return await applySessionCompactionCheckpointMutation({
1330+buildEntry: params.buildEntry,
1331+checkpointId: params.checkpointId,
1332+forkTranscriptFromCheckpoint: params.forkTranscriptFromCheckpoint,
1333+readKey: params.sessionStoreKey ?? params.sessionKey,
1334+storePath: params.storePath,
1335+writeKey: params.sessionKey,
1336+});
1337+}
1338+11671339/**
11681340 * Applies a session patch projection through the accessor boundary.
11691341 * The resolver sees a read-only snapshot and names the persisted key set; the
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。