UNPKG

claude-coordination-system

Version:

🤖 Multi-Claude Parallel Processing Coordination System - Organize multiple Claude AI instances to work together seamlessly on complex development tasks

475 lines (395 loc) 12.9 kB
/** * Claude Coordination Engine - Real-time Collaboration System * Enables natural Claude conversations with transparent coordination */ const fs = require('fs-extra'); const path = require('path'); const EventEmitter = require('events'); const chalk = require('chalk'); const chokidar = require('chokidar'); const { logCoordinator } = require('./development-logger'); class CoordinationEngine extends EventEmitter { constructor(projectRoot, options = {}) { super(); this.projectRoot = projectRoot; this.coordinationDir = path.join(projectRoot, '.claude-coord'); this.sessionFile = path.join(this.coordinationDir, 'active-sessions.json'); this.groupStateFile = path.join(this.coordinationDir, 'group-states.json'); this.options = { syncInterval: 1000, // 1 second sync heartbeatInterval: 5000, // 5 second heartbeat sessionTimeout: 30000, // 30 second timeout ...options }; // Active coordination state this.activeSessions = new Map(); this.groupStates = new Map(); this.fileLocks = new Map(); this.messageQueue = []; // Event handlers this.isRunning = false; this.syncTimer = null; this.heartbeatTimer = null; console.log(chalk.blue('🔄 Coordination Engine initialized')); } /** * Start coordination engine */ async start() { if (this.isRunning) return; this.isRunning = true; // Ensure coordination directory exists await fs.ensureDir(this.coordinationDir); // Load existing state await this.loadState(); // Start coordination loops this.startSyncLoop(); this.startHeartbeatMonitor(); // Setup file watching for coordination files this.setupFileWatching(); console.log(chalk.green('✅ Coordination Engine started')); await logCoordinator('Coordination Engine Started', { result: 'SUCCESS', sessionFile: this.sessionFile, groupStateFile: this.groupStateFile }); this.emit('engine:started'); } /** * Stop coordination engine */ async stop() { if (!this.isRunning) return; this.isRunning = false; // Clear timers if (this.syncTimer) clearInterval(this.syncTimer); if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); // Close file watchers if (this.fileWatcher) this.fileWatcher.close(); // Save final state await this.saveState(); console.log(chalk.yellow('🛑 Coordination Engine stopped')); this.emit('engine:stopped'); } /** * Register a new Claude session (terminal) */ async registerSession(sessionId, groupId, metadata = {}) { const session = { id: sessionId, group: groupId, status: 'active', joinedAt: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), metadata: { terminalId: metadata.terminalId || sessionId, userId: metadata.userId || 'default', ...metadata } }; this.activeSessions.set(sessionId, session); // Initialize group if needed if (!this.groupStates.has(groupId)) { this.groupStates.set(groupId, { id: groupId, sessions: [], primarySession: sessionId, currentTask: null, fileScope: [], createdAt: new Date().toISOString() }); } // Add to group const groupState = this.groupStates.get(groupId); if (!groupState.sessions.includes(sessionId)) { groupState.sessions.push(sessionId); } await this.saveState(); console.log(chalk.green(`👥 Session registered: ${sessionId} → ${groupId}`)); this.emit('session:registered', { sessionId, groupId, session }); return session; } /** * Update session heartbeat */ async updateHeartbeat(sessionId) { const session = this.activeSessions.get(sessionId); if (session) { session.lastHeartbeat = new Date().toISOString(); session.status = 'active'; await this.saveState(); } } /** * Broadcast message to group members */ async broadcastToGroup(groupId, message, excludeSessionId = null) { const groupState = this.groupStates.get(groupId); if (!groupState) return; const broadcastMessage = { id: this.generateMessageId(), type: 'group_broadcast', from: excludeSessionId, to: groupId, content: message, timestamp: new Date().toISOString() }; // Add to message queue for each group member for (const sessionId of groupState.sessions) { if (sessionId !== excludeSessionId) { this.messageQueue.push({ ...broadcastMessage, targetSession: sessionId }); } } await this.saveState(); this.emit('message:broadcast', { groupId, message: broadcastMessage, recipients: groupState.sessions.filter(id => id !== excludeSessionId) }); } /** * Acquire file lock for coordination */ async acquireFileLock(sessionId, filePath, lockType = 'write') { const normalizedPath = path.normalize(filePath); const existingLock = this.fileLocks.get(normalizedPath); // Check if file is already locked by another session if (existingLock && existingLock.sessionId !== sessionId) { return { success: false, lockedBy: existingLock.sessionId, lockType: existingLock.type }; } // Acquire lock const lock = { sessionId, filePath: normalizedPath, type: lockType, acquiredAt: new Date().toISOString() }; this.fileLocks.set(normalizedPath, lock); await this.saveState(); console.log(chalk.cyan(`🔒 File locked: ${path.relative(this.projectRoot, normalizedPath)} → ${sessionId}`)); this.emit('file:locked', lock); return { success: true, lock }; } /** * Release file lock */ async releaseFileLock(sessionId, filePath) { const normalizedPath = path.normalize(filePath); const existingLock = this.fileLocks.get(normalizedPath); if (existingLock && existingLock.sessionId === sessionId) { this.fileLocks.delete(normalizedPath); await this.saveState(); console.log(chalk.cyan(`🔓 File unlocked: ${path.relative(this.projectRoot, normalizedPath)} ← ${sessionId}`)); this.emit('file:unlocked', { sessionId, filePath: normalizedPath }); return true; } return false; } /** * Get messages for specific session */ getMessagesForSession(sessionId) { return this.messageQueue.filter(msg => msg.targetSession === sessionId); } /** * Clear messages for session */ clearMessagesForSession(sessionId) { this.messageQueue = this.messageQueue.filter(msg => msg.targetSession !== sessionId); } /** * Get group information */ getGroupInfo(groupId) { const groupState = this.groupStates.get(groupId); if (!groupState) return null; const sessionDetails = groupState.sessions.map(sessionId => { const session = this.activeSessions.get(sessionId); return session ? { id: sessionId, status: session.status, lastHeartbeat: session.lastHeartbeat, metadata: session.metadata } : null; }).filter(Boolean); return { ...groupState, sessionCount: groupState.sessions.length, activeSessionCount: sessionDetails.filter(s => s.status === 'active').length, sessions: sessionDetails }; } /** * Get coordination status */ getCoordinationStatus() { const groups = Array.from(this.groupStates.values()).map(group => ({ id: group.id, sessionCount: group.sessions.length, primarySession: group.primarySession, currentTask: group.currentTask })); return { engine: { running: this.isRunning, uptime: this.isRunning ? Date.now() - this.startTime : 0 }, sessions: { total: this.activeSessions.size, active: Array.from(this.activeSessions.values()).filter(s => s.status === 'active').length }, groups: { total: this.groupStates.size, list: groups }, fileLocks: { total: this.fileLocks.size, active: Array.from(this.fileLocks.values()) }, messageQueue: { pending: this.messageQueue.length } }; } /** * Load coordination state from disk */ async loadState() { try { // Load active sessions if (await fs.pathExists(this.sessionFile)) { const sessionsData = await fs.readJson(this.sessionFile); for (const [sessionId, session] of Object.entries(sessionsData)) { this.activeSessions.set(sessionId, session); } } // Load group states if (await fs.pathExists(this.groupStateFile)) { const groupsData = await fs.readJson(this.groupStateFile); for (const [groupId, groupState] of Object.entries(groupsData)) { this.groupStates.set(groupId, groupState); } } // Only log state on startup or significant changes if (this.options.verbose) { console.log(chalk.gray(`📂 Loaded state: ${this.activeSessions.size} sessions, ${this.groupStates.size} groups`)); } } catch (error) { console.warn(chalk.yellow('⚠️ Could not load coordination state:'), error.message); } } /** * Save coordination state to disk */ async saveState() { try { // Save active sessions const sessionsData = Object.fromEntries(this.activeSessions); await fs.writeJson(this.sessionFile, sessionsData, { spaces: 2 }); // Save group states const groupsData = Object.fromEntries(this.groupStates); await fs.writeJson(this.groupStateFile, groupsData, { spaces: 2 }); } catch (error) { console.error(chalk.red('❌ Failed to save coordination state:'), error.message); } } /** * Start sync loop for real-time coordination */ startSyncLoop() { this.syncTimer = setInterval(async () => { try { await this.processSyncUpdates(); } catch (error) { console.error(chalk.red('❌ Sync loop error:'), error.message); } }, this.options.syncInterval); } /** * Process sync updates */ async processSyncUpdates() { // Check for stale sessions const now = Date.now(); const staleSessions = []; for (const [sessionId, session] of this.activeSessions.entries()) { const lastHeartbeat = new Date(session.lastHeartbeat).getTime(); if (now - lastHeartbeat > this.options.sessionTimeout) { staleSessions.push(sessionId); } } // Remove stale sessions for (const sessionId of staleSessions) { await this.removeSession(sessionId); } // Save state if there were changes if (staleSessions.length > 0) { await this.saveState(); } } /** * Start heartbeat monitor */ startHeartbeatMonitor() { this.heartbeatTimer = setInterval(() => { this.emit('heartbeat:check'); }, this.options.heartbeatInterval); } /** * Setup file watching */ setupFileWatching() { this.fileWatcher = chokidar.watch([this.sessionFile, this.groupStateFile], { persistent: true, ignoreInitial: true }); this.fileWatcher.on('change', async (filePath) => { await this.loadState(); this.emit('state:updated', { filePath }); }); } /** * Remove session from coordination */ async removeSession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) return; // Remove from group const groupState = this.groupStates.get(session.group); if (groupState) { groupState.sessions = groupState.sessions.filter(id => id !== sessionId); // If this was the primary session, assign new primary if (groupState.primarySession === sessionId && groupState.sessions.length > 0) { groupState.primarySession = groupState.sessions[0]; } // Remove group if no sessions left if (groupState.sessions.length === 0) { this.groupStates.delete(session.group); } } // Release all file locks held by this session for (const [filePath, lock] of this.fileLocks.entries()) { if (lock.sessionId === sessionId) { this.fileLocks.delete(filePath); } } // Remove session this.activeSessions.delete(sessionId); console.log(chalk.yellow(`👋 Session removed: ${sessionId} (${session.group})`)); this.emit('session:removed', { sessionId, session }); } /** * Generate unique message ID */ generateMessageId() { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } module.exports = CoordinationEngine;