import { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore'; import { IFileSystemStorageService } from '#/persistence/interface/storage'; import { CHILD_SESSION_KIND, CHILD_SESSION_KIND_KEY, type SessionSummary } from './sessionIndex'; import { SESSION_INDEX_DIRTY_DIR, listDirtyMarks } from './sessionIndexDirtyJournal'; const META_SCOPE = 'session-meta'; const META_KEY = 'state.json'; export function parseTime(value: unknown): number { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const parsed = Date.parse(value); if (!Number.isNaN(parsed)) return parsed; } return 0; } export function parseTurnOutcome(value: unknown): 'completed' | 'cancelled' | 'failed' | undefined { return value === 'completed' || value === 'cancelled' || value === 'failed' ? value : undefined; } export function recoverCwd(meta: Record): string | undefined { if (typeof meta['cwd'] === 'string' && meta['cwd'].length > 0) return meta['cwd']; if (typeof meta['workDir'] === 'string' && meta['workDir'].length > 0) { return meta['workDir']; } const custom = meta['custom']; if (custom !== null && typeof custom === 'object' && !Array.isArray(custom)) { const fromCustom = (custom as Record)['cwd']; if (typeof fromCustom === 'string' && fromCustom.length > 0) return fromCustom; } return undefined; } export function buildSessionSummary(fields: { id: string; workspaceId: string; cwd?: string; title?: string; lastPrompt?: string; createdAt: number; updatedAt: number; archived: boolean; archivedAt?: number; custom?: Record; lastTurnReason?: 'completed' | 'cancelled' | 'failed'; }): SessionSummary { return { id: fields.id, workspaceId: fields.workspaceId, cwd: fields.cwd, title: fields.title, lastPrompt: fields.lastPrompt, createdAt: fields.createdAt, updatedAt: fields.updatedAt, archived: fields.archived, archivedAt: fields.archivedAt, custom: fields.custom, lastTurnReason: fields.lastTurnReason, }; } export function summaryMatchesChildOf( summary: SessionSummary, parentId: string | undefined, ): boolean { if (parentId === undefined) return true; const custom = summary.custom; return ( custom?.['parent_session_id'] === parentId && custom?.[CHILD_SESSION_KIND_KEY] === CHILD_SESSION_KIND ); } export function summaryEquals(a: SessionSummary, b: SessionSummary): boolean { return ( a.id === b.id && a.workspaceId === b.workspaceId && a.cwd === b.cwd && a.title === b.title && a.lastPrompt === b.lastPrompt && a.createdAt === b.createdAt && a.updatedAt === b.updatedAt && a.archived === b.archived && a.archivedAt === b.archivedAt && a.lastTurnReason === b.lastTurnReason && JSON.stringify(a.custom) === JSON.stringify(b.custom) ); } export async function listWorkspaceIds( storage: IFileSystemStorageService, sessionsScope: string, ): Promise { try { return (await storage.list(sessionsScope)).filter((entry) => entry !== SESSION_INDEX_DIRTY_DIR); } catch { return []; } } export async function listSessionIds( storage: IFileSystemStorageService, sessionsScope: string, workspaceId: string, ): Promise { try { return await storage.list(`${sessionsScope}/${workspaceId}`); } catch { return []; } } export async function readSessionSummary( docs: IAtomicDocumentStore, sessionsScope: string, workspaceId: string, sessionId: string, ): Promise { const base = `${sessionsScope}/${workspaceId}/${sessionId}`; const meta = (await readMeta(docs, base)) ?? (await readMeta(docs, `${base}/${META_SCOPE}`)); if (meta === undefined) return undefined; const rawCustom = meta['custom']; const custom = rawCustom !== null && typeof rawCustom === 'object' && !Array.isArray(rawCustom) ? (rawCustom as Record) : undefined; return buildSessionSummary({ id: sessionId, workspaceId, cwd: recoverCwd(meta), title: typeof meta['title'] === 'string' ? meta['title'] : undefined, lastPrompt: typeof meta['lastPrompt'] === 'string' ? meta['lastPrompt'] : undefined, createdAt: parseTime(meta['createdAt']), updatedAt: parseTime(meta['updatedAt']), archived: meta['archived'] === true, archivedAt: meta['archivedAt'] === undefined ? undefined : parseTime(meta['archivedAt']), custom, lastTurnReason: parseTurnOutcome(meta['lastTurnReason']), }); } async function readMeta( docs: IAtomicDocumentStore, scope: string, ): Promise | undefined> { try { return await docs.get>(scope, META_KEY); } catch { return undefined; } } export async function mapBounded( items: readonly T[], concurrency: number, fn: (item: T) => Promise, ): Promise { const out: R[] = []; let next = 0; const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => { while (next < items.length) { const item = items[next++]!; const value = await fn(item); if (value !== undefined) out.push(value); } }); await Promise.all(workers); return out; } export interface SessionsFreshness { readonly dirtyMarkCount: number; readonly sessionCount: number; } export async function scanSessionsFreshness( storage: IFileSystemStorageService, sessionsScope: string, ): Promise { const [marks, workspaceIds] = await Promise.all([ listDirtyMarks(storage, sessionsScope), listWorkspaceIds(storage, sessionsScope), ]); let sessionCount = 0; for (const workspaceId of workspaceIds) { sessionCount += (await listSessionIds(storage, sessionsScope, workspaceId)).length; } return { dirtyMarkCount: marks.length, sessionCount }; }