UNPKG

mega-minds

Version:

Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration

465 lines (391 loc) โ€ข 18.5 kB
// lib/core/StateMonitor.js // Monitors file system state changes and triggers appropriate actions // Implements PRD Core Feature #1: Real-time state monitoring const fs = require('fs-extra'); const path = require('path'); /** * Monitors file system state changes and triggers appropriate actions * Enables real-time coordination between Claude Code and mega-minds * Per PRD: Real-time updates without manual refresh */ class StateMonitor { constructor(sessionManager, agentStateTracker) { this.sessionManager = sessionManager; this.agentState = agentStateTracker; this.projectPath = sessionManager?.memoryPath?.replace('.mega-minds', '') || process.cwd(); this.stateDir = path.join(this.projectPath, '.mega-minds', 'state'); this.isMonitoring = false; this.watchIntervals = new Map(); this.lastFileStates = new Map(); this.pollInterval = 2000; // Poll every 2 seconds for file changes } /** * Start monitoring state files for changes * PRD Requirement: Real-time monitoring without requiring file watchers */ async startMonitoring() { if (this.isMonitoring) { console.log('๐Ÿ‘๏ธ State monitoring already active'); return; } console.log('๐Ÿ‘๏ธ Starting state monitoring...'); this.isMonitoring = true; // Ensure state directory exists await fs.ensureDir(this.stateDir); // Monitor agent state changes this.monitorFile( path.join(this.stateDir, 'active-agents.json'), 'agentState', this.handleAgentStateChange.bind(this) ); // Monitor handoff queue changes this.monitorFile( path.join(this.stateDir, 'handoff-queue.json'), 'handoffQueue', this.handleHandoffQueueChange.bind(this) ); // Monitor system status changes this.monitorFile( path.join(this.stateDir, 'system-status.json'), 'systemStatus', this.handleSystemStatusChange.bind(this) ); // Monitor work progress changes this.monitorFile( path.join(this.stateDir, 'work-progress.json'), 'workProgress', this.handleWorkProgressChange.bind(this) ); console.log('โœ… State monitoring active'); console.log(`๐Ÿ“ Monitoring state files in: ${this.stateDir}`); } /** * Stop monitoring state files */ async stopMonitoring() { if (!this.isMonitoring) return; console.log('โน๏ธ Stopping state monitoring...'); // Clear all watch intervals for (const [name, intervalId] of this.watchIntervals) { clearInterval(intervalId); console.log(`๐Ÿ“ Stopped monitoring: ${name}`); } this.watchIntervals.clear(); this.lastFileStates.clear(); this.isMonitoring = false; console.log('โœ… State monitoring stopped'); } /** * Monitor a specific file for changes using polling * @private */ monitorFile(filePath, name, handler) { // Initialize last state this.lastFileStates.set(filePath, null); const intervalId = setInterval(async () => { try { if (!await fs.pathExists(filePath)) { // File doesn't exist yet, skip return; } const currentContent = await fs.readFile(filePath, 'utf8'); const lastContent = this.lastFileStates.get(filePath); if (currentContent !== lastContent) { // File has changed this.lastFileStates.set(filePath, currentContent); if (lastContent !== null) { // Skip first read try { const data = JSON.parse(currentContent); await handler(data); } catch (parseError) { console.warn(`โš ๏ธ Error parsing ${name}:`, parseError.message); } } } } catch (error) { // Ignore errors for missing files or read failures if (error.code !== 'ENOENT') { console.warn(`โš ๏ธ Error monitoring ${name}:`, error.message); } } }, this.pollInterval); this.watchIntervals.set(name, intervalId); console.log(`๐Ÿ‘๏ธ Monitoring ${name}: ${path.basename(filePath)}`); } /** * Handle agent state changes * PRD Requirement: Agent activation/deactivation tracking */ async handleAgentStateChange(stateData) { try { const activeAgents = stateData.activeAgents || {}; const agentCount = Object.keys(activeAgents).length; console.log(`๐Ÿ”„ Agent state changed: ${agentCount} active agents`); // Update session manager with new agent states if (this.sessionManager && this.sessionManager.currentSession) { this.sessionManager.currentSession.agents.activeAgents = activeAgents; this.sessionManager.currentSession.lastUpdate = new Date().toISOString(); // EMERGENCY FIX: Prevent circular saves by being more selective // Only auto-save if this represents a REAL change (not broadcast-triggered) const shouldAutoSave = this.shouldTriggerAutoSave(activeAgents, agentCount); if (shouldAutoSave) { await this.sessionManager.saveActiveSession(false); // Use throttling console.log(`๐Ÿ’พ Session auto-saved with ${agentCount} active agents (significant change detected, throttled)`); } else { console.log(`โญ๏ธ Skipping auto-save for ${agentCount} agents (broadcast-triggered or minor change)`); } } // Update agent state tracker await this.syncAgentStateTracker(activeAgents); // Check for agent limit violations per PRD if (agentCount > 2) { console.error(`๐Ÿšจ AGENT LIMIT EXCEEDED: ${agentCount}/2 agents active!`); console.log('Consider completing work or deactivating agents.'); } } catch (error) { console.warn('โš ๏ธ Error handling agent state change:', error.message); } } /** * Handle handoff queue changes * PRD Requirement: Handoff event recording and failed detection */ async handleHandoffQueueChange(queueData) { try { const handoffs = queueData.handoffs || []; const pendingCount = queueData.pendingCount || 0; const failedCount = queueData.failedCount || 0; console.log(`๐Ÿ“ค Handoff queue changed: ${handoffs.length} handoffs (${pendingCount} pending, ${failedCount} failed)`); // Update session with handoff data if (this.sessionManager && this.sessionManager.currentSession) { if (!this.sessionManager.currentSession.handoffs) { this.sessionManager.currentSession.handoffs = { active: [], completed: [], failed: [], pending: [], metrics: {} }; } this.sessionManager.currentSession.handoffs.active = handoffs.filter(h => ['initiated', 'acknowledged', 'in_progress'].includes(h.status) ); this.sessionManager.currentSession.handoffs.pending = handoffs.filter(h => h.status === 'initiated' ); this.sessionManager.currentSession.handoffs.failed = handoffs.filter(h => h.status === 'failed' ); // Update metrics this.sessionManager.currentSession.handoffs.metrics = { totalInitiated: handoffs.length, totalCompleted: queueData.completedCount || 0, totalFailed: failedCount, acknowledgmentRate: queueData.acknowledgmentRate || 0, averageCompletionTime: queueData.averageCompletionTime || 0 }; // EMERGENCY FIX: Use throttled save to prevent excessive session creation await this.sessionManager.saveActiveSession(false); // Use throttling console.log(`๐Ÿ’พ Session updated with handoff data (throttled)`); } // Alert on failed handoffs per PRD requirement if (failedCount > 0) { console.error(`โŒ ${failedCount} handoff(s) have failed!`); console.log('Manual intervention may be required.'); } // Check for timeout handoffs (30 seconds per PRD) await this.checkHandoffTimeouts(handoffs); } catch (error) { console.warn('โš ๏ธ Error handling handoff queue change:', error.message); } } /** * Handle system status changes * PRD Requirement: Memory monitoring and alerts */ async handleSystemStatusChange(statusData) { try { const memoryStatus = statusData.memoryStatus; if (!memoryStatus) return; console.log(`๐Ÿ–ฅ๏ธ System status update: Memory ${memoryStatus.heapUsedMB}MB (${memoryStatus.status})`); // Handle memory warnings per PRD thresholds if (memoryStatus.status === 'warning') { console.warn(`โš ๏ธ MEMORY WARNING: ${memoryStatus.heapUsedMB}MB used (threshold: 2000MB)`); console.log('Consider running: npx mega-minds memory-cleanup'); // Auto-cleanup if session manager available if (this.sessionManager) { console.log('๐Ÿงน Initiating automatic memory cleanup...'); await this.sessionManager.forceMemoryCleanup(); } } else if (memoryStatus.status === 'critical') { console.error(`๐Ÿšจ CRITICAL MEMORY: ${memoryStatus.heapUsedMB}MB used (threshold: 3500MB)`); console.log('Emergency cleanup required!'); if (this.sessionManager) { console.log('๐Ÿšจ Emergency memory cleanup initiated...'); await this.sessionManager.forceMemoryCleanup(); // Force garbage collection if available if (global.gc) { global.gc(); console.log('๐Ÿ—‘๏ธ Forced garbage collection'); } } } } catch (error) { console.warn('โš ๏ธ Error handling system status change:', error.message); } } /** * Handle work progress changes * PRD Requirement: Work progress monitoring */ async handleWorkProgressChange(progressData) { try { const overallProgress = progressData.overallProgress || 0; const blockedItems = progressData.blockedItems || 0; console.log(`๐Ÿ“Š Work progress update: ${overallProgress}% overall`); if (blockedItems > 0) { console.warn(`โš ๏ธ ${blockedItems} work item(s) are blocked`); console.log('Consider escalating or providing assistance.'); } // Update session with progress data if (this.sessionManager && this.sessionManager.currentSession) { if (!this.sessionManager.currentSession.progress) { this.sessionManager.currentSession.progress = {}; } this.sessionManager.currentSession.progress = { overall: overallProgress, agents: progressData.agents || {}, blockedCount: blockedItems, lastUpdate: new Date().toISOString() }; // EMERGENCY FIX: Only save on major milestones and use throttling if (overallProgress % 50 === 0 && overallProgress > 0) { await this.sessionManager.saveActiveSession(false); // Use throttling console.log(`๐Ÿ’พ Session saved at ${overallProgress}% progress milestone (throttled)`); } } } catch (error) { console.warn('โš ๏ธ Error handling work progress change:', error.message); } } /** * Sync agent states with AgentStateTracker * @private */ async syncAgentStateTracker(activeAgents) { if (!this.agentState) return; for (const [agentName, agentData] of Object.entries(activeAgents)) { try { // Check if agent is already tracked const currentStates = await this.agentState.getAllAgentStates(); if (!currentStates[agentName]) { // New agent, activate it console.log(`๐Ÿค– Activating tracked agent: ${agentName}`); await this.agentState.activateAgent( agentName, agentData.currentTask || 'Unknown task', agentData ); } else { // Update existing agent await this.agentState.updateAgentProgress( agentName, agentData.progress || 0 ); if (agentData.status) { await this.agentState.updateAgentStatus( agentName, agentData.status, agentData.currentTask ); } } } catch (error) { console.warn(`โš ๏ธ Error syncing agent state for ${agentName}:`, error.message); } } } /** * Check for handoff timeouts per PRD requirement (30 seconds) * @private */ async checkHandoffTimeouts(handoffs) { const now = Date.now(); const timeoutMs = 30000; // 30 seconds per PRD for (const handoff of handoffs) { if (handoff.status === 'initiated' && !handoff.acknowledgmentReceived) { const handoffAge = now - new Date(handoff.timestamp).getTime(); if (handoffAge > timeoutMs) { console.error(`โฐ HANDOFF TIMEOUT: ${handoff.toAgent} has not acknowledged handoff ${handoff.id}`); console.log(`Handoff from ${handoff.fromAgent} has been waiting for ${Math.round(handoffAge / 1000)} seconds`); console.log('Consider manual intervention or re-initiating the handoff.'); } } } } /** * Get monitoring status */ getStatus() { return { isMonitoring: this.isMonitoring, monitoredFiles: Array.from(this.watchIntervals.keys()), stateDirectory: this.stateDir, pollInterval: this.pollInterval }; } /** * EMERGENCY FIX: Determine if agent state change should trigger auto-save * Prevents circular saves from broadcast-triggered changes * @param {object} activeAgents - Current active agents * @param {number} agentCount - Number of active agents * @returns {boolean} Whether to trigger auto-save */ shouldTriggerAutoSave(activeAgents, agentCount) { // Track previous state to detect real changes if (!this.lastKnownAgentState) { this.lastKnownAgentState = {}; } // Don't auto-save if no agents (empty state) if (agentCount === 0) { this.lastKnownAgentState = activeAgents; return false; } // Check if this is a meaningful change (new agent, status change, etc.) const previousCount = Object.keys(this.lastKnownAgentState).length; const hasCountChange = agentCount !== previousCount; // Check for agent additions/removals const currentAgentNames = Object.keys(activeAgents).sort(); const previousAgentNames = Object.keys(this.lastKnownAgentState).sort(); const hasAgentChange = JSON.stringify(currentAgentNames) !== JSON.stringify(previousAgentNames); // Check for significant status changes (not just timestamp updates) let hasStatusChange = false; for (const [agentName, agentData] of Object.entries(activeAgents)) { const previousData = this.lastKnownAgentState[agentName]; if (previousData) { // Check for meaningful changes (exclude timestamps) const currentStatus = agentData.status; const currentTask = agentData.task || agentData.currentTask; const currentProgress = agentData.progress || 0; const previousStatus = previousData.status; const previousTask = previousData.task || previousData.currentTask; const previousProgress = previousData.progress || 0; if (currentStatus !== previousStatus || currentTask !== previousTask || Math.abs(currentProgress - previousProgress) >= 10) { // 10% progress change hasStatusChange = true; break; } } } // Update state tracking this.lastKnownAgentState = JSON.parse(JSON.stringify(activeAgents)); // Only auto-save for significant changes const shouldSave = hasCountChange || hasAgentChange || hasStatusChange; if (shouldSave) { console.log(`๐Ÿ” Auto-save triggered: count=${hasCountChange}, agents=${hasAgentChange}, status=${hasStatusChange}`); } return shouldSave; } } module.exports = StateMonitor;