UNPKG

claude-expert-workflow-mcp

Version:

Production-ready MCP server for AI-powered product development consultation through specialized expert roles. Enterprise-grade with memory management, monitoring, and Claude Code integration.

474 lines (395 loc) 15.3 kB
// Memory Management Foundation for Claude Expert Workflow MCP // Prevents memory leaks, manages resource usage, and ensures system stability import { correlationTracker } from './correlationTracker'; export interface MemoryConfiguration { // Conversation state limits maxConversations: number; conversationTTL: number; // milliseconds maxMessagesPerConversation: number; // Thinking block limits maxThinkingBlocks: number; maxThinkingBlockSize: number; // bytes thinkingBlockTTL: number; // milliseconds // General resource limits maxTotalMemoryMB: number; cleanupInterval: number; // milliseconds gracefulDegradationThreshold: number; // percentage of max memory // Cache and temporary data maxCacheEntries: number; cacheTTL: number; // milliseconds } export const DEFAULT_MEMORY_CONFIG: MemoryConfiguration = { maxConversations: 1000, conversationTTL: 3600000, // 1 hour maxMessagesPerConversation: 100, maxThinkingBlocks: 10, maxThinkingBlockSize: 50000, // 50KB per block thinkingBlockTTL: 1800000, // 30 minutes maxTotalMemoryMB: 500, cleanupInterval: 300000, // 5 minutes gracefulDegradationThreshold: 80, // 80% of max memory maxCacheEntries: 500, cacheTTL: 1800000 // 30 minutes }; export interface ConversationMetrics { id: string; createdAt: number; lastAccessedAt: number; messageCount: number; thinkingBlockCount: number; estimatedSizeBytes: number; correlationIds: string[]; } export interface MemoryMetrics { totalConversations: number; totalMessages: number; totalThinkingBlocks: number; estimatedMemoryUsage: number; // bytes oldestConversation?: number; // timestamp newestConversation?: number; // timestamp avgConversationSize: number; memoryPressure: 'low' | 'medium' | 'high' | 'critical'; } /** * Comprehensive Memory Management System * Handles conversation state, thinking blocks, and resource cleanup */ export class MemoryManager { private static instance: MemoryManager; private config: MemoryConfiguration; private conversationMetrics = new Map<string, ConversationMetrics>(); private cleanupTimer?: NodeJS.Timeout; private lastCleanup: number = 0; private constructor(config: MemoryConfiguration = DEFAULT_MEMORY_CONFIG) { this.config = config; this.startCleanupScheduler(); } static getInstance(config?: MemoryConfiguration): MemoryManager { if (!MemoryManager.instance) { MemoryManager.instance = new MemoryManager(config); } return MemoryManager.instance; } /** * Register a new conversation for memory tracking */ registerConversation( conversationId: string, initialSize: number = 1000, correlationId?: string ): void { const now = Date.now(); this.conversationMetrics.set(conversationId, { id: conversationId, createdAt: now, lastAccessedAt: now, messageCount: 0, thinkingBlockCount: 0, estimatedSizeBytes: initialSize, correlationIds: correlationId ? [correlationId] : [] }); console.error(`[MEMORY-MANAGER] Registered conversation ${conversationId} | Total: ${this.conversationMetrics.size}`); // Check if we need cleanup after registration if (this.conversationMetrics.size > this.config.maxConversations) { this.performEmergencyCleanup(); } } /** * Update conversation metrics when accessed or modified */ updateConversationAccess( conversationId: string, messageAdded?: boolean, thinkingBlocksAdded?: number, additionalSize?: number, correlationId?: string ): void { const metrics = this.conversationMetrics.get(conversationId); if (metrics) { metrics.lastAccessedAt = Date.now(); if (messageAdded) { metrics.messageCount++; } if (thinkingBlocksAdded) { metrics.thinkingBlockCount += thinkingBlocksAdded; } if (additionalSize) { metrics.estimatedSizeBytes += additionalSize; } if (correlationId && !metrics.correlationIds.includes(correlationId)) { metrics.correlationIds.push(correlationId); } // Check if conversation exceeds limits if (metrics.messageCount > this.config.maxMessagesPerConversation) { console.error(`[MEMORY-WARNING] Conversation ${conversationId} exceeds message limit: ${metrics.messageCount}`); } } } /** * Validate and manage thinking blocks for memory safety */ validateThinkingBlocks( conversationId: string, thinkingBlocks: any[] ): { validBlocks: any[]; warnings: string[] } { const warnings: string[] = []; const validBlocks: any[] = []; if (!Array.isArray(thinkingBlocks)) { warnings.push('Thinking blocks must be an array'); return { validBlocks: [], warnings }; } for (let i = 0; i < thinkingBlocks.length; i++) { const block = thinkingBlocks[i]; // Validate block structure if (!block || typeof block !== 'object') { warnings.push(`Invalid thinking block at index ${i}: not an object`); continue; } if (block.type !== 'thinking') { warnings.push(`Invalid thinking block at index ${i}: type must be 'thinking'`); continue; } // Additional validation for required fields if (!block.hasOwnProperty('content') && !block.hasOwnProperty('id')) { warnings.push(`Invalid thinking block at index ${i}: missing content or id`); continue; } // Validate block size const blockSize = JSON.stringify(block).length; if (blockSize > this.config.maxThinkingBlockSize) { warnings.push(`Thinking block at index ${i} exceeds size limit (${blockSize} > ${this.config.maxThinkingBlockSize})`); continue; } validBlocks.push(block); // Check total blocks limit if (validBlocks.length >= this.config.maxThinkingBlocks) { warnings.push(`Truncating thinking blocks at ${this.config.maxThinkingBlocks} blocks for memory management`); break; } } // Update conversation metrics this.updateConversationAccess( conversationId, false, validBlocks.length, validBlocks.reduce((size, block) => size + JSON.stringify(block).length, 0) ); return { validBlocks, warnings }; } /** * Clean up expired conversations and thinking blocks */ performCleanup(): { removedConversations: number; warnings: string[] } { const now = Date.now(); const warnings: string[] = []; let removedConversations = 0; // Clean up expired conversations for (const [conversationId, metrics] of this.conversationMetrics.entries()) { const age = now - metrics.createdAt; const lastAccessAge = now - metrics.lastAccessedAt; if (age > this.config.conversationTTL || lastAccessAge > this.config.conversationTTL) { this.conversationMetrics.delete(conversationId); removedConversations++; console.error(`[MEMORY-CLEANUP] Removed expired conversation ${conversationId} (age: ${Math.round(age / 1000)}s)`); } } // Clean up correlation tracker data try { correlationTracker.cleanup(this.config.cacheTTL); } catch (error) { warnings.push(`Correlation tracker cleanup error: ${error}`); } this.lastCleanup = now; console.error(`[MEMORY-CLEANUP] Completed cleanup | Removed: ${removedConversations} conversations | Active: ${this.conversationMetrics.size}`); return { removedConversations, warnings }; } /** * Emergency cleanup when memory pressure is high */ performEmergencyCleanup(): void { console.error('[MEMORY-EMERGENCY] Performing emergency cleanup due to memory pressure'); // Sort conversations by last access time (oldest first) const sortedConversations = Array.from(this.conversationMetrics.entries()) .sort(([, a], [, b]) => a.lastAccessedAt - b.lastAccessedAt); // Remove oldest 25% of conversations const toRemove = Math.ceil(sortedConversations.length * 0.25); for (let i = 0; i < toRemove; i++) { const [conversationId] = sortedConversations[i]; this.conversationMetrics.delete(conversationId); console.error(`[MEMORY-EMERGENCY] Removed conversation ${conversationId}`); } // Force correlation tracker cleanup correlationTracker.cleanup(this.config.cacheTTL / 2); } /** * Get current memory metrics for monitoring */ getMemoryMetrics(): MemoryMetrics { const conversations = Array.from(this.conversationMetrics.values()); const totalMessages = conversations.reduce((sum, conv) => sum + conv.messageCount, 0); const totalThinkingBlocks = conversations.reduce((sum, conv) => sum + conv.thinkingBlockCount, 0); const totalSizeBytes = conversations.reduce((sum, conv) => sum + conv.estimatedSizeBytes, 0); const timestamps = conversations.map(conv => conv.createdAt); const oldestConversation = timestamps.length > 0 ? Math.min(...timestamps) : undefined; const newestConversation = timestamps.length > 0 ? Math.max(...timestamps) : undefined; const avgConversationSize = conversations.length > 0 ? totalSizeBytes / conversations.length : 0; // Estimate memory pressure const maxMemoryBytes = this.config.maxTotalMemoryMB * 1024 * 1024; const memoryUsagePercent = (totalSizeBytes / maxMemoryBytes) * 100; let memoryPressure: 'low' | 'medium' | 'high' | 'critical' = 'low'; if (memoryUsagePercent > 90) memoryPressure = 'critical'; else if (memoryUsagePercent > this.config.gracefulDegradationThreshold) memoryPressure = 'high'; else if (memoryUsagePercent > 60) memoryPressure = 'medium'; return { totalConversations: conversations.length, totalMessages, totalThinkingBlocks, estimatedMemoryUsage: totalSizeBytes, oldestConversation, newestConversation, avgConversationSize, memoryPressure }; } /** * Check if system should enter graceful degradation mode */ shouldEnterGracefulDegradation(): boolean { const metrics = this.getMemoryMetrics(); return metrics.memoryPressure === 'high' || metrics.memoryPressure === 'critical'; } /** * Get recommendations for memory optimization */ getOptimizationRecommendations(): string[] { const metrics = this.getMemoryMetrics(); const recommendations: string[] = []; if (metrics.memoryPressure === 'critical') { recommendations.push('CRITICAL: Immediate memory cleanup required'); recommendations.push('Consider restarting the server to clear memory leaks'); } if (metrics.memoryPressure === 'high') { recommendations.push('Perform manual cleanup of old conversations'); recommendations.push('Reduce thinking block limits temporarily'); } if (metrics.totalConversations > this.config.maxConversations * 0.8) { recommendations.push('Approaching maximum conversation limit'); } if (metrics.avgConversationSize > 100000) { // 100KB average recommendations.push('Large average conversation size detected - review message retention'); } return recommendations; } /** * Start automatic cleanup scheduler */ private startCleanupScheduler(): void { this.cleanupTimer = setInterval(() => { this.performCleanup(); }, this.config.cleanupInterval); console.error(`[MEMORY-MANAGER] Started cleanup scheduler (${this.config.cleanupInterval}ms interval)`); } /** * Stop cleanup scheduler (for testing or shutdown) */ stopCleanupScheduler(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; console.error('[MEMORY-MANAGER] Stopped cleanup scheduler'); } } /** * Remove specific conversation from tracking */ removeConversation(conversationId: string): boolean { const removed = this.conversationMetrics.delete(conversationId); if (removed) { console.error(`[MEMORY-MANAGER] Manually removed conversation ${conversationId}`); } return removed; } /** * Get conversation metrics for debugging */ getConversationMetrics(conversationId: string): ConversationMetrics | undefined { return this.conversationMetrics.get(conversationId); } /** * Get all conversation metrics (for monitoring dashboard) */ getAllConversationMetrics(): ConversationMetrics[] { return Array.from(this.conversationMetrics.values()); } /** * Get current configuration */ getConfiguration(): MemoryConfiguration { return { ...this.config }; } /** * Update memory manager configuration */ updateConfiguration(newConfig: Partial<MemoryConfiguration>): void { const oldConfig = { ...this.config }; this.config = { ...this.config, ...newConfig }; // Log configuration changes const changes = Object.entries(newConfig).filter(([key, value]) => oldConfig[key as keyof MemoryConfiguration] !== value ); if (changes.length > 0) { console.error('[MEMORY-MANAGER] Configuration updated:', changes.map(([key, value]) => `${key}: ${oldConfig[key as keyof MemoryConfiguration]}${value}`) ); // Restart cleanup scheduler with new interval if changed if (newConfig.cleanupInterval !== undefined && newConfig.cleanupInterval !== oldConfig.cleanupInterval) { this.stopCleanupScheduler(); this.startCleanupScheduler(); } // Perform immediate cleanup if limits were reduced if (newConfig.maxConversations !== undefined && newConfig.maxConversations < oldConfig.maxConversations) { this.performEmergencyCleanup(); } // Perform immediate cleanup if memory limits were reduced if (newConfig.maxTotalMemoryMB !== undefined && newConfig.maxTotalMemoryMB < oldConfig.maxTotalMemoryMB) { this.performCleanup(); } } } } // Singleton instance for easy access export const memoryManager = MemoryManager.getInstance(); /** * Helper function to integrate memory management with conversation state */ export function withMemoryManagement<T extends Map<string, any>>( conversationStates: T, memoryManager: MemoryManager ): T { // Override set method to register conversations const originalSet = conversationStates.set.bind(conversationStates); conversationStates.set = function(key: string, value: any) { // Register or update conversation if (!memoryManager.getConversationMetrics(key)) { memoryManager.registerConversation(key); } memoryManager.updateConversationAccess(key, true); return originalSet(key, value); }; // Override get method to track access const originalGet = conversationStates.get.bind(conversationStates); conversationStates.get = function(key: string) { const result = originalGet(key); if (result) { memoryManager.updateConversationAccess(key); } return result; }; // Override delete method to clean up tracking const originalDelete = conversationStates.delete.bind(conversationStates); conversationStates.delete = function(key: string) { memoryManager.removeConversation(key); return originalDelete(key); }; return conversationStates; }