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
JavaScript
/**
* 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;