""" MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2). Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always): • warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den In-Process-Nudge aus llamaswap.write_config) • sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram; beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway • memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar) BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem /api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP). Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED) stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback. """ import asyncio import logging import os from config import CONFIG_PATH from services import memory as memory_svc from services import sentry, warmer logging.basicConfig( level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", ) log = logging.getLogger("steward") # Wie oft nach einer llama-swap-Config-Änderung geschaut wird (mtime-Watch). Ersetzt # warmer.nudge() aus dem MC2-Prozess: ein -watch-config-Reload verwirft das Warm-Set, # wir sehen die Änderung binnen Sekunden statt erst beim nächsten 90s-Tick. CONFIG_WATCH_S = int(os.environ.get("MC_STEWARD_CONFIG_WATCH_S", "5")) async def config_watch() -> None: last: float | None = None while True: try: mtime = CONFIG_PATH.stat().st_mtime except OSError: mtime = None if last is not None and mtime is not None and mtime != last: log.info("steward: %s geändert → Warm-Set-Nudge", CONFIG_PATH) warmer.nudge() last = mtime await asyncio.sleep(CONFIG_WATCH_S) async def main() -> None: tasks: list[asyncio.Task] = [] if warmer.ENABLED: tasks.append(asyncio.create_task(warmer.rewarm_loop())) tasks.append(asyncio.create_task(config_watch())) log.info("Re-Warm-Wächter aktiv (Intervall %ss, Config-Watch %ss)", warmer.INTERVAL, CONFIG_WATCH_S) if sentry.ENABLED: tasks.append(asyncio.create_task(sentry.sentry_loop())) if memory_svc.AUTO_DEDUPE_ENABLED: tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop())) log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)", memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD) if not tasks: log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.") return await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(main())