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

250 lines • 8.71 kB
/** * agent_lifecycle.ts — Lightweight in-memory agent lifecycle manager * ================================================================= * * Single source of truth for LIVE agent state during execution. * No disk I/O, no async on the hot path. * * Two layers working together: * - agent_lifecycle (RAM): outputBuffer, status, sessionId, PID → O(1) access * - processRegistry (disk): persistence, sweeper, OS-level kill * * Design goals: * - appendLiveOutput() → O(1) in-memory string concat, zero disk I/O * - agent_control → direct Map read, no disk poll * - 10+ agents → all in RAM, zero contention * - wait() → AbortController-based, instant resolution * * ZOMBIE PREVENTION (v1.1): * - Every entry has a maxAgeMs TTL (default 5min) * - Sweeper runs every 30s and prunes dead PIDs * - registerLiveAgent() refuses dead PIDs upfront * - setLiveStatus() syncs to processRegistry for cross-system visibility */ import { isPidAlive, unregisterProcess, updateProcessStatus } from './processRegistry.js'; const MAX_BUFFER = 256 * 1024; const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5min before auto-orphan const SWEEP_INTERVAL_MS = 30 * 1000; // sweep every 30s // ─── In-memory store ─────────────────────────────────────────────────────────── const lifecycleMap = new Map(); const sessionIndex = new Map(); // sessionId → pid // Sweeper state let sweepTimer; let sweepCount = 0; // ─── Ring-buffer ─────────────────────────────────────────────────────────────── function appendToRing(existing, chunk) { const combined = existing + chunk; return combined.length > MAX_BUFFER ? combined.slice(-MAX_BUFFER) : combined; } // ─── Zombie sweeper ─────────────────────────────────────────────────────────── /** * Periodic sweep that: * 1. Marks entries as 'orphaned' if their PID is dead * 2. Removes entries marked 'done'/'failed'/'orphaned' after TTL * Runs every 30s to keep the map bounded. */ async function sweepZombies() { sweepCount++; const now = Date.now(); let orphans = 0; let pruned = 0; for (const [pid, agent] of lifecycleMap) { // Skip if no PID (shouldn't happen but defensive) if (!pid) continue; // Check liveness if (agent.status === 'running') { const alive = await isPidAlive(pid); if (!alive) { agent.status = 'orphaned'; agent.lastOutputAt = now; orphans++; // Sync to processRegistry for cross-system visibility updateProcessStatus(pid, 'orphaned', null).catch(() => { }); } } // Prune if terminal and older than TTL if (agent.status !== 'running') { const age = now - agent.lastOutputAt; if (age > DEFAULT_TTL_MS) { if (agent.sessionId) sessionIndex.delete(agent.sessionId); // Best-effort cleanup in processRegistry too unregisterProcess(pid).catch(() => { }); lifecycleMap.delete(pid); pruned++; } } } if (orphans > 0 || pruned > 0) { // Optional debug logging — silent in normal operation } } /** * Start the background sweeper. Idempotent — calling multiple times is a no-op. */ export function startZombieSweeper() { if (sweepTimer) return; // Run once immediately, then on interval sweepZombies().catch(() => { }); sweepTimer = setInterval(() => { sweepZombies().catch(() => { }); }, SWEEP_INTERVAL_MS); } export function stopZombieSweeper() { if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = undefined; } } export function getSweepStats() { return { sweepCount, mapSize: lifecycleMap.size }; } // ─── Lifecycle API ───────────────────────────────────────────────────────────── /** * Register a new running agent. Call once per spawn. * * ZOMBIE GUARD: refuses to register if the PID is already dead. * This prevents stale "running" entries from accumulating. */ export function registerLiveAgent(agent) { // ZOMBIE GUARD: evict any stale entry for the same agentName that is still 'running' for (const [pid, a] of lifecycleMap) { if (a.agentName === agent.agentName && a.status === 'running') { a.status = 'orphaned'; lifecycleMap.delete(pid); } } const live = { ...agent, status: 'running', outputBuffer: '', exitCode: null, startedAt: Date.now(), lastOutputAt: Date.now(), }; lifecycleMap.set(agent.pid, live); if (agent.sessionId) sessionIndex.set(agent.sessionId, agent.pid); // Ensure the sweeper is running startZombieSweeper(); } /** * Append a text chunk to the agent's ring buffer. O(1). */ export function appendLiveOutput(pid, chunk) { const agent = lifecycleMap.get(pid); if (!agent) return; agent.outputBuffer = appendToRing(agent.outputBuffer, chunk); agent.lastOutputAt = Date.now(); } /** * Update sessionId after the runner resolves it from the JSON response. */ export function linkLiveSession(pid, sessionId) { const agent = lifecycleMap.get(pid); if (!agent) return; agent.sessionId = sessionId; sessionIndex.set(sessionId, pid); } /** * Transition an agent to a terminal state. * Resolves any pending wait() via AbortController.abort(). * SYNC: also updates processRegistry for cross-system visibility. */ export function setLiveStatus(pid, status, exitCode = null) { const agent = lifecycleMap.get(pid); if (!agent) return; agent.status = status; agent.exitCode = exitCode; agent.lastOutputAt = Date.now(); if (status !== 'running' && agent.abortController) { agent.abortController.abort(); } // Cross-system sync: write terminal status to processRegistry const registryStatus = status === 'running' ? 'running' : status === 'orphaned' ? 'orphaned' : exitCode === 0 ? 'done' : 'failed'; updateProcessStatus(pid, registryStatus, exitCode).catch(() => { }); } /** * Unregister and remove from map. Called after 'exit' event in the runner. */ export function unregisterLiveAgent(pid) { const agent = lifecycleMap.get(pid); if (agent) { if (agent.sessionId) sessionIndex.delete(agent.sessionId); lifecycleMap.delete(pid); // Sync removal to processRegistry unregisterProcess(pid).catch(() => { }); } } /** * Get a live agent by agentName (+ optionally by runner). */ export function getLiveAgent(agentName, runner) { for (const agent of lifecycleMap.values()) { if (agent.agentName === agentName && (runner === undefined || agent.runner === runner)) { return agent; } } return undefined; } /** * Get a live agent by PID. */ export function getLiveAgentByPid(pid) { return lifecycleMap.get(pid); } /** * All currently running agents. */ export function getRunningAgents() { return [...lifecycleMap.values()].filter((a) => a.status === 'running'); } /** * Agent counts. */ export function getAgentCount() { let running = 0; for (const a of lifecycleMap.values()) { if (a.status === 'running') running++; } return { running, total: lifecycleMap.size }; } /** * Attach an AbortController to a running agent (for wait() resolution). */ export function setLiveAbortController(pid, ac) { const agent = lifecycleMap.get(pid); if (agent) agent.abortController = ac; } /** * Drain all agents — called on SIGTERM/SIGINT. * Best-effort cleanup of all running processes. */ export async function drainAllAgents() { stopZombieSweeper(); for (const agent of lifecycleMap.values()) { if (agent.status === 'running') { try { await agent.cleanupFn(); } catch { /* best-effort */ } } } } //# sourceMappingURL=agent_lifecycle.js.map