export type ProjectSessionMetadata = { sessionId: string; agentId: string; archivedAt: string | null; unreadCount: number; createdAt: string; updatedAt: string; }; export type ProjectConversationState = { schemaVersion: 1; sessions: ProjectSessionMetadata[]; updatedAt: string; }; function cleanString(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } function normalizeSession(value: unknown): ProjectSessionMetadata | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const record = value as Partial; const sessionId = cleanString(record.sessionId); const agentId = cleanString(record.agentId); if (!sessionId || !/^[a-z0-9][a-z0-9_-]{0,127}$/i.test(sessionId) || !agentId) return null; const now = new Date().toISOString(); const createdAt = cleanString(record.createdAt) || now; const updatedAt = cleanString(record.updatedAt) || createdAt; const unreadCount = typeof record.unreadCount === 'number' && Number.isFinite(record.unreadCount) ? Math.max(0, Math.floor(record.unreadCount)) : 0; return { sessionId, agentId, archivedAt: cleanString(record.archivedAt) || null, unreadCount, createdAt, updatedAt, }; } export function createProjectConversationState(now = new Date().toISOString()): ProjectConversationState { return { schemaVersion: 1, sessions: [], updatedAt: now, }; } export function normalizeProjectConversationState(value: unknown): ProjectConversationState { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('Project conversation state must be an object'); } const record = value as Partial; if (record.schemaVersion !== 1) throw new Error('Unsupported project conversation state schema'); const sessions = Array.isArray(record.sessions) ? record.sessions.map(normalizeSession).filter((item): item is ProjectSessionMetadata => Boolean(item)) : []; const deduped = new Map(); for (const session of sessions) deduped.set(session.sessionId, session); return { schemaVersion: 1, sessions: [...deduped.values()], updatedAt: cleanString(record.updatedAt) || new Date().toISOString(), }; } export function upsertProjectSessionMetadata( state: ProjectConversationState, sessionId: string, agentId: string, now = new Date().toISOString(), ): ProjectConversationState { const current = state.sessions.find((item) => item.sessionId === sessionId); const next: ProjectSessionMetadata = { sessionId, agentId, archivedAt: current?.archivedAt ?? null, unreadCount: current?.unreadCount ?? 0, createdAt: current?.createdAt ?? now, updatedAt: now, }; return { schemaVersion: 1, sessions: [next, ...state.sessions.filter((item) => item.sessionId !== sessionId)], updatedAt: now, }; } export function patchProjectSessionMetadata( state: ProjectConversationState, sessionId: string, patch: Partial>, now = new Date().toISOString(), ): ProjectConversationState { const sessions = state.sessions.map((item) => item.sessionId === sessionId ? { ...item, ...(patch.agentId ? { agentId: patch.agentId } : {}), ...(patch.archivedAt !== undefined ? { archivedAt: patch.archivedAt } : {}), ...(patch.unreadCount !== undefined ? { unreadCount: Math.max(0, Math.floor(patch.unreadCount)) } : {}), updatedAt: now, } : item); return { schemaVersion: 1, sessions, updatedAt: now }; } export function removeProjectSessionMetadata( state: ProjectConversationState, sessionId: string, now = new Date().toISOString(), ): ProjectConversationState { return { schemaVersion: 1, sessions: state.sessions.filter((item) => item.sessionId !== sessionId), updatedAt: now, }; } export function completeProjectSession( state: ProjectConversationState, sessionId: string, incrementUnread: boolean, now = new Date().toISOString(), ): ProjectConversationState { const current = state.sessions.find((item) => item.sessionId === sessionId); if (!current) return state; return patchProjectSessionMetadata( state, sessionId, { unreadCount: current.unreadCount + (incrementUnread ? 1 : 0) }, now, ); }