mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
refactor: rename checkpoint transcript locators
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user