/**
* Session Persistence — save/load conversations to disk
*
* Extracted from agent-loop.ts for single-responsibility.
* All consumers should import from agent-loop.ts (re-export facade).
*/
import type Anthropic from "@anthropic-ai/sdk";
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, appendFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { getModel } from "./model-manager.js";
const SESSIONS_DIR = join(homedir(), ".swagmanager", "sessions");
function ensureSessionsDir(): void {
if (!existsSync(SESSIONS_DIR)) mkdirSync(SESSIONS_DIR, { recursive: true });
}
export interface SessionMeta {
id: string;
title: string;
model: string;
messageCount: number;
createdAt: string;
updatedAt: string;
cwd?: string;
}
export function saveSession(
messages: Anthropic.MessageParam[],
sessionId?: string
): string {
ensureSessionsDir();
const id = sessionId || `session-${Date.now()}`;
const meta: SessionMeta = {
id,
title: extractSessionTitle(messages),
model: getModel(),
messageCount: messages.length,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
cwd: process.cwd(),
};
const data = JSON.stringify({ meta, messages }, null, 2);
writeFileSync(join(SESSIONS_DIR, `${id}.json`), data, "utf-8");
logSessionHistory(meta);
return id;
}
export function loadSession(sessionId: string): { meta: SessionMeta; messages: Anthropic.MessageParam[] } | null {
const path = join(SESSIONS_DIR, `${sessionId}.json`);
if (!existsSync(path)) return null;
try {
return JSON.parse(readFileSync(path, "utf-8"));
} catch { return null; }
}
export function listSessions(limit = 20): SessionMeta[] {
ensureSessionsDir();
const files = readdirSync(SESSIONS_DIR)
.filter((f) => f.endsWith(".json"))
.sort()
.reverse()
.slice(0, limit);
const sessions: SessionMeta[] = [];
for (const f of files) {
try {
const data = JSON.parse(readFileSync(join(SESSIONS_DIR, f), "utf-8"));
if (data.meta) sessions.push(data.meta);
} catch { /* skip corrupted */ }
}
return sessions;
}
const HISTORY_FILE = join(homedir(), ".swagmanager", "history.jsonl");
function logSessionHistory(meta: SessionMeta): void {
try {
const dir = join(homedir(), ".swagmanager");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
const entry = {
display: meta.title,
project: meta.cwd || process.cwd(),
timestamp: meta.updatedAt,
sessionId: meta.id,
model: meta.model,
};
appendFileSync(HISTORY_FILE, JSON.stringify(entry) + "\n");
} catch { /* best effort */ }
}
export function findLatestSessionForCwd(): SessionMeta | null {
const cwd = process.cwd();
const sessions = listSessions(100);
return sessions.find(s => s.cwd === cwd) || null;
}
function extractSessionTitle(messages: Anthropic.MessageParam[]): string {
// Use first user message as title (truncated)
for (const m of messages) {
if (m.role === "user" && typeof m.content === "string") {
return m.content.slice(0, 60) + (m.content.length > 60 ? "..." : "");
}
if (m.role === "user" && Array.isArray(m.content)) {
for (const block of m.content) {
if ("text" in block && typeof block.text === "string") {
return block.text.slice(0, 60) + (block.text.length > 60 ? "..." : "");
}
}
}
}
return "Untitled session";
}