From 16d2d29edae54316eb76b5fc02e196b574ec8ca8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 08:58:25 +0100 Subject: [PATCH] refactor: rename checkpoint transcript locators --- docs/refactor/database-first.md | 4 +- src/gateway/server-methods/sessions.ts | 4 +- .../session-compaction-checkpoints.test.ts | 70 +++++++++---------- src/gateway/session-compaction-checkpoints.ts | 46 ++++++------ 4 files changed, 64 insertions(+), 60 deletions(-) diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index e39380019dc..43e57b6de18 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -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 diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 43861f66db4..d68dfdeceec 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -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) { diff --git a/src/gateway/session-compaction-checkpoints.test.ts b/src/gateway/session-compaction-checkpoints.test.ts index 62ee8c9194e..71269efd3cc 100644 --- a/src/gateway/session-compaction-checkpoints.test.ts +++ b/src/gateway/session-compaction-checkpoints.test.ts @@ -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> = 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> = 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(); diff --git a/src/gateway/session-compaction-checkpoints.ts b/src/gateway/session-compaction-checkpoints.ts index 3d3f0310824..92e9bae3fe5 100644 --- a/src/gateway/session-compaction-checkpoints.ts +++ b/src/gateway/session-compaction-checkpoints.ts @@ -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 { - 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, }; }