Skip to main content
Glama
session.tsβ€’20.5 kB
import { Router, Request, Response } from 'express'; import { MemAgent } from '@core/brain/memAgent/index.js'; import { successResponse, errorResponse, ERROR_CODES } from '../utils/response.js'; import { validateSessionId, validateCreateSession, validateListParams, } from '../middleware/validation.js'; import { logger } from '@core/logger/index.js'; export function createSessionRoutes(agent: MemAgent): Router { const router = Router(); /** * GET /api/sessions * List all active sessions with metadata - OPTIMIZED for performance */ router.get('/', validateListParams, async (req: Request, res: Response) => { const startTime = Date.now(); try { logger.info('Listing sessions', { requestId: req.requestId }); // PERFORMANCE OPTIMIZATION: Get session IDs with optimized batch processing let sessionIds: string[] = []; try { sessionIds = await agent.listSessions(); logger.debug( `Agent returned ${sessionIds.length} session IDs in ${Date.now() - startTime}ms` ); } catch (listError) { logger.error('Failed to get session IDs from agent:', listError); // Continue with empty list rather than failing completely sessionIds = []; } if (sessionIds.length === 0) { logger.debug('No sessions found, returning empty list'); successResponse( res, { sessions: [], count: 0, currentSession: agent.getCurrentSessionId?.() || null, processingTime: Date.now() - startTime, }, 200, req.requestId ); return; } // PERFORMANCE OPTIMIZATION: Use batch processing for session metadata const sessionManager = agent.sessionManager; let sessionsMetadata: Map<string, any>; try { // Use the new batch processing method for optimal performance sessionsMetadata = await sessionManager.getBatchSessionMetadata(sessionIds); logger.debug(`Batch loaded ${sessionsMetadata.size} session metadata entries`); } catch (batchError) { logger.warn( 'Batch metadata loading failed, falling back to individual processing:', batchError ); // Fallback to individual processing if batch fails sessionsMetadata = new Map(); for (const sessionId of sessionIds.slice(0, 50)) { // Limit to prevent overload try { const metadata = await agent.getSessionMetadata(sessionId); if (metadata) { sessionsMetadata.set(sessionId, metadata); } } catch (error) { logger.debug(`Failed to get metadata for session ${sessionId}:`, error); } } } // CRITICAL FIX: Filter out sessions with 0 messages to prevent phantom sessions const validSessions = Array.from(sessionsMetadata.values()).filter(session => { return session.messageCount > 0; }); const processingTime = Date.now() - startTime; logger.info( `Successfully listed ${validSessions.length}/${Array.from(sessionsMetadata.values()).length} valid sessions in ${processingTime}ms` ); successResponse( res, { sessions: validSessions, count: validSessions.length, currentSession: agent.getCurrentSessionId?.() || null, processingTime, }, 200, req.requestId ); return; } catch (error) { // CRITICAL FIX: Properly extract error message to prevent [object Object] display let errorMsg: string; try { errorMsg = error instanceof Error ? error.message : String(error); } catch { errorMsg = 'Unknown error occurred during session listing'; } logger.error('Error listing sessions:', error); errorResponse( res, ERROR_CODES.INTERNAL_ERROR, 'Failed to list sessions', 500, { details: errorMsg, processingTime: Date.now() - startTime }, req.requestId ); } }); /** * POST /api/sessions * Create a new session */ /** * Validate and sanitize session ID to prevent problematic IDs */ function sanitizeSessionId(sessionId: string | undefined): string | null { if (!sessionId) return null; // Auto-generate // Remove problematic patterns and characters const sanitized = sessionId .trim() .replace(/[^\w-]/g, '-') // Replace non-alphanumeric with hyphens .replace(/^empty-?/i, '') // Remove "empty" prefix .replace(/^null-?/i, '') // Remove "null" prefix .replace(/^undefined-?/i, '') // Remove "undefined" prefix .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens .replace(/-+/g, '-') // Collapse multiple hyphens .substring(0, 64); // Limit length // If sanitization results in empty string, return null for auto-generation if (!sanitized || sanitized.length < 3) return null; return sanitized; } router.post('/', validateCreateSession, async (req: Request, res: Response) => { try { const { sessionId: rawSessionId } = req.body; const sessionId = sanitizeSessionId(rawSessionId); logger.info('Creating new session', { requestId: req.requestId, originalSessionId: rawSessionId, sessionId: sessionId || 'auto-generated', }); const session = await agent.createSession(sessionId || undefined); successResponse( res, { session: { id: session.id, // Only include serializable session properties createdAt: new Date().toISOString(), }, created: true, timestamp: new Date().toISOString(), }, 201, req.requestId ); } catch (error) { // Safely extract error message to avoid circular reference issues let errorMsg: string; try { errorMsg = error instanceof Error ? error.message : String(error); } catch { errorMsg = 'Unknown error occurred during session creation'; } logger.error('Failed to create session', { requestId: req.requestId, error: errorMsg, }); // Check if error is due to duplicate session ID if (errorMsg.includes('already exists') || errorMsg.includes('duplicate')) { errorResponse( res, ERROR_CODES.BAD_REQUEST, `Session creation failed: ${errorMsg}`, 400, undefined, req.requestId ); } else { errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to create session: ${errorMsg}`, 500, undefined, req.requestId ); } } }); /** * GET /api/sessions/current * Get current working session */ router.get('/current', async (req: Request, res: Response) => { try { const currentSessionId = agent.getCurrentSessionId(); const metadata = await agent.getSessionMetadata(currentSessionId); if (!metadata) { errorResponse( res, ERROR_CODES.SESSION_NOT_FOUND, 'Current session not found', 404, undefined, req.requestId ); return; } successResponse( res, { sessionId: currentSessionId, metadata, isCurrent: true, }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error('Failed to get current session', { requestId: req.requestId, error: errorMsg, }); errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to get current session: ${errorMsg}`, 500, undefined, req.requestId ); } }); /** * GET /api/sessions/:sessionId * Get session details */ router.get('/:sessionId', validateSessionId, async (req: Request, res: Response) => { try { const { sessionId } = req.params; if (!sessionId) { errorResponse( res, ERROR_CODES.BAD_REQUEST, 'Session ID is required', 400, undefined, req.requestId ); return; } logger.info('Getting session details', { requestId: req.requestId, sessionId, }); const session = await agent.getSession(sessionId); if (!session) { errorResponse( res, ERROR_CODES.SESSION_NOT_FOUND, `Session ${sessionId} not found`, 404, undefined, req.requestId ); return; } const metadata = await agent.getSessionMetadata(sessionId); successResponse( res, { sessionId, metadata, isCurrent: sessionId === agent.getCurrentSessionId(), }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error('Failed to get session details', { requestId: req.requestId, sessionId: req.params.sessionId, error: errorMsg, }); errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to get session details: ${errorMsg}`, 500, undefined, req.requestId ); } }); /** * POST /api/sessions/:sessionId/load * Load session as current */ router.post('/:sessionId/load', validateSessionId, async (req: Request, res: Response) => { try { const { sessionId: rawSessionId } = req.params; // Validate and sanitize session ID const sessionId = sanitizeSessionId(rawSessionId); if (!sessionId) { logger.warn('Attempted to load session with invalid ID:', rawSessionId); errorResponse( res, ERROR_CODES.BAD_REQUEST, 'Invalid session ID provided', 400, undefined, req.requestId ); return; } logger.info('Loading session', { requestId: req.requestId, sessionId, }); // CRITICAL FIX: Handle non-existent sessions gracefully let session; try { session = await agent.loadSession(sessionId); } catch (loadError) { const errorMsg = loadError instanceof Error ? loadError.message : String(loadError); logger.warn(`Session ${sessionId} not found, creating new session:`, { originalSessionId: rawSessionId, sanitizedSessionId: sessionId, error: errorMsg, requestId: req.requestId, }); // Create new session with the requested ID try { session = await agent.createSession(sessionId); logger.info(`Created new session: ${sessionId}`); } catch (createError) { const createErrorMsg = createError instanceof Error ? createError.message : String(createError); logger.error(`Failed to create session ${sessionId}:`, createErrorMsg); // If we can't create with the specific ID, create with auto-generated ID session = await agent.createSession(); logger.info(`Created auto-generated session: ${session.id}`); } } // CRITICAL FIX: Ensure conversation history is available in the loaded session // This is essential for UI mode to display previous messages when switching sessions let conversationHistory: any[] = []; try { // Force refresh conversation history after loading if (session && typeof session.refreshConversationHistory === 'function') { await session.refreshConversationHistory(); logger.debug(`Session ${sessionId}: Refreshed conversation history after loading`); } // Get the conversation history to return with the response if (session && typeof session.getConversationHistory === 'function') { conversationHistory = await session.getConversationHistory(); logger.info( `Session ${sessionId}: Loaded with ${conversationHistory.length} messages in conversation history` ); } } catch (historyError) { logger.warn(`Session ${sessionId}: Failed to refresh history after loading:`, historyError); // Continue even if history refresh fails } successResponse( res, { sessionId: session.id, loaded: true, currentSession: agent.getCurrentSessionId(), conversationHistory, // CRITICAL FIX: Return history with session load timestamp: new Date().toISOString(), }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error('Failed to load session', { requestId: req.requestId, sessionId: req.params.sessionId, error: errorMsg, }); if (errorMsg.includes('not found')) { errorResponse( res, ERROR_CODES.SESSION_NOT_FOUND, `Session ${req.params.sessionId} not found`, 404, undefined, req.requestId ); } else { errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to load session: ${errorMsg}`, 500, undefined, req.requestId ); } } }); /** * GET /api/sessions/:sessionId/history * Get session conversation history - OPTIMIZED with caching */ router.get('/:sessionId/history', validateSessionId, async (req: Request, res: Response) => { const startTime = Date.now(); try { const { sessionId } = req.params; // Handle null or invalid session IDs if (!sessionId || sessionId === 'null' || sessionId === 'undefined') { logger.warn('Attempted to get history for session with invalid ID:', sessionId); errorResponse( res, ERROR_CODES.BAD_REQUEST, 'Invalid session ID provided', 400, undefined, req.requestId ); return; } logger.info('Getting session history', { requestId: req.requestId, sessionId, }); // PERFORMANCE OPTIMIZATION: Use optimized history retrieval with request deduplication const sessionManager = agent.sessionManager; const cacheKey = `history_${sessionId}`; // Check if there's already a pending request for this session's history if ((sessionManager as any).requestDeduplicator?.has(cacheKey)) { logger.debug(`Deduplicating concurrent history request for session ${sessionId}`); const history = await (sessionManager as any).requestDeduplicator.get(cacheKey); successResponse( res, { sessionId, history: history || [], count: (history || []).length, timestamp: new Date().toISOString(), processingTime: Date.now() - startTime, source: 'deduplication', }, 200, req.requestId ); return; } let history = []; let historySource = 'none'; // PERFORMANCE OPTIMIZATION: Parallel data retrieval const historyPromise = (async () => { // Try session manager first (fastest) try { const session = await agent.getSession(sessionId); if (session) { const sessionHistory = await agent.getSessionHistory(sessionId); if (sessionHistory && sessionHistory.length > 0) { return { history: sessionHistory, source: 'session-manager' }; } } } catch { logger.debug(`Session ${sessionId} not found in session manager`); } // Try database storage (parallel queries for better performance) try { const storageManager = (sessionManager as any).storageManager; if (storageManager?.isConnected()) { const backends = storageManager.getBackends(); if (backends?.database) { // Query both message and session keys in parallel const [messageHistory, sessionData] = await Promise.allSettled([ backends.database.get(`messages:${sessionId}`), backends.database.get(`session:${sessionId}`), ]); // Priority: messages key first if ( messageHistory.status === 'fulfilled' && messageHistory.value && Array.isArray(messageHistory.value) ) { return { history: messageHistory.value, source: 'database-messages' }; } // Fallback: session conversation history if ( sessionData.status === 'fulfilled' && sessionData.value?.conversationHistory && Array.isArray(sessionData.value.conversationHistory) ) { return { history: sessionData.value.conversationHistory, source: 'database-session', }; } } } } catch (error) { logger.warn(`Failed to get history from database for session ${sessionId}:`, error); } return { history: [], source: 'none' }; })(); // Store promise for request deduplication if ((sessionManager as any).requestDeduplicator) { (sessionManager as any).requestDeduplicator.set( cacheKey, historyPromise.then(result => result.history) ); } try { const result = await historyPromise; history = result.history; historySource = result.source; } finally { // Clean up deduplication if ((sessionManager as any).requestDeduplicator) { (sessionManager as any).requestDeduplicator.delete(cacheKey); } } const processingTime = Date.now() - startTime; // Log the final result logger.info( `Session ${sessionId} history retrieval: ${history.length} messages from ${historySource} in ${processingTime}ms` ); successResponse( res, { sessionId, history, count: history.length, timestamp: new Date().toISOString(), processingTime, source: historySource, }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); const processingTime = Date.now() - startTime; logger.error('Failed to get session history', { requestId: req.requestId, sessionId: req.params.sessionId, error: errorMsg, processingTime, }); errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to get session history: ${errorMsg}`, 500, { processingTime }, req.requestId ); } }); /** * DELETE /api/sessions/:sessionId * Delete session */ router.delete('/:sessionId', validateSessionId, async (req: Request, res: Response) => { try { const { sessionId } = req.params; // Handle null or invalid session IDs if (!sessionId || sessionId === 'null' || sessionId === 'undefined') { logger.warn('Attempted to delete session with invalid ID:', sessionId); errorResponse( res, ERROR_CODES.BAD_REQUEST, 'Invalid session ID provided', 400, undefined, req.requestId ); return; } logger.info('Deleting session', { requestId: req.requestId, sessionId, }); // CRITICAL FIX: Always return success for deletion to prevent frontend inconsistencies // The session manager will handle cleanup of non-existent sessions gracefully const success = await agent.removeSession(sessionId); // Always return success for deletions to prevent UI inconsistencies // Even if session wasn't found, it's effectively "deleted" from the user's perspective // CRITICAL FIX: Always return success to prevent UI inconsistencies // The frontend should show the session as deleted immediately successResponse( res, { sessionId, deleted: true, successful: success, timestamp: new Date().toISOString(), }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error('Failed to delete session', { requestId: req.requestId, sessionId: req.params.sessionId, error: errorMsg, }); if (errorMsg.includes('Cannot remove the currently active session')) { errorResponse( res, ERROR_CODES.BAD_REQUEST, 'Cannot delete the currently active session. Switch to another session first.', 400, undefined, req.requestId ); } else { errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to delete session: ${errorMsg}`, 500, undefined, req.requestId ); } } }); /** * GET /api/sessions/stats * Get session performance statistics - MONITORING ENDPOINT */ router.get('/stats', async (req: Request, res: Response) => { try { logger.info('Getting session performance stats', { requestId: req.requestId }); const sessionManager = agent.sessionManager; const stats = await sessionManager.getSessionStats(); // Add additional runtime metrics const runtimeStats = { uptime: process.uptime(), memoryUsage: process.memoryUsage(), timestamp: new Date().toISOString(), requestId: req.requestId, }; successResponse( res, { sessionStats: stats, runtimeStats, optimizationStatus: { cachingEnabled: true, batchProcessingEnabled: true, requestDeduplicationEnabled: true, performanceMonitoringEnabled: true, }, }, 200, req.requestId ); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error('Failed to get session stats', { requestId: req.requestId, error: errorMsg, }); errorResponse( res, ERROR_CODES.INTERNAL_ERROR, `Failed to get session stats: ${errorMsg}`, 500, undefined, req.requestId ); } }); return router; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/campfirein/cipher'

If you have feedback or need assistance with the MCP directory API, please join our Discord server