UNPKG

overmind-mcp

Version:

Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour

274 lines • 10.5 kB
/** * processRegistry.ts — Persistence layer (disk) + OS-level kill helpers * ===================================================================== * * ONLY handles: * - Read/write ProcessEntry to bridge/process-registry.json * - Background sweeper (cleanupRegistry every 5min) * - OS-level killProcessTree + isPidAlive (Windows tasklist / Unix kill -0) * * Hot-path state (outputBuffer, live status) lives in agent_lifecycle. */ import fs from 'fs/promises'; import path from 'path'; import { spawn } from 'child_process'; import { getWorkspaceDir } from './config.js'; import { rootLogger } from './logger.js'; const logger = rootLogger.child({ module: 'processRegistry' }); const REGISTRY_FILE = 'bridge/process-registry.json'; const PROCESS_TTL_MS = 60 * 60 * 1000; const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // ─── Path helpers ───────────────────────────────────────────────────────────── function getRegistryPath(workspaceDir) { return path.resolve(workspaceDir || getWorkspaceDir(), REGISTRY_FILE); } function buildKey(runner, agentName) { return runner ? `${runner}:${agentName}` : agentName; } // ─── Disk persistence ────────────────────────────────────────────────────────── async function readStore(workspaceDir) { const filePath = getRegistryPath(workspaceDir); try { const content = await fs.readFile(filePath, 'utf-8'); return { store: JSON.parse(content), path: filePath }; } catch { return { store: { processes: {}, version: 2 }, path: filePath }; } } let writePending = false; const pendingWrites = new Map(); // Mutex per file path — prevents concurrent writes to the same file // which was causing earlier writes to be silently dropped. const fileLocks = new Map(); async function writeStore(store, filePath) { // Wait for any in-flight write to the same file before proceeding const existingLock = fileLocks.get(filePath); if (existingLock) await existingLock; pendingWrites.set(filePath, store); if (writePending) return; writePending = true; const lock = (async () => { try { await new Promise((resolve) => { setTimeout(async () => { writePending = false; const entries = new Map(pendingWrites); pendingWrites.clear(); for (const [fPath, s] of entries) { try { await fs.mkdir(path.dirname(fPath), { recursive: true }); const tmp = fPath + '.tmp.' + Math.random().toString(36).slice(2); await fs.writeFile(tmp, JSON.stringify(s, null, 2), 'utf-8'); await fs.rename(tmp, fPath); } catch (e) { logger.error({ fPath, error: e }, '[ProcessRegistry] Write failed.'); } } resolve(); }, 250); }); } finally { fileLocks.delete(filePath); } })(); fileLocks.set(filePath, lock.then(() => { }).catch(() => { })); } // ─── OS-level process utilities ─────────────────────────────────────────────── const pidCache = new Map(); const PID_CACHE_TTL = 2000; export async function isPidAlive(pid) { if (!Number.isFinite(pid) || pid <= 0) return false; const cached = pidCache.get(pid); if (cached && Date.now() - cached.ts < PID_CACHE_TTL) return cached.alive; try { if (process.platform === 'win32') { const result = await new Promise((resolve) => { const child = spawn('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], }); let out = ''; child.stdout.on('data', (d) => { out += d.toString(); }); child.on('close', () => { const trimmed = out.trim(); resolve(!trimmed.includes('No tasks are running') && trimmed.includes(String(pid))); }); child.on('error', () => resolve(false)); }); pidCache.set(pid, { alive: result, ts: Date.now() }); return result; } else { const { execFileSync } = await import('child_process'); try { execFileSync('kill', ['-0', String(pid)], { timeout: 2000 }); pidCache.set(pid, { alive: true, ts: Date.now() }); return true; } catch { pidCache.set(pid, { alive: false, ts: Date.now() }); return false; } } } catch { return false; } } export async function killProcessTree(pid) { if (!Number.isFinite(pid) || pid <= 0) return; try { if (process.platform === 'win32') { await new Promise((resolve) => { const child = spawn('taskkill', ['/F', '/T', '/PID', String(pid)], { windowsHide: true, stdio: 'pipe', }); child.on('close', () => resolve()); child.on('error', () => resolve()); }); } else { await new Promise((resolve) => { const child = spawn('kill', ['-9', String(pid)], { stdio: 'pipe' }); child.on('close', () => resolve()); child.on('error', () => resolve()); }); } pidCache.delete(pid); } catch { // Already dead — fine } } // ─── Registry operations ────────────────────────────────────────────────────── export async function registerProcess(pid, meta) { const { store, path: filePath } = await readStore(meta.configPath); const key = buildKey(meta.runner, meta.agentName); store.processes[key] = { id: '', ts: Date.now(), pid, runner: meta.runner, agentName: meta.agentName, status: 'running', exitCode: null, }; await writeStore(store, filePath); } export async function linkSessionToPid(sessionId, pid, configPath) { const { store, path: filePath } = await readStore(configPath); for (const key of Object.keys(store.processes)) { const entry = store.processes[key]; if (entry.pid === pid) { entry.id = sessionId; entry.ts = Date.now(); store.processes[key] = entry; await writeStore(store, filePath); return; } } } /** appendOutput — NO-OP: agent_lifecycle handles output in RAM */ export async function appendOutput(pid, chunk, configPath) { void pid; void chunk; void configPath; } export async function updateProcessStatus(pid, status, exitCode, configPath) { const { store, path: filePath } = await readStore(configPath); for (const key of Object.keys(store.processes)) { const entry = store.processes[key]; if (entry.pid === pid) { entry.status = status; entry.exitCode = exitCode ?? null; entry.lastOutputAt = Date.now(); store.processes[key] = entry; await writeStore(store, filePath); return; } } } export async function getProcessStatus(agentName, runner, configPath) { const { store } = await readStore(configPath); const key = buildKey(runner, agentName); return store.processes[key] ?? null; } export async function killAgent(agentName, runner, configPath) { const { store, path: filePath } = await readStore(configPath); const key = buildKey(runner, agentName); const entry = store.processes[key]; if (!entry || entry.status !== 'running') return { killed: false }; const pid = entry.pid; if (pid) await killProcessTree(pid); entry.status = 'failed'; entry.exitCode = null; entry.lastOutputAt = Date.now(); store.processes[key] = entry; await writeStore(store, filePath); return { killed: true, pid }; } export async function unregisterProcess(pid, configPath) { const { store, path: filePath } = await readStore(configPath); let changed = false; for (const key of Object.keys(store.processes)) { if (store.processes[key].pid === pid) { delete store.processes[key]; changed = true; } } if (changed) await writeStore(store, filePath); } export async function cleanupRegistry(configPath) { const { store, path: filePath } = await readStore(configPath); const now = Date.now(); let orphaned = 0; let expired = 0; let changed = false; for (const key of Object.keys(store.processes)) { const entry = store.processes[key]; if (entry.status === 'running' && entry.pid) { const alive = await isPidAlive(entry.pid); if (!alive) { entry.status = 'orphaned'; orphaned++; changed = true; } } if (entry.status !== 'running') { const age = now - (entry.lastOutputAt || entry.ts); if (age > PROCESS_TTL_MS) { delete store.processes[key]; expired++; changed = true; } } } if (changed) await writeStore(store, filePath); return { orphaned, expired }; } export async function getRunningProcesses(configPath) { const { store } = await readStore(configPath); return Object.values(store.processes).filter((e) => e.status === 'running'); } export function startAutoCleanup(configPath) { cleanupRegistry(configPath).catch(() => { }); setInterval(() => { cleanupRegistry(configPath).catch(() => { }); }, CLEANUP_INTERVAL_MS); } //# sourceMappingURL=processRegistry.js.map