import fsp from 'node:fs/promises'; import { sessionsDb } from '@/modules/database/index.js'; import { providerRegistry } from '@/modules/providers/provider.registry.js'; import type { FetchHistoryOptions, FetchHistoryResult, LLMProvider, NormalizedMessage, } from '@/shared/types.js'; import { AppError } from '@/shared/utils.js'; /** * Removes one file if it exists. */ async function removeFileIfExists(filePath: string): Promise { try { await fsp.unlink(filePath); return true; } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code === 'ENOENT') { return false; } throw error; } } /** * Application service for provider-backed session message operations. * * Callers pass a provider id and this service resolves the concrete provider * class, keeping normalization/history call sites decoupled from implementation * file layout. */ export const sessionsService = { /** * Lists provider ids that can load session history and normalize live messages. */ listProviderIds(): LLMProvider[] { return providerRegistry.listProviders().map((provider) => provider.id); }, /** * Normalizes one provider-native event into frontend session message events. */ normalizeMessage( providerName: string, raw: unknown, sessionId: string | null, ): NormalizedMessage[] { return providerRegistry.resolveProvider(providerName).sessions.normalizeMessage(raw, sessionId); }, /** * Fetches persisted history by session id. * * Provider and provider-specific lookup hints are resolved from the indexed * session metadata in the database. */ fetchHistory( sessionId: string, options: Pick = {}, ): Promise { const session = sessionsDb.getSessionById(sessionId); if (!session) { throw new AppError(`Session "${sessionId}" was not found.`, { code: 'SESSION_NOT_FOUND', statusCode: 404, }); } const provider = session.provider as LLMProvider; return providerRegistry.resolveProvider(provider).sessions.fetchHistory(sessionId, { limit: options.limit ?? null, offset: options.offset ?? 0, projectPath: session.project_path ?? '', }); }, /** * Deletes one persisted session row by id. * * When `deletedFromDisk` is true and a session `jsonl_path` exists, the path * is deleted from disk before the DB row is removed. */ async deleteSessionById( sessionId: string, deletedFromDisk = false, ): Promise<{ sessionId: string; deletedFromDisk: boolean }> { const session = sessionsDb.getSessionById(sessionId); if (!session) { throw new AppError(`Session "${sessionId}" was not found.`, { code: 'SESSION_NOT_FOUND', statusCode: 404, }); } let removedFromDisk = false; if (deletedFromDisk && session.jsonl_path) { removedFromDisk = await removeFileIfExists(session.jsonl_path); } const deleted = sessionsDb.deleteSessionById(sessionId); if (!deleted) { throw new AppError(`Session "${sessionId}" was not found.`, { code: 'SESSION_NOT_FOUND', statusCode: 404, }); } return { sessionId, deletedFromDisk: removedFromDisk }; }, /** * Renames one session by id without requiring the caller to pass provider. */ renameSessionById(sessionId: string, summary: string): { sessionId: string; summary: string } { const session = sessionsDb.getSessionById(sessionId); if (!session) { throw new AppError(`Session "${sessionId}" was not found.`, { code: 'SESSION_NOT_FOUND', statusCode: 404, }); } sessionsDb.updateSessionCustomName(sessionId, summary); return { sessionId, summary }; }, };