import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { ensureRunsDir, readCurrentSession, writeCurrentSession, clearCurrentSession, updateIndexEntry, findInProgressSessions, readIndex, type IndexEntry, } from './runs-dir.js'; export interface SessionJson { session_id: string; task: string; start_time: string; end_time?: string; status: 'in_progress' | 'completed'; expected_record_types?: string[]; total_records?: number; } export function generateSessionId(): string { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const h = String(now.getHours()).padStart(2, '0'); const min = String(now.getMinutes()).padStart(2, '0'); return `adhoc_${y}${m}${d}_${h}${min}`; } export function isoNow(): string { return new Date().toISOString(); } export function createSession( task: string, sessionId?: string, projectRoot?: string, ): string { const sid = sessionId || generateSessionId(); const runsDir = ensureRunsDir(projectRoot); const sessionDir = join(runsDir, sid); mkdirSync(sessionDir, { recursive: true }); const session: SessionJson = { session_id: sid, task, start_time: isoNow(), status: 'in_progress', expected_record_types: ['data', 'change', 'conversation'], }; writeFileSync(join(sessionDir, 'session.json'), JSON.stringify(session, null, 2), 'utf-8'); writeCurrentSession(sid, projectRoot); updateIndexEntry({ id: sid, type: 'adhoc', status: 'in_progress', task, created: session.start_time, last_updated: session.start_time, }, projectRoot); return sid; } export function getSessionDir(sessionId: string, projectRoot?: string): string { return join(ensureRunsDir(projectRoot), sessionId); } export function getActiveSession(projectRoot?: string): SessionJson | null { const sid = readCurrentSession(projectRoot); if (!sid) return null; return readSession(sid, projectRoot); } export function readSession(sessionId: string, projectRoot?: string): SessionJson | null { const path = join(getSessionDir(sessionId, projectRoot), 'session.json'); try { return JSON.parse(readFileSync(path, 'utf-8')) as SessionJson; } catch { return null; } } export function updateSession( sessionId: string, updates: Partial, projectRoot?: string, ): void { const session = readSession(sessionId, projectRoot) || { session_id: sessionId, task: '', start_time: isoNow(), status: 'in_progress' as const }; Object.assign(session, updates); writeFileSync( join(getSessionDir(sessionId, projectRoot), 'session.json'), JSON.stringify(session, null, 2), 'utf-8', ); } export function endSession(sessionId: string, projectRoot?: string): void { updateSession(sessionId, { status: 'completed', end_time: isoNow() }, projectRoot); updateIndexEntry({ id: sessionId, type: 'adhoc', status: 'completed', last_updated: isoNow() }, projectRoot); clearCurrentSession(projectRoot); } export function appendToTrail(sessionId: string, records: Record[], projectRoot?: string): void { const trailPath = join(getSessionDir(sessionId, projectRoot), 'audit_trail.jsonl'); const lines = records.map(r => JSON.stringify(r)).join('\n') + '\n'; appendFileSync(trailPath, lines, 'utf-8'); } export function readTrail(sessionId: string, projectRoot?: string): Record[] { const trailPath = join(getSessionDir(sessionId, projectRoot), 'audit_trail.jsonl'); try { const content = readFileSync(trailPath, 'utf-8').trim(); if (!content) return []; return content.split('\n').filter(Boolean).map(line => JSON.parse(line) as Record); } catch { return []; } } export interface RecoverySummary { sessionId: string; task: string; recentActivity: IndexEntry[]; userCorrections: Record[]; unresolvedIssues: string[]; recommendedPriorities: string[]; level: number; } export function recoverContext( sessionId: string, level: number, projectRoot?: string, ): RecoverySummary { const session = readSession(sessionId, projectRoot); const idx = readIndex(projectRoot); const recentActivity = idx.entries.slice(-5); const trail = readTrail(sessionId, projectRoot); const userCorrections = trail.filter(r => r['action_type'] === 'user_correction'); const summary: RecoverySummary = { sessionId, task: session?.task || '(unknown)', recentActivity, userCorrections, unresolvedIssues: [], recommendedPriorities: [], level, }; if (level >= 1) { const last = trail.slice(-3); if (last.length > 0) { summary.recommendedPriorities.push(`Last action: ${JSON.stringify(last[last.length - 1]?.['action'] || 'none')}`); } } if (level >= 3) { summary.recommendedPriorities.push(`Full trail: ${trail.length} records`); } let checkCount = 0; for (const entry of recentActivity) { if (entry.status === 'in_progress' && entry.id !== sessionId) { summary.unresolvedIssues.push(`Unfinished session: ${entry.id} (${entry.task || 'no task'})`); checkCount++; if (checkCount >= 3) break; } } return summary; } export function checkUnfinishedSessions(projectRoot?: string): IndexEntry[] { return findInProgressSessions(projectRoot); } export function generateSessionSummary(sessionId: string, projectRoot?: string): string { const session = readSession(sessionId, projectRoot); const trail = readTrail(sessionId, projectRoot); const corrections = trail.filter(r => r['action_type'] === 'user_correction'); const changes = trail.filter(r => r['action'] === 'edit_file' || r['action'] === 'create_file' || r['action'] === 'delete_file'); const lines: string[] = [ `# Session Summary | ${sessionId}`, '', `## Task: ${session?.task || '(unknown)'}`, `## Time: ${session?.start_time || '?'} → ${session?.end_time || 'in_progress'}`, `## Status: ${session?.status || 'unknown'}`, '', '## Completed Work', ]; for (const r of trail) { if (r['action']) { lines.push(`- ${r['action']}: ${r['detail'] || r['reason'] || '(no detail)'}`); } } if (corrections.length > 0) { lines.push('', '## User Corrections'); for (const c of corrections) { lines.push(`- Original: ${c['original_claim']}`); lines.push(` Correction: ${c['correction']}`); if (c['principle_extracted']) { lines.push(` Principle: ${c['principle_extracted']}`); } } } if (changes.length > 0) { lines.push('', '## Files Changed'); const fileSet = new Set(); for (const c of changes) { const files = c['files']; if (Array.isArray(files)) { for (const f of files) fileSet.add(String(f)); } } for (const f of fileSet) lines.push(`- ${f}`); } lines.push('', '## Stats'); lines.push(`- Total records: ${trail.length}`); lines.push(`- Corrections: ${corrections.length}`); lines.push(`- File changes: ${changes.length}`); return lines.join('\n'); }