341 lines · 11.3 KB · TypeScript
RawDownload
/** * HTTP route handlers for Session Operations. * * Routes: * - GET /api/projects/:projectId/sessions - List sessions * - GET /api/projects/:projectId/sessions-paginated - Paginated sessions * - GET /api/projects/:projectId/sessions/:sessionId - Full session detail * - GET /api/projects/:projectId/sessions/:sessionId/groups - Conversation groups * - GET /api/projects/:projectId/sessions/:sessionId/metrics - Session metrics * - GET /api/projects/:projectId/sessions/:sessionId/waterfall - Waterfall data */ import { createLogger } from '@shared/utils/logger'; import { coercePageLimit, validateProjectId, validateSessionId } from '../ipc/guards';import { DataCache } from '../services'; import type { SessionsByIdsOptions, SessionsPaginationOptions } from '../types';import type { HttpServices } from './index';import type { FastifyInstance } from 'fastify'; const logger = createLogger('HTTP:sessions'); export function registerSessionRoutes(app: FastifyInstance, services: HttpServices): void {  // List sessions  app.get<{ Params: { projectId: string } }>(    '/api/projects/:projectId/sessions',    async (request) => {      try {        const validated = validateProjectId(request.params.projectId);        if (!validated.valid) {          logger.error(`GET sessions rejected: ${validated.error ?? 'unknown'}`);          return [];        }         const sessions = await services.projectScanner.listSessions(validated.value!);        return sessions;      } catch (error) {        logger.error(`Error in GET sessions for ${request.params.projectId}:`, error);        return [];      }    }  );   // Paginated sessions  app.get<{    Params: { projectId: string };    Querystring: {      cursor?: string;      limit?: string;      includeTotalCount?: string;      prefilterAll?: string;      metadataLevel?: 'light' | 'deep';    };  }>('/api/projects/:projectId/sessions-paginated', async (request) => {    try {      const validated = validateProjectId(request.params.projectId);      if (!validated.valid) {        logger.error(`GET sessions-paginated rejected: ${validated.error ?? 'unknown'}`);        return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };      }       const cursor = request.query.cursor || null;      const limit = coercePageLimit(        request.query.limit ? Number(request.query.limit) : undefined,        20      );      const options: SessionsPaginationOptions = {        includeTotalCount: request.query.includeTotalCount !== 'false',        prefilterAll: request.query.prefilterAll !== 'false',        metadataLevel: request.query.metadataLevel,      };       const result = await services.projectScanner.listSessionsPaginated(        validated.value!,        cursor,        limit,        options      );      return result;    } catch (error) {      logger.error(`Error in GET sessions-paginated for ${request.params.projectId}:`, error);      return { sessions: [], nextCursor: null, hasMore: false, totalCount: 0 };    }  });   // Fetch sessions by IDs (for pinned sessions beyond paginated page)  app.post<{ Params: { projectId: string } }>(    '/api/projects/:projectId/sessions-by-ids',    async (request) => {      try {        const validated = validateProjectId(request.params.projectId);        if (!validated.valid) {          logger.error(`POST sessions-by-ids rejected: ${validated.error ?? 'unknown'}`);          return [];        }         const { sessionIds } = request.body as { sessionIds?: string[] };        if (!Array.isArray(sessionIds)) {          logger.error('POST sessions-by-ids rejected: sessionIds must be an array');          return [];        }        const { metadataLevel } = request.body as SessionsByIdsOptions;         // Cap at 50 IDs        const capped = sessionIds.slice(0, 50);         // Validate each session ID        const validIds: string[] = [];        for (const id of capped) {          const result = validateSessionId(id);          if (result.valid) {            validIds.push(result.value!);          }        }         if (validIds.length === 0) {          return [];        }         const fsType = services.projectScanner.getFileSystemProvider().type;        const effectiveMetadataLevel = metadataLevel ?? (fsType === 'ssh' ? 'light' : 'deep');        const results = await Promise.all(          validIds.map((id) =>            services.projectScanner.getSessionWithOptions(validated.value!, id, {              metadataLevel: effectiveMetadataLevel,            })          )        );         return results.filter((s): s is NonNullable<typeof s> => s !== null);      } catch (error) {        logger.error(`Error in POST sessions-by-ids for ${request.params.projectId}:`, error);        return [];      }    }  );   // Session detail  app.get<{ Params: { projectId: string; sessionId: string } }>(    '/api/projects/:projectId/sessions/:sessionId',    async (request) => {      try {        const validatedProject = validateProjectId(request.params.projectId);        const validatedSession = validateSessionId(request.params.sessionId);        if (!validatedProject.valid || !validatedSession.valid) {          logger.error(            `GET session-detail rejected: ${validatedProject.error ?? validatedSession.error ?? 'unknown'}`          );          return null;        }         const safeProjectId = validatedProject.value!;        const safeSessionId = validatedSession.value!;        const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);         // Check cache first        let sessionDetail = services.dataCache.get(cacheKey);        if (sessionDetail) {          return sessionDetail;        }         const fsType = services.projectScanner.getFileSystemProvider().type;        // In SSH mode, avoid an extra deep metadata scan before full parse.        const session = await services.projectScanner.getSessionWithOptions(          safeProjectId,          safeSessionId,          {            metadataLevel: fsType === 'ssh' ? 'light' : 'deep',          }        );        if (!session) {          logger.error(`Session not found: ${safeSessionId}`);          return null;        }         // Parse session messages        const parsedSession = await services.sessionParser.parseSession(          safeProjectId,          safeSessionId        );         // Resolve subagents        const subagents = await services.subagentResolver.resolveSubagents(          safeProjectId,          safeSessionId,          parsedSession.taskCalls,          parsedSession.messages        );        session.hasSubagents = subagents.length > 0;         // Build session detail with chunks        sessionDetail = services.chunkBuilder.buildSessionDetail(          session,          parsedSession.messages,          subagents        );         // Cache the result        services.dataCache.set(cacheKey, sessionDetail);         return sessionDetail;      } catch (error) {        logger.error(          `Error in GET session-detail for ${request.params.projectId}/${request.params.sessionId}:`,          error        );        return null;      }    }  );   // Conversation groups  app.get<{ Params: { projectId: string; sessionId: string } }>(    '/api/projects/:projectId/sessions/:sessionId/groups',    async (request) => {      try {        const validatedProject = validateProjectId(request.params.projectId);        const validatedSession = validateSessionId(request.params.sessionId);        if (!validatedProject.valid || !validatedSession.valid) {          logger.error(            `GET session-groups rejected: ${validatedProject.error ?? validatedSession.error ?? 'unknown'}`          );          return [];        }         const safeProjectId = validatedProject.value!;        const safeSessionId = validatedSession.value!;         const parsedSession = await services.sessionParser.parseSession(          safeProjectId,          safeSessionId        );         const subagents = await services.subagentResolver.resolveSubagents(          safeProjectId,          safeSessionId,          parsedSession.taskCalls,          parsedSession.messages        );         const groups = services.chunkBuilder.buildGroups(parsedSession.messages, subagents);        return groups;      } catch (error) {        logger.error(          `Error in GET session-groups for ${request.params.projectId}/${request.params.sessionId}:`,          error        );        return [];      }    }  );   // Session metrics  app.get<{ Params: { projectId: string; sessionId: string } }>(    '/api/projects/:projectId/sessions/:sessionId/metrics',    async (request) => {      try {        const validatedProject = validateProjectId(request.params.projectId);        const validatedSession = validateSessionId(request.params.sessionId);        if (!validatedProject.valid || !validatedSession.valid) {          return null;        }         const safeProjectId = validatedProject.value!;        const safeSessionId = validatedSession.value!;         // Try cache first        const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);        const cached = services.dataCache.get(cacheKey);        if (cached) {          return cached.metrics;        }         const parsedSession = await services.sessionParser.parseSession(          safeProjectId,          safeSessionId        );        return parsedSession.metrics;      } catch (error) {        logger.error(          `Error in GET session-metrics for ${request.params.projectId}/${request.params.sessionId}:`,          error        );        return null;      }    }  );   // Waterfall data  app.get<{ Params: { projectId: string; sessionId: string } }>(    '/api/projects/:projectId/sessions/:sessionId/waterfall',    async (request) => {      try {        const validatedProject = validateProjectId(request.params.projectId);        const validatedSession = validateSessionId(request.params.sessionId);        if (!validatedProject.valid || !validatedSession.valid) {          return null;        }         const safeProjectId = validatedProject.value!;        const safeSessionId = validatedSession.value!;        const cacheKey = DataCache.buildKey(safeProjectId, safeSessionId);         // Try cache first for session detail        let detail = services.dataCache.get(cacheKey);         if (!detail) {          const session = await services.projectScanner.getSession(safeProjectId, safeSessionId);          if (!session) return null;           const parsedSession = await services.sessionParser.parseSession(            safeProjectId,            safeSessionId          );          const subagents = await services.subagentResolver.resolveSubagents(            safeProjectId,            safeSessionId,            parsedSession.taskCalls,            parsedSession.messages          );           detail = services.chunkBuilder.buildSessionDetail(            session,            parsedSession.messages,            subagents          );          services.dataCache.set(cacheKey, detail);        }         return services.chunkBuilder.buildWaterfallData(detail.chunks, detail.processes);      } catch (error) {        logger.error(          `Error in GET waterfall for ${request.params.projectId}/${request.params.sessionId}:`,          error        );        return null;      }    }  );}