mega-minds
Version:
Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration
730 lines (623 loc) • 21.7 kB
JavaScript
/**
* Streaming Update Manager for Mega-Minds Variable-Driven Agent System
* Phase 3: Real-time Claude.md Updates and Live Context Streaming
*/
const EventEmitter = require('events');
const fs = require('fs').promises;
const path = require('path');
class StreamingUpdateManager extends EventEmitter {
constructor(projectPath, mcpServer, config = {}) {
super();
this.projectPath = projectPath;
this.mcpServer = mcpServer;
this.config = {
updateInterval: config.updateInterval || 15000, // 15 seconds
maxUpdateBuffer: config.maxUpdateBuffer || 100,
enableFileWatching: config.enableFileWatching || true,
enableContextStreaming: config.enableContextStreaming || true,
debounceDelay: config.debounceDelay || 2000, // 2 seconds
...config
};
this.activeStreams = new Map();
this.updateBuffer = [];
this.fileWatchers = new Map();
this.lastUpdateTime = new Map();
this.debounceTimers = new Map();
this.initialize();
}
/**
* Initialize streaming update system
*/
async initialize() {
try {
// Set up file watchers for template changes
if (this.config.enableFileWatching) {
await this.setupFileWatchers();
}
// Start periodic update check
this.startPeriodicUpdates();
console.log('Streaming Update Manager initialized');
this.emit('initialized');
} catch (error) {
console.error('Failed to initialize Streaming Update Manager:', error);
this.emit('error', error);
}
}
/**
* Enable real-time streaming updates for a session
* @param {string} sessionId - Session ID to stream updates to
* @param {Object} options - Streaming options
* @returns {string} Stream ID for managing the stream
*/
enableStreamingUpdates(sessionId, options = {}) {
const streamId = `stream_${sessionId}_${Date.now()}`;
const streamConfig = {
sessionId: sessionId,
startTime: new Date(),
options: {
includeVariables: options.includeVariables !== false,
includeSections: options.includeSections !== false,
includePerformance: options.includePerformance !== false,
includeSystemHealth: options.includeSystemHealth !== false,
updateTypes: options.updateTypes || ['context', 'variables', 'performance', 'health'],
...options
},
lastUpdate: null,
updateCount: 0
};
this.activeStreams.set(streamId, streamConfig);
// Start streaming for this session
this.startSessionStream(streamId);
console.log(`Streaming updates enabled for session ${sessionId} (stream: ${streamId})`);
this.emit('stream-started', { streamId, sessionId });
return streamId;
}
/**
* Disable streaming updates for a specific stream
* @param {string} streamId - Stream ID to disable
*/
disableStreamingUpdates(streamId) {
const stream = this.activeStreams.get(streamId);
if (stream) {
// Clear any timers
if (stream.intervalId) {
clearInterval(stream.intervalId);
}
this.activeStreams.delete(streamId);
console.log(`Streaming updates disabled for stream ${streamId}`);
this.emit('stream-stopped', { streamId, sessionId: stream.sessionId });
}
}
/**
* Start streaming for a specific session
* @param {string} streamId - Stream ID
*/
startSessionStream(streamId) {
const stream = this.activeStreams.get(streamId);
if (!stream) return;
const streamUpdates = async () => {
try {
const updates = await this.generateStreamingUpdates(stream);
if (updates && updates.length > 0) {
stream.lastUpdate = new Date();
stream.updateCount += updates.length;
this.emit('updates-available', {
streamId: streamId,
sessionId: stream.sessionId,
updates: updates,
timestamp: stream.lastUpdate
});
}
} catch (error) {
console.error(`Streaming update error for ${streamId}:`, error);
this.emit('stream-error', { streamId, error });
}
};
// Initial update
streamUpdates();
// Set up periodic updates
stream.intervalId = setInterval(streamUpdates, this.config.updateInterval);
}
/**
* Generate streaming updates for a session
* @param {Object} stream - Stream configuration
* @returns {Array} Array of updates
*/
async generateStreamingUpdates(stream) {
const updates = [];
const sessionId = stream.sessionId;
try {
// Get current context from MCP server
const currentContext = await this.mcpServer.getAgentContext(sessionId);
// Check for context changes
if (stream.options.updateTypes.includes('context')) {
const contextUpdates = await this.detectContextChanges(sessionId, currentContext);
updates.push(...contextUpdates);
}
// Check for variable updates
if (stream.options.updateTypes.includes('variables')) {
const variableUpdates = await this.detectVariableChanges(sessionId, currentContext);
updates.push(...variableUpdates);
}
// Check for performance updates
if (stream.options.updateTypes.includes('performance')) {
const performanceUpdates = await this.detectPerformanceChanges(sessionId, currentContext);
updates.push(...performanceUpdates);
}
// Check for system health updates
if (stream.options.updateTypes.includes('health')) {
const healthUpdates = await this.detectHealthChanges(sessionId, currentContext);
updates.push(...healthUpdates);
}
// Check for template file changes
const templateUpdates = await this.detectTemplateChanges();
updates.push(...templateUpdates);
} catch (error) {
console.error('Error generating streaming updates:', error);
updates.push({
type: 'error',
message: 'Failed to generate updates',
error: error.message,
timestamp: new Date().toISOString()
});
}
return updates;
}
/**
* Detect context changes for streaming
* @param {string} sessionId - Session ID
* @param {Object} currentContext - Current context
* @returns {Array} Context change updates
*/
async detectContextChanges(sessionId, currentContext) {
const updates = [];
const cacheKey = `context_${sessionId}`;
const lastContext = this.getLastContext(cacheKey);
if (!lastContext) {
// First time - record current context
this.cacheContext(cacheKey, currentContext);
return [{
type: 'context-initialized',
sessionId: sessionId,
context: currentContext,
timestamp: new Date().toISOString()
}];
}
// Check for significant changes
const changes = this.compareContexts(lastContext, currentContext);
if (changes.length > 0) {
updates.push({
type: 'context-changed',
sessionId: sessionId,
changes: changes,
newContext: currentContext,
timestamp: new Date().toISOString()
});
// Update cached context
this.cacheContext(cacheKey, currentContext);
}
return updates;
}
/**
* Detect variable changes for streaming
* @param {string} sessionId - Session ID
* @param {Object} currentContext - Current context
* @returns {Array} Variable change updates
*/
async detectVariableChanges(sessionId, currentContext) {
const updates = [];
const cacheKey = `variables_${sessionId}`;
try {
// Generate current variables
const currentVariables = await this.mcpServer.variableEngine.generateVariables(
'default',
'streaming',
currentContext
);
const lastVariables = this.getLastVariables(cacheKey);
if (lastVariables) {
const variableChanges = this.compareVariables(lastVariables, currentVariables);
if (variableChanges.length > 0) {
updates.push({
type: 'variables-changed',
sessionId: sessionId,
changes: variableChanges,
newVariables: currentVariables,
timestamp: new Date().toISOString()
});
}
}
// Cache current variables
this.cacheVariables(cacheKey, currentVariables);
} catch (error) {
console.error('Error detecting variable changes:', error);
}
return updates;
}
/**
* Detect performance changes for streaming
* @param {string} sessionId - Session ID
* @param {Object} currentContext - Current context
* @returns {Array} Performance change updates
*/
async detectPerformanceChanges(sessionId, currentContext) {
const updates = [];
const cacheKey = `performance_${sessionId}`;
const currentPerf = {
memoryUsage: currentContext.memory?.current || 0,
systemHealth: currentContext.system?.status || 'unknown',
coordinationSuccess: currentContext.activeAgents?.coordinationSuccess || 0,
optimizationScore: await this.calculateOptimizationScore(currentContext)
};
const lastPerf = this.getLastPerformance(cacheKey);
if (lastPerf) {
const perfChanges = [];
// Check for significant memory changes (>5% change)
const memoryChange = Math.abs(currentPerf.memoryUsage - lastPerf.memoryUsage);
if (memoryChange > (lastPerf.memoryUsage * 0.05)) {
perfChanges.push({
metric: 'memory',
from: lastPerf.memoryUsage,
to: currentPerf.memoryUsage,
change: memoryChange
});
}
// Check for system health changes
if (currentPerf.systemHealth !== lastPerf.systemHealth) {
perfChanges.push({
metric: 'system-health',
from: lastPerf.systemHealth,
to: currentPerf.systemHealth
});
}
// Check for coordination success changes (>5% change)
const coordChange = Math.abs(currentPerf.coordinationSuccess - lastPerf.coordinationSuccess);
if (coordChange > 5) {
perfChanges.push({
metric: 'coordination',
from: lastPerf.coordinationSuccess,
to: currentPerf.coordinationSuccess,
change: coordChange
});
}
if (perfChanges.length > 0) {
updates.push({
type: 'performance-changed',
sessionId: sessionId,
changes: perfChanges,
currentPerformance: currentPerf,
timestamp: new Date().toISOString()
});
}
}
// Cache current performance
this.cachePerformance(cacheKey, currentPerf);
return updates;
}
/**
* Detect system health changes
* @param {string} sessionId - Session ID
* @param {Object} currentContext - Current context
* @returns {Array} Health change updates
*/
async detectHealthChanges(sessionId, currentContext) {
const updates = [];
const cacheKey = `health_${sessionId}`;
const currentHealth = {
status: currentContext.system?.status || 'unknown',
memory: currentContext.system?.memory || {},
uptime: currentContext.system?.uptime || 0,
alerts: this.generateHealthAlerts(currentContext)
};
const lastHealth = this.getLastHealth(cacheKey);
if (lastHealth && lastHealth.status !== currentHealth.status) {
updates.push({
type: 'health-changed',
sessionId: sessionId,
from: lastHealth.status,
to: currentHealth.status,
alerts: currentHealth.alerts,
timestamp: new Date().toISOString()
});
}
// Check for new alerts
if (lastHealth && currentHealth.alerts.length > lastHealth.alerts.length) {
const newAlerts = currentHealth.alerts.filter(alert =>
!lastHealth.alerts.some(lastAlert => lastAlert.id === alert.id)
);
if (newAlerts.length > 0) {
updates.push({
type: 'new-alerts',
sessionId: sessionId,
alerts: newAlerts,
timestamp: new Date().toISOString()
});
}
}
// Cache current health
this.cacheHealth(cacheKey, currentHealth);
return updates;
}
/**
* Detect template file changes
* @returns {Array} Template change updates
*/
async detectTemplateChanges() {
const updates = [];
// Check if any watched files have changed
for (const [filePath, lastModified] of this.lastUpdateTime) {
try {
const stats = await fs.stat(filePath);
const currentModified = stats.mtime.getTime();
if (currentModified > lastModified) {
updates.push({
type: 'template-changed',
filePath: filePath,
lastModified: new Date(currentModified).toISOString(),
timestamp: new Date().toISOString()
});
// Update the last modified time
this.lastUpdateTime.set(filePath, currentModified);
// Debounce template reload
this.debounceTemplateReload(filePath);
}
} catch (error) {
console.error(`Error checking file ${filePath}:`, error);
}
}
return updates;
}
/**
* Setup file watchers for template changes
*/
async setupFileWatchers() {
const watchPaths = [
path.join(this.projectPath, 'templates/claude.md'),
path.join(this.projectPath, 'templates/RULES.md'),
path.join(this.projectPath, 'templates/QUICKREF.md'),
path.join(this.projectPath, 'workflows')
];
for (const watchPath of watchPaths) {
try {
const stats = await fs.stat(watchPath);
if (stats.isDirectory()) {
// Watch all .md files in directory
const files = await fs.readdir(watchPath);
for (const file of files) {
if (file.endsWith('.md')) {
const filePath = path.join(watchPath, file);
await this.watchFile(filePath);
}
}
} else {
// Watch single file
await this.watchFile(watchPath);
}
} catch (error) {
console.error(`Cannot watch path ${watchPath}:`, error);
}
}
}
/**
* Watch a single file for changes
* @param {string} filePath - File path to watch
*/
async watchFile(filePath) {
try {
const stats = await fs.stat(filePath);
this.lastUpdateTime.set(filePath, stats.mtime.getTime());
// Note: In a real implementation, you'd use fs.watch() or chokidar
// For this implementation, we'll rely on periodic checks
console.log(`Watching file: ${filePath}`);
} catch (error) {
console.error(`Cannot watch file ${filePath}:`, error);
}
}
/**
* Debounce template reload to prevent excessive updates
* @param {string} filePath - File path that changed
*/
debounceTemplateReload(filePath) {
const timerId = this.debounceTimers.get(filePath);
if (timerId) {
clearTimeout(timerId);
}
const newTimerId = setTimeout(async () => {
try {
// Notify all active streams about template change
for (const [streamId, stream] of this.activeStreams) {
this.emit('template-reloaded', {
streamId: streamId,
sessionId: stream.sessionId,
filePath: filePath,
timestamp: new Date().toISOString()
});
}
console.log(`Template reloaded: ${filePath}`);
} catch (error) {
console.error('Error during template reload:', error);
} finally {
this.debounceTimers.delete(filePath);
}
}, this.config.debounceDelay);
this.debounceTimers.set(filePath, newTimerId);
}
/**
* Start periodic updates for all streams
*/
startPeriodicUpdates() {
// This is handled per-stream in startSessionStream
console.log('Periodic updates started');
}
/**
* Compare two contexts and return differences
* @param {Object} lastContext - Previous context
* @param {Object} currentContext - Current context
* @returns {Array} Array of changes
*/
compareContexts(lastContext, currentContext) {
const changes = [];
// Check memory changes
if (lastContext.memory?.current !== currentContext.memory?.current) {
changes.push({
path: 'memory.current',
from: lastContext.memory?.current,
to: currentContext.memory?.current
});
}
// Check agent count changes
if (lastContext.activeAgents?.count !== currentContext.activeAgents?.count) {
changes.push({
path: 'activeAgents.count',
from: lastContext.activeAgents?.count,
to: currentContext.activeAgents?.count
});
}
// Check system status changes
if (lastContext.system?.status !== currentContext.system?.status) {
changes.push({
path: 'system.status',
from: lastContext.system?.status,
to: currentContext.system?.status
});
}
return changes;
}
/**
* Compare two variable sets
* @param {Object} lastVariables - Previous variables
* @param {Object} currentVariables - Current variables
* @returns {Array} Array of changes
*/
compareVariables(lastVariables, currentVariables) {
const changes = [];
for (const [key, value] of Object.entries(currentVariables)) {
if (lastVariables[key] !== value) {
changes.push({
variable: key,
from: lastVariables[key],
to: value
});
}
}
return changes;
}
/**
* Generate health alerts from context
* @param {Object} context - Current context
* @returns {Array} Health alerts
*/
generateHealthAlerts(context) {
const alerts = [];
const memoryUsage = (context.memory?.current || 0) / (context.memory?.limit || 3500) * 100;
if (memoryUsage > 90) {
alerts.push({
id: 'memory-critical',
level: 'critical',
message: 'Critical memory usage detected',
value: `${Math.round(memoryUsage)}%`
});
} else if (memoryUsage > 75) {
alerts.push({
id: 'memory-warning',
level: 'warning',
message: 'High memory usage detected',
value: `${Math.round(memoryUsage)}%`
});
}
if (context.activeAgents?.count >= context.activeAgents?.limit) {
alerts.push({
id: 'agent-limit',
level: 'info',
message: 'Maximum concurrent agents reached',
value: `${context.activeAgents.count}/${context.activeAgents.limit}`
});
}
return alerts;
}
/**
* Calculate optimization score from context
* @param {Object} context - Context object
* @returns {number} Optimization score
*/
async calculateOptimizationScore(context) {
// This would integrate with the actual variable engine
return 8.5 + Math.random() * 1.5; // 8.5-10.0
}
// Cache management methods
cacheContext(key, context) {
// Simple in-memory cache - in production would use Redis or similar
this.contextCache = this.contextCache || new Map();
this.contextCache.set(key, context);
}
getLastContext(key) {
this.contextCache = this.contextCache || new Map();
return this.contextCache.get(key);
}
cacheVariables(key, variables) {
this.variablesCache = this.variablesCache || new Map();
this.variablesCache.set(key, variables);
}
getLastVariables(key) {
this.variablesCache = this.variablesCache || new Map();
return this.variablesCache.get(key);
}
cachePerformance(key, performance) {
this.performanceCache = this.performanceCache || new Map();
this.performanceCache.set(key, performance);
}
getLastPerformance(key) {
this.performanceCache = this.performanceCache || new Map();
return this.performanceCache.get(key);
}
cacheHealth(key, health) {
this.healthCache = this.healthCache || new Map();
this.healthCache.set(key, health);
}
getLastHealth(key) {
this.healthCache = this.healthCache || new Map();
return this.healthCache.get(key);
}
/**
* Get streaming statistics
* @returns {Object} Statistics object
*/
getStreamingStats() {
const stats = {
activeStreams: this.activeStreams.size,
totalUpdates: this.updateBuffer.length,
watchedFiles: this.lastUpdateTime.size,
cacheSize: {
context: this.contextCache?.size || 0,
variables: this.variablesCache?.size || 0,
performance: this.performanceCache?.size || 0,
health: this.healthCache?.size || 0
}
};
return stats;
}
/**
* Cleanup resources and stop all streams
*/
cleanup() {
// Stop all active streams
for (const [streamId, stream] of this.activeStreams) {
if (stream.intervalId) {
clearInterval(stream.intervalId);
}
}
this.activeStreams.clear();
// Clear all timers
for (const timerId of this.debounceTimers.values()) {
clearTimeout(timerId);
}
this.debounceTimers.clear();
// Clear caches
if (this.contextCache) this.contextCache.clear();
if (this.variablesCache) this.variablesCache.clear();
if (this.performanceCache) this.performanceCache.clear();
if (this.healthCache) this.healthCache.clear();
console.log('Streaming Update Manager cleaned up');
}
}
module.exports = { StreamingUpdateManager };