| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import fs from "node:fs/promises"; |
| import path from "node:path"; |
| import { writeJsonAtomic } from "../infra/json-files.js"; |
| import { |
| OMEGA_DEFAULT_AUTONOMOUS_INTERVAL_MINUTES, |
| OMEGA_INTERACTION_LOCK_REFRESH_MS, |
| OMEGA_INTERACTION_LOCK_TIMEOUT_MS, |
| } from "./autonomous-runtime.js"; |
|
|
| const INTERACTION_LOCK_FILE = ".interaction-lock"; |
|
|
| export interface DaemonParams { |
| workspaceRoot: string; |
| sessionKey: string; |
| signal?: AbortSignal; |
| } |
|
|
| export interface DaemonDeps { |
| checkInteractionLock?: (path: string) => Promise<boolean>; |
| runHeartbeatCycle?: (params: { workspaceRoot: string; sessionKey: string }) => Promise<void>; |
| sleep?: (ms: number, signal?: AbortSignal) => Promise<void>; |
| log?: (...args: any[]) => void; |
| error?: (...args: any[]) => void; |
| } |
|
|
| type InteractionLockPayload = { |
| owner: "tui"; |
| pid: number; |
| refreshedAt: number; |
| }; |
|
|
| async function writeInteractionLock(workspaceRoot: string): Promise<void> { |
| const lockFilePath = path.join(workspaceRoot, INTERACTION_LOCK_FILE); |
| const payload: InteractionLockPayload = { |
| owner: "tui", |
| pid: process.pid, |
| refreshedAt: Date.now(), |
| }; |
| await writeJsonAtomic(lockFilePath, payload, { trailingNewline: true }); |
| } |
|
|
| export async function startAutonomousDaemon(params: DaemonParams, deps: DaemonDeps = {}) { |
| const lockFilePath = path.join(params.workspaceRoot, INTERACTION_LOCK_FILE); |
| const log = deps.log ?? console.log; |
| const errorLog = deps.error ?? console.error; |
|
|
| log("[DAEMON] 🚀 Iniciando OpenSkyNet Autonomous Daemon"); |
| log(`[DAEMON] Session: ${params.sessionKey}`); |
| log(`[DAEMON] Lock file: ${lockFilePath}`); |
| log(`[DAEMON] Presiona Ctrl+C para detener`); |
| log(""); |
|
|
| const wrappedLoop = async () => { |
| let cycleCount = 0; |
|
|
| while (!params.signal?.aborted) { |
| cycleCount++; |
|
|
| const isInteracting = deps.checkInteractionLock |
| ? await deps.checkInteractionLock(lockFilePath) |
| : await checkInteractionLock(lockFilePath); |
|
|
| if (isInteracting) { |
| log(`[DAEMON] 🧑 Interacción detectada. Daemon en pausa (ciclo ${cycleCount})...`); |
| const sleepFn = deps.sleep ?? sleep; |
| await sleepFn(OMEGA_INTERACTION_LOCK_REFRESH_MS, params.signal); |
| continue; |
| } |
|
|
| log(`[DAEMON] 🤖 Ciclo #${cycleCount} - Daemon activo`); |
| try { |
| if (deps.runHeartbeatCycle) { |
| await deps.runHeartbeatCycle({ |
| workspaceRoot: params.workspaceRoot, |
| sessionKey: params.sessionKey, |
| }); |
| } else { |
| const { runOneHeartbeatCycle } = await import("./heartbeat.js"); |
| await runOneHeartbeatCycle({ |
| workspaceRoot: params.workspaceRoot, |
| sessionKey: params.sessionKey, |
| }); |
| } |
| log(`[DAEMON] ✓ Ciclo ${cycleCount} completado`); |
| } catch (error) { |
| errorLog(`[DAEMON] ❌ Error en ciclo ${cycleCount}:`, error); |
| } |
|
|
| if (params.signal?.aborted) break; |
|
|
| log( |
| `[DAEMON] ⏰ Próximo ciclo en ${OMEGA_DEFAULT_AUTONOMOUS_INTERVAL_MINUTES} min (o si termina TUI)...`, |
| ); |
| const sleepFn = deps.sleep ?? sleep; |
| await sleepFn(OMEGA_DEFAULT_AUTONOMOUS_INTERVAL_MINUTES * 60 * 1000, params.signal); |
| } |
| }; |
|
|
| try { |
| await wrappedLoop(); |
| } catch (error) { |
| if (params.signal?.aborted) { |
| log("[DAEMON] 🛑 Detenido por señal de aborto."); |
| } else { |
| throw error; |
| } |
| } |
| } |
|
|
| |
| |
| |
| export async function checkInteractionLock(lockFilePath: string): Promise<boolean> { |
| try { |
| const stat = await fs.stat(lockFilePath); |
| const ageMs = Date.now() - stat.mtimeMs; |
|
|
| |
| if (ageMs < OMEGA_INTERACTION_LOCK_TIMEOUT_MS) { |
| |
| return true; |
| } |
|
|
| |
| await fs.unlink(lockFilePath); |
| return false; |
| } catch { |
| |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| export async function createInteractionLock(workspaceRoot: string): Promise<void> { |
| await writeInteractionLock(workspaceRoot); |
| console.log("[TUI] 🔒 Lock creado - Daemon pausado"); |
| } |
|
|
| |
| |
| |
| export async function releaseInteractionLock(workspaceRoot: string): Promise<void> { |
| const lockFilePath = path.join(workspaceRoot, INTERACTION_LOCK_FILE); |
| try { |
| await fs.unlink(lockFilePath); |
| console.log("[TUI] 🔓 Lock liberado - Daemon reanuda"); |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| export async function refreshInteractionLock( |
| workspaceRoot: string, |
| intervalMs = OMEGA_INTERACTION_LOCK_REFRESH_MS, |
| ): Promise<() => Promise<void>> { |
| const lockFilePath = path.join(workspaceRoot, INTERACTION_LOCK_FILE); |
|
|
| |
| await createInteractionLock(workspaceRoot); |
|
|
| |
| const interval = setInterval(async () => { |
| try { |
| await writeInteractionLock(workspaceRoot); |
| } catch { |
| |
| } |
| }, intervalMs); |
|
|
| |
| return async () => { |
| clearInterval(interval); |
| await releaseInteractionLock(workspaceRoot); |
| }; |
| } |
|
|
| |
| |
| |
| export function sleep(ms: number, signal?: AbortSignal): Promise<void> { |
| return new Promise((resolve, reject) => { |
| if (signal?.aborted) return reject(signal.reason); |
|
|
| const timeout = setTimeout(resolve, ms); |
|
|
| if (signal) { |
| signal.addEventListener( |
| "abort", |
| () => { |
| clearTimeout(timeout); |
| reject(signal.reason); |
| }, |
| { once: true }, |
| ); |
| } |
| }); |
| } |
|
|