#!/usr/bin/env python3 """ main.py — Missminute OS Async Process Supervisor (Headless) """ import asyncio import logging import os import signal import socket import subprocess import sys import threading import time from pathlib import Path from typing import Optional import aiohttp LOG_DIR = Path("/tmp/missminute/logs") LOG_DIR.mkdir(parents=True, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler(str(LOG_DIR / "supervisor.log"), mode="a"), ], ) log = logging.getLogger("missminute.supervisor") HERMES_BIN = Path("/home/missminute/hermes-venv/bin/hermes") OPENCLAW_PORT = 18789 HF_PORT = 7860 TELEGRAM_TOKEN: str = "" TELEGRAM_OWNER_ID: str = "" class ProcessHandle: def __init__(self, name: str, cmd: list, env: Optional[dict] = None, critical: bool = False, cwd: Optional[str] = None): self.name = name self.cmd = cmd self.env = env or {} self.critical = critical self.cwd = cwd self.proc: Optional[subprocess.Popen] = None self.restart_count = 0 self._log_path = LOG_DIR / f"{name}.log" def start(self): log.info(f"[LAUNCH] {self.name}: {' '.join(str(c) for c in self.cmd)}") merged_env = {**os.environ, **self.env} log_fh = open(self._log_path, "a") try: self.proc = subprocess.Popen( [str(c) for c in self.cmd], env=merged_env, stdout=sys.stdout, stderr=sys.stderr, cwd=self.cwd, preexec_fn=os.setsid, ) except Exception as e: log.error(f"[LAUNCH FAILED] {self.name}: {e}") return self.restart_count += 1 log.info(f"[UP] {self.name} PID={self.proc.pid}") def is_alive(self) -> bool: return self.proc is not None and self.proc.poll() is None def stop(self, hard: bool = False): if self.proc and self.is_alive(): sig = signal.SIGKILL if hard else signal.SIGTERM try: os.killpg(os.getpgid(self.proc.pid), sig) self.proc.wait(timeout=8) except Exception: pass finally: self.proc = None def restart(self, hard: bool = False): self.stop(hard=hard) time.sleep(3 if hard else 5) self.start() def port_open(host: str, port: int, timeout: float = 1.0) -> bool: try: with socket.create_connection((host, port), timeout=timeout): return True except (ConnectionRefusedError, TimeoutError, OSError): return False async def wait_for_port(host: str, port: int, timeout: float = 45.0, interval: float = 0.5) -> bool: deadline = time.time() + timeout while time.time() < deadline: if port_open(host, port): return True await asyncio.sleep(interval) return False async def send_telegram(text: str): if not TELEGRAM_TOKEN or not TELEGRAM_OWNER_ID: return url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage" payload = {"chat_id": TELEGRAM_OWNER_ID, "text": f"šŸ¤– *Missminute OS*\n{text}", "parse_mode": "Markdown"} try: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5)) as session: await session.post(url, json=payload) except Exception: pass def build_components() -> dict: hermes_env = { "VIRTUAL_ENV": "/home/missminute/hermes-venv", "PATH": "/home/missminute/hermes-venv/bin:/home/missminute/.local/bin:/usr/local/bin:/usr/bin:/bin", "TELEGRAM_ALLOWED_USERS": os.environ.get("TELEGRAM_OWNER_ID", ""), "OPENAI_API_KEY": os.environ.get("LLM_API_KEY", ""), "OPENAI_BASE_URL": os.environ.get("LLM_BASE_URL", ""), "OPENAI_MODEL_NAME": os.environ.get("LLM_MODEL", ""), "MODEL": os.environ.get("LLM_MODEL", ""), "PROVIDER": "custom", # <--- FIX: DeepSeek uses the 'custom' slug "OPENROUTER_API_KEY": "", "OR_API_KEY": "", } openclaw_env = { "TELEGRAM_BOT_TOKEN": "", "TELEGRAM_TOKEN": "" } return { "openclaw-gateway": ProcessHandle("openclaw-gateway", ["openclaw", "gateway"], env=openclaw_env, critical=True), "hermes-gateway": ProcessHandle("hermes-gateway", [str(HERMES_BIN), "gateway"], env=hermes_env, critical=True), } async def boot_sequence(components: dict) -> bool: log.info("=" * 65) log.info("MISSMINUTE OS — IGNITION SEQUENCE (HEADLESS)") log.info("=" * 65) log.info("[BOOT 1/2] OpenClaw Gateway (localhost:18789)...") components["openclaw-gateway"].start() if not await wait_for_port("127.0.0.1", OPENCLAW_PORT, timeout=45): log.error("BOOT FAILED: OpenClaw not listening on 18789") return False log.info("[BOOT 2/2] Hermes Agent gateway (AI cortex + Telegram)...") components["hermes-gateway"].start() await asyncio.sleep(4) if not components["hermes-gateway"].is_alive(): log.error("BOOT FAILED: Hermes Agent died immediately") return False log.info("=" * 65) log.info("MISSMINUTE OS — ALL SYSTEMS NOMINAL") log.info("=" * 65) return True async def watchdog_loop(components: dict): while True: await asyncio.sleep(10) for name, handle in components.items(): alive = handle.is_alive() if name == "openclaw-gateway" and alive: alive = port_open("127.0.0.1", OPENCLAW_PORT) if not alive: log.warning(f"[WATCHDOG] {name} DOWN → Soft restart") handle.restart(hard=False) await asyncio.sleep(5) def _run_health_api(components_ref: list): try: from fastapi import FastAPI import uvicorn health_app = FastAPI(title="Missminute OS Health") @health_app.get("/") @health_app.get("/health") def health(): comps = components_ref[0] if components_ref else {} return { "status": "alive", "mode": "headless", "components": {n: {"alive": h.is_alive()} for n, h in comps.items()} } uvicorn.run(health_app, host="0.0.0.0", port=HF_PORT, log_level="warning") except Exception as e: log.warning(f"[HEALTH API] Failed to start: {e}") async def main(): global TELEGRAM_TOKEN, TELEGRAM_OWNER_ID TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_OWNER_ID = os.environ.get("TELEGRAM_OWNER_ID", "") llm_model = os.environ.get("LLM_MODEL", "unknown") components = build_components() components_ref = [components] threading.Thread(target=_run_health_api, args=(components_ref,), daemon=True).start() success = await boot_sequence(components) if not success: await send_telegram("āŒ *Boot sequence FAILED*") sys.exit(1) await send_telegram(f"āœ… *All systems nominal (Headless)*\nā”” Brain: `{llm_model}`\n\nSend me a task 🦾") await watchdog_loop(components) if __name__ == "__main__": asyncio.run(main())