refactor: rename checkpoint transcript locators

This commit is contained in:
Peter Steinberger
2026-05-09 08:58:25 +01:00
parent f4f94ec714
commit 16d2d29eda
4 changed files with 64 additions and 60 deletions

View File

@@ -782,7 +782,9 @@ Move these into agent databases:
- Agent transcript events. Done for runtime writes.
- Compaction checkpoints and transcript snapshots. Done for runtime writes:
checkpoint transcript copies are SQLite transcript rows and checkpoint
metadata is recorded in `transcript_snapshots`.
metadata is recorded in `transcript_snapshots`. Gateway checkpoint helpers
now name these values as transcript locators rather than source/snapshot
files.
- Agent VFS scratch/workspace namespaces. Done for runtime VFS writes.
- Tool artifacts. Done for runtime writes.
- Run artifacts. Done for worker runtime writes through the per-agent

View File

@@ -1258,7 +1258,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const branchedSession = await forkCompactionCheckpointTranscriptAsync({
agentId: target.agentId,
sourceFile: checkpoint.preCompaction.transcriptLocator,
sourceTranscriptLocator: checkpoint.preCompaction.transcriptLocator,
sourceSessionId: checkpoint.preCompaction.sessionId,
});
if (!branchedSession?.transcriptLocator) {
@@ -1374,7 +1374,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
const target = resolveGatewaySessionDatabaseTarget({ cfg: loaded.cfg, key: canonicalKey });
const restoredSession = await forkCompactionCheckpointTranscriptAsync({
agentId: target.agentId,
sourceFile: checkpoint.preCompaction.transcriptLocator,
sourceTranscriptLocator: checkpoint.preCompaction.transcriptLocator,
sourceSessionId: checkpoint.preCompaction.sessionId,
});
if (!restoredSession?.transcriptLocator) {

View File

@@ -61,9 +61,9 @@ describe("session-compaction-checkpoints", () => {
timestamp: Date.now(),
} as AssistantMessage);
const sessionFile = session.getSessionFile();
const transcriptLocator = session.getTranscriptLocator();
const leafId = session.getLeafId();
expect(sessionFile).toBeTruthy();
expect(transcriptLocator).toBeTruthy();
expect(leafId).toBeTruthy();
const sessionManagerOpenSpy = vi.spyOn(SessionManager, "open");
@@ -74,7 +74,7 @@ describe("session-compaction-checkpoints", () => {
try {
const snapshot = await captureCompactionCheckpointSnapshotAsync({
sessionManager: session,
sessionFile: sessionFile!,
transcriptLocator: transcriptLocator!,
});
expect(sessionManagerOpenSpy).not.toHaveBeenCalled();
@@ -82,8 +82,8 @@ describe("session-compaction-checkpoints", () => {
expect(snapshot?.agentId).toBe(DEFAULT_AGENT_ID);
expect(snapshot?.sourceSessionId).toBe(session.getSessionId());
expect(snapshot?.leafId).toBe(leafId);
expect(snapshot?.sessionFile).not.toBe(sessionFile);
expect(snapshot?.sessionFile).toContain("sqlite-transcript://");
expect(snapshot?.transcriptLocator).not.toBe(transcriptLocator);
expect(snapshot?.transcriptLocator).toContain("sqlite-transcript://");
expect(
hasSqliteSessionTranscriptSnapshot({
agentId: DEFAULT_AGENT_ID,
@@ -153,19 +153,19 @@ describe("session-compaction-checkpoints", () => {
timestamp: Date.now(),
} as unknown as AssistantMessage);
const sessionFile = session.getSessionFile();
const transcriptLocator = session.getTranscriptLocator();
const sessionId = session.getSessionId();
const leafId = session.getLeafId();
expect(sessionFile).toBeTruthy();
expect(transcriptLocator).toBeTruthy();
expect(sessionId).toBeTruthy();
expect(leafId).toBeTruthy();
const sessionManagerOpenSpy = vi.spyOn(SessionManager, "open");
let snapshot: Awaited<ReturnType<typeof captureCompactionCheckpointSnapshotAsync>> = null;
try {
expect(await readSessionLeafIdFromTranscriptAsync(sessionFile!)).toBe(leafId);
expect(await readSessionLeafIdFromTranscriptAsync(transcriptLocator!)).toBe(leafId);
snapshot = await captureCompactionCheckpointSnapshotAsync({
sessionFile: sessionFile!,
transcriptLocator: transcriptLocator!,
});
expect(sessionManagerOpenSpy).not.toHaveBeenCalled();
@@ -174,8 +174,8 @@ describe("session-compaction-checkpoints", () => {
expect(snapshot?.sourceSessionId).toBe(sessionId);
expect(snapshot?.sessionId).not.toBe(sessionId);
expect(snapshot?.leafId).toBe(leafId);
expect(snapshot?.sessionFile).not.toBe(sessionFile);
expect(snapshot?.sessionFile).toContain("sqlite-transcript://");
expect(snapshot?.transcriptLocator).not.toBe(transcriptLocator);
expect(snapshot?.transcriptLocator).toContain("sqlite-transcript://");
} finally {
await cleanupCompactionCheckpointSnapshot(snapshot);
sessionManagerOpenSpy.mockRestore();
@@ -184,14 +184,14 @@ describe("session-compaction-checkpoints", () => {
test("async capture keeps checkpoint transcript locators virtual for SQLite sources", async () => {
const sourceSessionId = "source-capture-virtual";
const sourceFile = createSqliteSessionTranscriptLocator({
const sourceTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: DEFAULT_AGENT_ID,
sessionId: sourceSessionId,
});
replaceSqliteSessionTranscriptEvents({
agentId: DEFAULT_AGENT_ID,
sessionId: sourceSessionId,
transcriptPath: sourceFile,
transcriptPath: sourceTranscriptLocator,
events: [
{
type: "session",
@@ -209,15 +209,15 @@ describe("session-compaction-checkpoints", () => {
});
const snapshot = await captureCompactionCheckpointSnapshotAsync({
sessionFile: sourceFile,
transcriptLocator: sourceTranscriptLocator,
});
expect(snapshot).not.toBeNull();
expect(snapshot?.leafId).toBe("capture-leaf");
expect(snapshot?.sessionFile).toBeTruthy();
expect(isSqliteSessionTranscriptLocator(snapshot?.sessionFile)).toBe(true);
expect(snapshot?.sessionFile).toContain("sqlite-transcript://");
expect(snapshot?.sessionFile).not.toMatch(/^sqlite-transcript:\/[^/]/u);
expect(snapshot?.transcriptLocator).toBeTruthy();
expect(isSqliteSessionTranscriptLocator(snapshot?.transcriptLocator)).toBe(true);
expect(snapshot?.transcriptLocator).toContain("sqlite-transcript://");
expect(snapshot?.transcriptLocator).not.toMatch(/^sqlite-transcript:\/[^/]/u);
expect(
hasSqliteSessionTranscriptSnapshot({
agentId: DEFAULT_AGENT_ID,
@@ -228,7 +228,7 @@ describe("session-compaction-checkpoints", () => {
expect(readSqliteTranscriptEvents(snapshot!.sessionId)[0]).toMatchObject({
type: "session",
id: snapshot!.sessionId,
parentSession: sourceFile,
parentSession: sourceTranscriptLocator,
});
});
@@ -242,12 +242,12 @@ describe("session-compaction-checkpoints", () => {
content: "before compaction",
timestamp: Date.now(),
});
const sessionFile = session.getSessionFile();
expect(sessionFile).toBeTruthy();
const transcriptLocator = session.getTranscriptLocator();
expect(transcriptLocator).toBeTruthy();
const snapshot = await captureCompactionCheckpointSnapshotAsync({
sessionManager: session,
sessionFile: sessionFile!,
transcriptLocator: transcriptLocator!,
maxBytes: 64,
});
@@ -274,21 +274,21 @@ describe("session-compaction-checkpoints", () => {
timestamp: Date.now(),
} as unknown as AssistantMessage);
const sessionFile = session.getSessionFile();
expect(sessionFile).toBeTruthy();
const transcriptLocator = session.getTranscriptLocator();
expect(transcriptLocator).toBeTruthy();
const openSpy = vi.spyOn(SessionManager, "open");
const forkSpy = vi.spyOn(SessionManager, "forkFrom");
let forked: Awaited<ReturnType<typeof forkCompactionCheckpointTranscriptAsync>> = null;
try {
forked = await forkCompactionCheckpointTranscriptAsync({
sourceFile: sessionFile!,
sourceTranscriptLocator: transcriptLocator!,
});
expect(openSpy).not.toHaveBeenCalled();
expect(forkSpy).not.toHaveBeenCalled();
expect(forked).not.toBeNull();
expect(forked?.sessionFile).not.toBe(sessionFile);
expect(forked?.transcriptLocator).not.toBe(transcriptLocator);
expect(forked?.sessionId).toBeTruthy();
} finally {
openSpy.mockRestore();
@@ -302,7 +302,7 @@ describe("session-compaction-checkpoints", () => {
type: "session",
id: forked!.sessionId,
cwd: dir,
parentSession: sessionFile,
parentSession: transcriptLocator,
});
expect(forkedEntries.slice(1)).toEqual(
sourceEntries.filter((entry) => entry.type !== "session"),
@@ -311,14 +311,14 @@ describe("session-compaction-checkpoints", () => {
test("async fork keeps transcript locators virtual for SQLite sources", async () => {
const sourceSessionId = "source-fork-virtual";
const sourceFile = createSqliteSessionTranscriptLocator({
const sourceTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: DEFAULT_AGENT_ID,
sessionId: sourceSessionId,
});
replaceSqliteSessionTranscriptEvents({
agentId: DEFAULT_AGENT_ID,
sessionId: sourceSessionId,
transcriptPath: sourceFile,
transcriptPath: sourceTranscriptLocator,
events: [
{
type: "session",
@@ -336,20 +336,20 @@ describe("session-compaction-checkpoints", () => {
});
const forked = await forkCompactionCheckpointTranscriptAsync({
sourceFile,
sourceTranscriptLocator,
});
expect(forked).not.toBeNull();
expect(forked?.sessionId).toBeTruthy();
expect(isSqliteSessionTranscriptLocator(forked?.sessionFile)).toBe(true);
expect(forked?.sessionFile).toContain("sqlite-transcript://");
expect(forked?.sessionFile).not.toMatch(/^sqlite-transcript:\/[^/]/u);
expect(isSqliteSessionTranscriptLocator(forked?.transcriptLocator)).toBe(true);
expect(forked?.transcriptLocator).toContain("sqlite-transcript://");
expect(forked?.transcriptLocator).not.toMatch(/^sqlite-transcript:\/[^/]/u);
const forkedEntries = readSqliteTranscriptEvents(forked!.sessionId);
expect(forkedEntries[0]).toMatchObject({
type: "session",
id: forked!.sessionId,
cwd: "/tmp/openclaw-virtual-fork",
parentSession: sourceFile,
parentSession: sourceTranscriptLocator,
});
expect(forkedEntries[1]).toMatchObject({
type: "message",
@@ -364,7 +364,7 @@ describe("session-compaction-checkpoints", () => {
test("async fork ignores legacy checkpoint locators that doctor has not imported", async () => {
const forked = await forkCompactionCheckpointTranscriptAsync({
sourceFile: path.join(os.tmpdir(), "openclaw-unimported-legacy-session.jsonl"),
sourceTranscriptLocator: path.join(os.tmpdir(), "openclaw-unimported-legacy-session.jsonl"),
});
expect(forked).toBeNull();

View File

@@ -3,7 +3,7 @@ import {
CURRENT_SESSION_VERSION,
migrateSessionEntries,
SessionManager,
type FileEntry as PiTranscriptLocatorEntry,
type FileEntry as PiTranscriptEntry,
type SessionHeader,
} from "../agents/transcript/session-transcript-contract.js";
import { patchSessionEntry } from "../config/sessions.js";
@@ -79,8 +79,8 @@ export function resolveSessionCompactionCheckpointReason(params: {
return "auto-threshold";
}
function cloneTranscriptEvents(events: unknown[]): PiTranscriptLocatorEntry[] | null {
const entries = events.filter((event): event is PiTranscriptLocatorEntry =>
function cloneTranscriptEvents(events: unknown[]): PiTranscriptEntry[] | null {
const entries = events.filter((event): event is PiTranscriptEntry =>
Boolean(event && typeof event === "object"),
);
const firstEntry = entries[0] as { type?: unknown; id?: unknown } | undefined;
@@ -94,7 +94,7 @@ function loadTranscriptEntriesFromSqlite(params: {
agentId?: string;
sessionId?: string;
transcriptLocator?: string;
}): PiTranscriptLocatorEntry[] | null {
}): PiTranscriptEntry[] | null {
let agentId = params.agentId?.trim() || DEFAULT_AGENT_ID;
let sessionId = params.sessionId?.trim();
if (!sessionId && params.transcriptLocator?.trim()) {
@@ -115,7 +115,7 @@ function loadTranscriptEntriesFromSqlite(params: {
);
}
function transcriptEventsByteLength(events: readonly PiTranscriptLocatorEntry[]): number {
function transcriptEventsByteLength(events: readonly PiTranscriptEntry[]): number {
let total = 0;
for (const event of events) {
total += Buffer.byteLength(`${JSON.stringify(event)}\n`, "utf8");
@@ -123,7 +123,7 @@ function transcriptEventsByteLength(events: readonly PiTranscriptLocatorEntry[])
return total;
}
function latestEntryId(entries: readonly PiTranscriptLocatorEntry[]): string | null {
function latestEntryId(entries: readonly PiTranscriptEntry[]): string | null {
for (let index = entries.length - 1; index >= 0; index -= 1) {
const entry = entries[index] as { type?: unknown; id?: unknown } | undefined;
if (entry?.type === "session") {
@@ -136,15 +136,17 @@ function latestEntryId(entries: readonly PiTranscriptLocatorEntry[]): string | n
return null;
}
function createCheckpointVirtualTranscriptPath(params: {
sourceFile?: string;
function createCheckpointVirtualTranscriptLocator(params: {
sourceTranscriptLocator?: string;
checkpointId: string;
}): string | undefined {
const sourceFile = params.sourceFile?.trim();
if (!sourceFile) {
const sourceTranscriptLocator = params.sourceTranscriptLocator?.trim();
if (!sourceTranscriptLocator) {
return undefined;
}
const scope = resolveSqliteSessionTranscriptScopeForLocator({ transcriptLocator: sourceFile });
const scope = resolveSqliteSessionTranscriptScopeForLocator({
transcriptLocator: sourceTranscriptLocator,
});
return createSqliteSessionTranscriptLocator({
agentId: scope?.agentId ?? DEFAULT_AGENT_ID,
sessionId: params.checkpointId,
@@ -163,16 +165,16 @@ export async function readSessionLeafIdFromTranscriptAsync(
}
export async function forkCompactionCheckpointTranscriptAsync(params: {
sourceFile?: string;
sourceTranscriptLocator?: string;
sourceSessionId?: string;
agentId?: string;
targetCwd?: string;
}): Promise<ForkedCompactionCheckpointTranscript | null> {
const sourceFile = params.sourceFile?.trim();
const sourceTranscriptLocator = params.sourceTranscriptLocator?.trim();
const entries = loadTranscriptEntriesFromSqlite({
agentId: params.agentId,
sessionId: params.sourceSessionId,
transcriptLocator: sourceFile,
transcriptLocator: sourceTranscriptLocator,
});
if (!entries) {
return null;
@@ -186,8 +188,8 @@ export async function forkCompactionCheckpointTranscriptAsync(params: {
const targetCwd = params.targetCwd ?? sourceHeader.cwd ?? process.cwd();
const sessionId = randomUUID();
const timestamp = new Date().toISOString();
const sourceScope = sourceFile
? resolveSqliteSessionTranscriptScopeForLocator({ transcriptLocator: sourceFile })
const sourceScope = sourceTranscriptLocator
? resolveSqliteSessionTranscriptScopeForLocator({ transcriptLocator: sourceTranscriptLocator })
: undefined;
const agentId = params.agentId?.trim() || sourceScope?.agentId || DEFAULT_AGENT_ID;
const transcriptLocator = createSqliteSessionTranscriptLocator({ agentId, sessionId });
@@ -197,7 +199,7 @@ export async function forkCompactionCheckpointTranscriptAsync(params: {
id: sessionId,
timestamp,
cwd: targetCwd,
...(sourceFile ? { parentSession: sourceFile } : {}),
...(sourceTranscriptLocator ? { parentSession: sourceTranscriptLocator } : {}),
};
try {
@@ -256,8 +258,8 @@ export async function captureCompactionCheckpointSnapshotAsync(params: {
return null;
}
const snapshotSessionId = randomUUID();
const snapshotFile = createCheckpointVirtualTranscriptPath({
sourceFile: transcriptLocator,
const snapshotTranscriptLocator = createCheckpointVirtualTranscriptLocator({
sourceTranscriptLocator: transcriptLocator,
checkpointId: snapshotSessionId,
});
const sourceScope = resolveSqliteSessionTranscriptScopeForLocator({
@@ -286,15 +288,15 @@ export async function captureCompactionCheckpointSnapshotAsync(params: {
eventCount: entries.length,
metadata: {
leafId,
sourceTranscriptPath: transcriptLocator,
...(snapshotFile ? { snapshotTranscriptPath: snapshotFile } : {}),
sourceTranscriptLocator: transcriptLocator,
...(snapshotTranscriptLocator ? { snapshotTranscriptLocator } : {}),
},
});
return {
agentId: snapshotAgentId,
sourceSessionId: sourceHeader.id,
sessionId: snapshotSessionId,
transcriptLocator: snapshotFile,
transcriptLocator: snapshotTranscriptLocator,
leafId,
};
}