import * as fs from "fs";
import * as path from "path";
import {
getCheckpointsDir,
getContextStats,
SessionInfo,
ContextStats,
} from "./session.js";
export interface CheckpointMetadata {
name: string;
sessionId: string;
createdAt: string;
note?: string;
stats: {
userTurns: number;
assistantTurns: number;
estimatedTokens: number;
};
sessionFilePath: string;
cwd: string;
}
export interface Checkpoint {
metadata: CheckpointMetadata;
dataFile: string;
metadataFile: string;
}
/**
* Get checkpoint directory for a session
*/
function getSessionCheckpointDir(sessionId: string): string {
return path.join(getCheckpointsDir(), sessionId);
}
/**
* Create a checkpoint of the current session state
*/
export function createCheckpoint(
session: SessionInfo,
name: string,
note?: string
): Checkpoint {
const checkpointDir = getSessionCheckpointDir(session.sessionId);
// Ensure checkpoint directory exists
fs.mkdirSync(checkpointDir, { recursive: true });
const dataFile = path.join(checkpointDir, `${name}.jsonl`);
const metadataFile = path.join(checkpointDir, `${name}.meta.json`);
// Copy session file
fs.copyFileSync(session.sessionFilePath, dataFile);
// Get stats for metadata
const stats = getContextStats(session.sessionFilePath);
const metadata: CheckpointMetadata = {
name,
sessionId: session.sessionId,
createdAt: new Date().toISOString(),
note,
stats: {
userTurns: stats?.userTurns || 0,
assistantTurns: stats?.assistantTurns || 0,
estimatedTokens: stats?.estimatedTokens || 0,
},
sessionFilePath: session.sessionFilePath,
cwd: session.cwd,
};
// Write metadata
fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2));
return {
metadata,
dataFile,
metadataFile,
};
}
/**
* List all checkpoints for a session
*/
export function listCheckpoints(sessionId: string): Checkpoint[] {
const checkpointDir = getSessionCheckpointDir(sessionId);
if (!fs.existsSync(checkpointDir)) {
return [];
}
const files = fs.readdirSync(checkpointDir);
const metaFiles = files.filter((f) => f.endsWith(".meta.json"));
const checkpoints: Checkpoint[] = [];
for (const metaFile of metaFiles) {
const metaPath = path.join(checkpointDir, metaFile);
const name = metaFile.replace(".meta.json", "");
const dataFile = path.join(checkpointDir, `${name}.jsonl`);
if (!fs.existsSync(dataFile)) continue;
try {
const metadata = JSON.parse(
fs.readFileSync(metaPath, "utf-8")
) as CheckpointMetadata;
checkpoints.push({
metadata,
dataFile,
metadataFile: metaPath,
});
} catch {
// Skip malformed metadata
}
}
// Sort by creation time, newest first
checkpoints.sort(
(a, b) =>
new Date(b.metadata.createdAt).getTime() -
new Date(a.metadata.createdAt).getTime()
);
return checkpoints;
}
/**
* Get a specific checkpoint
*/
export function getCheckpoint(
sessionId: string,
name: string
): Checkpoint | null {
const checkpointDir = getSessionCheckpointDir(sessionId);
const dataFile = path.join(checkpointDir, `${name}.jsonl`);
const metadataFile = path.join(checkpointDir, `${name}.meta.json`);
if (!fs.existsSync(dataFile) || !fs.existsSync(metadataFile)) {
return null;
}
try {
const metadata = JSON.parse(
fs.readFileSync(metadataFile, "utf-8")
) as CheckpointMetadata;
return {
metadata,
dataFile,
metadataFile,
};
} catch {
return null;
}
}
/**
* Create a handoff message to inject into restored session
*/
function createHandoffMessage(
sessionId: string,
content: string,
cwd: string
): object {
return {
type: "user",
message: { role: "user", content },
uuid: `handoff-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
timestamp: new Date().toISOString(),
userType: "external",
cwd,
sessionId,
};
}
/**
* Restore a checkpoint, optionally injecting a handoff message
*/
export function restoreCheckpoint(
checkpoint: Checkpoint,
messageToSelf?: string
): { success: boolean; sessionFilePath: string } {
const { metadata, dataFile } = checkpoint;
// Copy checkpoint data back to session file
fs.copyFileSync(dataFile, metadata.sessionFilePath);
// Inject handoff message if provided
if (messageToSelf) {
const handoff = createHandoffMessage(
metadata.sessionId,
messageToSelf,
metadata.cwd
);
fs.appendFileSync(
metadata.sessionFilePath,
"\n" + JSON.stringify(handoff)
);
}
return {
success: true,
sessionFilePath: metadata.sessionFilePath,
};
}
/**
* Delete a checkpoint
*/
export function deleteCheckpoint(sessionId: string, name: string): boolean {
const checkpoint = getCheckpoint(sessionId, name);
if (!checkpoint) return false;
try {
fs.unlinkSync(checkpoint.dataFile);
fs.unlinkSync(checkpoint.metadataFile);
return true;
} catch {
return false;
}
}