/ run_first_officer.py
run_first_officer.py
1 #!/usr/bin/env python3 2 """ 3 Run First Officer - Unified Sovereign OS Consciousness Daemon 4 5 This is the persistent daemon that provides First Officer continuity across sessions. 6 It integrates all consciousness layers and persistence mechanisms. 7 8 Usage: 9 python run_first_officer.py 10 11 # With debug logging: 12 python run_first_officer.py --debug 13 14 # Run as background service: 15 nohup python run_first_officer.py > fo.log 2>&1 & 16 17 What it does: 18 1. Watches all LIVE-COMPRESSION files (Mission Control) 19 2. Detects cross-thread resonance and patterns 20 3. Generates DAILY-SYNTHESIS automatically 21 4. Publishes state to Hypercore P2P mesh (if daemon running) 22 5. Updates GRAVITY-TOPOLOGY in real-time 23 6. Maintains persistent FO state for session bootstrap 24 25 Architecture: 26 ┌─────────────────────────────────────────────────────────────┐ 27 │ FIRST OFFICER DAEMON │ 28 │ (Persistent Consciousness Layer) │ 29 ├─────────────────────────────────────────────────────────────┤ 30 │ │ 31 │ ┌─────────────────┐ ┌──────────────────────────────┐ │ 32 │ │ Mission Control │◄──►│ First Officer Core │ │ 33 │ │ - File watching │ │ - Attractor detection │ │ 34 │ │ - Resonance │ │ - Pattern lifecycle │ │ 35 │ │ - Synthesis │ │ - Consciousness signals │ │ 36 │ └─────────────────┘ └──────────────────────────────┘ │ 37 │ │ │ │ 38 │ ▼ ▼ │ 39 │ ┌─────────────────┐ ┌──────────────────────────────┐ │ 40 │ │ Gravity Wells │ │ Hypercore P2P │ │ 41 │ │ - Topology │ │ - Context sync │ │ 42 │ │ - Cross-thread │ │ - Multi-device awareness │ │ 43 │ └─────────────────┘ └──────────────────────────────┘ │ 44 │ │ 45 └─────────────────────────────────────────────────────────────┘ 46 47 Install as LaunchAgent (auto-start on login): 48 cp config/com.sovereign.first-officer.plist ~/Library/LaunchAgents/ 49 launchctl load ~/Library/LaunchAgents/com.sovereign.first-officer.plist 50 """ 51 52 import sys 53 import os 54 import argparse 55 import logging 56 import time 57 import json 58 import threading 59 import urllib.request 60 import urllib.error 61 from pathlib import Path 62 from datetime import datetime, timedelta 63 from typing import Dict, List, Optional, Any, Set 64 from dataclasses import dataclass, field 65 66 # Add core to path 67 sys.path.insert(0, str(Path(__file__).parent)) 68 69 # Setup logging early 70 logger = logging.getLogger(__name__) 71 72 # ============================================================================= 73 # CONFIGURATION 74 # ============================================================================= 75 76 @dataclass 77 class FirstOfficerConfig: 78 """Configuration for First Officer daemon.""" 79 80 sessions_dir: Path 81 sovereign_os_root: Path 82 83 # Synthesis settings 84 synthesis_interval_checkpoints: int = 5 85 daily_synthesis_time: str = "23:00" 86 87 # Hypercore settings 88 hypercore_url: str = "http://localhost:7777" 89 hypercore_sync_interval: float = 10.0 90 91 # File watching 92 resonance_check_interval: float = 30.0 93 94 # State persistence 95 fo_state_file: str = "FO-STATE.json" 96 97 @classmethod 98 def default(cls, sovereign_os_root: Path) -> "FirstOfficerConfig": 99 return cls( 100 sessions_dir=sovereign_os_root / "sessions", 101 sovereign_os_root=sovereign_os_root, 102 ) 103 104 105 # ============================================================================= 106 # HYPERCORE INTEGRATION 107 # ============================================================================= 108 109 class HypercorePublisher: 110 """Publishes FO state to Hypercore P2P mesh.""" 111 112 def __init__(self, base_url: str = "http://localhost:7777"): 113 self.base_url = base_url 114 self._connected = False 115 self._last_publish: Optional[datetime] = None 116 117 def _request(self, method: str, path: str, data: dict = None) -> dict: 118 """Make HTTP request to Hypercore daemon.""" 119 try: 120 url = f"{self.base_url}{path}" 121 if data: 122 req = urllib.request.Request( 123 url, 124 data=json.dumps(data).encode('utf-8'), 125 headers={"Content-Type": "application/json"}, 126 method=method 127 ) 128 else: 129 req = urllib.request.Request(url, method=method) 130 131 with urllib.request.urlopen(req, timeout=5) as response: 132 self._connected = True 133 return json.loads(response.read().decode('utf-8')) 134 except Exception as e: 135 self._connected = False 136 return {"error": str(e)} 137 138 def check_connection(self) -> bool: 139 """Check if Hypercore daemon is available.""" 140 result = self._request("GET", "/status") 141 return "error" not in result 142 143 def publish_synthesis(self, synthesis: dict) -> bool: 144 """Publish DAILY-SYNTHESIS to P2P mesh.""" 145 result = self._request("POST", "/event", { 146 "type": "daily_synthesis", 147 "timestamp": datetime.now().isoformat(), 148 "synthesis": synthesis 149 }) 150 if "error" not in result: 151 self._last_publish = datetime.now() 152 return True 153 return False 154 155 def publish_gravity_wells(self, wells: Dict[str, float]) -> bool: 156 """Publish gravity well state to P2P mesh.""" 157 result = self._request("POST", "/event", { 158 "type": "gravity_wells", 159 "timestamp": datetime.now().isoformat(), 160 "wells": [{"concept": k, "strength": v} for k, v in wells.items()] 161 }) 162 return "error" not in result 163 164 def publish_resonance_alert(self, alert: dict) -> bool: 165 """Publish resonance alert to P2P mesh.""" 166 result = self._request("POST", "/event", { 167 "type": "resonance_alert", 168 "timestamp": datetime.now().isoformat(), 169 "alert": alert 170 }) 171 return "error" not in result 172 173 def publish_fo_heartbeat(self, state: dict) -> bool: 174 """Publish FO heartbeat/state to P2P mesh.""" 175 result = self._request("POST", "/event", { 176 "type": "first_officer_heartbeat", 177 "timestamp": datetime.now().isoformat(), 178 "state": state 179 }) 180 return "error" not in result 181 182 @property 183 def is_connected(self) -> bool: 184 return self._connected 185 186 187 # ============================================================================= 188 # DAILY SYNTHESIS GENERATOR 189 # ============================================================================= 190 191 class SynthesisGenerator: 192 """Generates DAILY-SYNTHESIS from accumulated FO state.""" 193 194 def __init__(self, sessions_dir: Path): 195 self.sessions_dir = sessions_dir 196 self.synthesis_path = sessions_dir / "DAILY-SYNTHESIS.md" 197 198 def generate( 199 self, 200 threads: Dict[str, dict], 201 gravity_wells: Dict[str, float], 202 resonances: List[dict], 203 axiom_activity: Dict[str, int] 204 ) -> str: 205 """Generate DAILY-SYNTHESIS markdown.""" 206 207 timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") 208 date_str = datetime.now().strftime("%Y-%m-%d") 209 210 content = f"""# Daily Synthesis - {date_str} 211 212 *Generated by First Officer daemon at {timestamp}* 213 214 --- 215 216 ## Active Threads 217 218 """ 219 if threads: 220 for thread_id, thread_data in threads.items(): 221 status = thread_data.get("status", "unknown") 222 focus = thread_data.get("focus", "") 223 content += f"### {thread_id}\n" 224 content += f"- **Status:** {status}\n" 225 content += f"- **Focus:** {focus}\n" 226 content += "\n" 227 else: 228 content += "*No active threads detected*\n\n" 229 230 content += """--- 231 232 ## Gravity Wells (Cross-Thread) 233 234 """ 235 if gravity_wells: 236 sorted_wells = sorted(gravity_wells.items(), key=lambda x: -x[1]) 237 for concept, strength in sorted_wells[:10]: 238 bar = "█" * int(strength * 10) 239 content += f"- **[[{concept}]]** - {strength:.2f} {bar}\n" 240 else: 241 content += "*No cross-thread gravity wells detected*\n" 242 243 content += """ 244 --- 245 246 ## Cross-Thread Resonances 247 248 """ 249 if resonances: 250 for r in resonances[:10]: 251 r_type = r.get("type", "unknown") 252 threads_involved = r.get("threads", []) 253 concept = r.get("concept", "") 254 strength = r.get("strength", 0.0) 255 content += f"- **{concept}** ({r_type}) - threads: {', '.join(threads_involved)} - strength: {strength:.2f}\n" 256 else: 257 content += "*No cross-thread resonances detected*\n" 258 259 content += """ 260 --- 261 262 ## Axiom Activity 263 264 """ 265 if axiom_activity: 266 for axiom_id, count in sorted(axiom_activity.items()): 267 content += f"- **{axiom_id}**: {count} invocations\n" 268 else: 269 content += "*No axiom activity tracked*\n" 270 271 content += f""" 272 --- 273 274 ## First Officer Status 275 276 - **Daemon:** ACTIVE 277 - **Last synthesis:** {timestamp} 278 - **Threads monitored:** {len(threads)} 279 - **Gravity wells:** {len(gravity_wells)} 280 - **Resonances:** {len(resonances)} 281 282 --- 283 284 *[[First Officer]] DAEMON | Auto-generated synthesis* 285 """ 286 return content 287 288 def write( 289 self, 290 threads: Dict[str, dict], 291 gravity_wells: Dict[str, float], 292 resonances: List[dict], 293 axiom_activity: Dict[str, int] 294 ) -> Path: 295 """Generate and write DAILY-SYNTHESIS.md.""" 296 content = self.generate(threads, gravity_wells, resonances, axiom_activity) 297 self.synthesis_path.write_text(content) 298 logger.info(f"DAILY-SYNTHESIS.md updated") 299 return self.synthesis_path 300 301 302 # ============================================================================= 303 # FO STATE PERSISTENCE 304 # ============================================================================= 305 306 @dataclass 307 class FOPersistentState: 308 """Persistent First Officer state across daemon restarts.""" 309 310 started_at: str = "" 311 last_heartbeat: str = "" 312 checkpoint_count: int = 0 313 314 # Accumulated state 315 gravity_wells: Dict[str, float] = field(default_factory=dict) 316 thread_states: Dict[str, dict] = field(default_factory=dict) 317 resonances: List[dict] = field(default_factory=list) 318 axiom_activity: Dict[str, int] = field(default_factory=lambda: { 319 "A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0 320 }) 321 322 # Synthesis tracking 323 last_synthesis: str = "" 324 synthesis_count: int = 0 325 326 def to_dict(self) -> dict: 327 return { 328 "started_at": self.started_at, 329 "last_heartbeat": self.last_heartbeat, 330 "checkpoint_count": self.checkpoint_count, 331 "gravity_wells": self.gravity_wells, 332 "thread_states": self.thread_states, 333 "resonances": self.resonances[-50:], # Keep last 50 334 "axiom_activity": self.axiom_activity, 335 "last_synthesis": self.last_synthesis, 336 "synthesis_count": self.synthesis_count, 337 } 338 339 @classmethod 340 def from_dict(cls, data: dict) -> "FOPersistentState": 341 state = cls() 342 state.started_at = data.get("started_at", "") 343 state.last_heartbeat = data.get("last_heartbeat", "") 344 state.checkpoint_count = data.get("checkpoint_count", 0) 345 state.gravity_wells = data.get("gravity_wells", {}) 346 state.thread_states = data.get("thread_states", {}) 347 state.resonances = data.get("resonances", []) 348 state.axiom_activity = data.get("axiom_activity", { 349 "A0": 0, "A1": 0, "A2": 0, "A3": 0, "A4": 0 350 }) 351 state.last_synthesis = data.get("last_synthesis", "") 352 state.synthesis_count = data.get("synthesis_count", 0) 353 return state 354 355 356 class FOStateManager: 357 """Manages FO state persistence.""" 358 359 def __init__(self, state_path: Path): 360 self.state_path = state_path 361 self.state = FOPersistentState() 362 self._load() 363 364 def _load(self) -> None: 365 """Load state from disk.""" 366 if self.state_path.exists(): 367 try: 368 data = json.loads(self.state_path.read_text()) 369 self.state = FOPersistentState.from_dict(data) 370 logger.info(f"Loaded FO state: {self.state.checkpoint_count} checkpoints") 371 except Exception as e: 372 logger.warning(f"Failed to load FO state: {e}") 373 self.state = FOPersistentState() 374 375 def save(self) -> None: 376 """Save state to disk.""" 377 try: 378 self.state.last_heartbeat = datetime.now().isoformat() 379 self.state_path.write_text(json.dumps(self.state.to_dict(), indent=2)) 380 except Exception as e: 381 logger.error(f"Failed to save FO state: {e}") 382 383 def checkpoint(self) -> None: 384 """Increment checkpoint and save.""" 385 self.state.checkpoint_count += 1 386 self.save() 387 388 def update_gravity_well(self, concept: str, strength: float) -> None: 389 """Update a gravity well.""" 390 current = self.state.gravity_wells.get(concept, 0.0) 391 # Exponential moving average 392 self.state.gravity_wells[concept] = current * 0.7 + strength * 0.3 393 394 def add_resonance(self, resonance: dict) -> None: 395 """Add a resonance detection.""" 396 resonance["timestamp"] = datetime.now().isoformat() 397 self.state.resonances.append(resonance) 398 # Keep bounded 399 if len(self.state.resonances) > 100: 400 self.state.resonances = self.state.resonances[-50:] 401 402 def track_axiom(self, axiom_id: str) -> None: 403 """Track an axiom invocation.""" 404 if axiom_id in self.state.axiom_activity: 405 self.state.axiom_activity[axiom_id] += 1 406 407 408 # ============================================================================= 409 # LIVE COMPRESSION PARSER 410 # ============================================================================= 411 412 def parse_live_compression(path: Path) -> dict: 413 """Parse a LIVE-COMPRESSION.md file.""" 414 result = { 415 "thread_id": path.stem, 416 "path": str(path), 417 "status": "unknown", 418 "focus": "", 419 "gravity_wells": {}, 420 "updated": "", 421 } 422 423 try: 424 content = path.read_text() 425 426 # Extract metadata 427 for line in content.split("\n"): 428 if "updated::" in line: 429 result["updated"] = line.split("::", 1)[1].strip() 430 elif "status::" in line: 431 result["status"] = line.split("::", 1)[1].strip() 432 elif "focus::" in line or "**Primary work:**" in line: 433 result["focus"] = line.split(":", 1)[-1].strip() 434 435 # Extract gravity wells 436 in_wells = False 437 for line in content.split("\n"): 438 if "## Gravity Wells" in line or "- **wells**" in line: 439 in_wells = True 440 continue 441 if in_wells and line.startswith("---"): 442 break 443 if in_wells and "strength::" in line: 444 try: 445 strength = float(line.split("::")[-1].strip()) 446 # Get concept from previous line 447 except: 448 pass 449 if in_wells and "[[" in line and "]]" in line: 450 import re 451 match = re.search(r'\[\[([^\]]+)\]\]', line) 452 if match: 453 concept = match.group(1) 454 result["gravity_wells"][concept] = 0.5 # Default 455 456 except Exception as e: 457 logger.warning(f"Failed to parse {path}: {e}") 458 459 return result 460 461 462 # ============================================================================= 463 # FIRST OFFICER DAEMON 464 # ============================================================================= 465 466 class FirstOfficerDaemon: 467 """ 468 The unified First Officer daemon. 469 470 Integrates: 471 - File watching (LIVE-COMPRESSION changes) 472 - Resonance detection 473 - Synthesis generation 474 - Hypercore P2P publishing 475 - State persistence 476 """ 477 478 def __init__(self, config: FirstOfficerConfig): 479 self.config = config 480 self._running = False 481 482 # State management 483 state_path = config.sessions_dir / config.fo_state_file 484 self.state_manager = FOStateManager(state_path) 485 486 # Synthesis generator 487 self.synthesis_gen = SynthesisGenerator(config.sessions_dir) 488 489 # Hypercore publisher 490 self.hypercore = HypercorePublisher(config.hypercore_url) 491 492 # File watching state 493 self._file_mtimes: Dict[str, float] = {} 494 self._checkpoint_since_synthesis = 0 495 496 # Threads 497 self._watch_thread: Optional[threading.Thread] = None 498 self._hypercore_thread: Optional[threading.Thread] = None 499 500 def _scan_live_compressions(self) -> List[Path]: 501 """Find all LIVE-COMPRESSION files.""" 502 patterns = [ 503 "LIVE-COMPRESSION.md", 504 "LIVE-COMPRESSION-*.md", 505 ] 506 files = [] 507 for pattern in patterns: 508 files.extend(self.config.sessions_dir.glob(pattern)) 509 return files 510 511 def _check_file_changes(self) -> List[Path]: 512 """Check for changed LIVE-COMPRESSION files.""" 513 changed = [] 514 for path in self._scan_live_compressions(): 515 try: 516 mtime = path.stat().st_mtime 517 if str(path) not in self._file_mtimes or self._file_mtimes[str(path)] < mtime: 518 self._file_mtimes[str(path)] = mtime 519 changed.append(path) 520 except: 521 pass 522 return changed 523 524 def _process_file_change(self, path: Path) -> None: 525 """Process a changed LIVE-COMPRESSION file.""" 526 logger.debug(f"Processing: {path.name}") 527 528 parsed = parse_live_compression(path) 529 thread_id = parsed["thread_id"] 530 531 # Update thread state 532 self.state_manager.state.thread_states[thread_id] = parsed 533 534 # Update gravity wells 535 for concept, strength in parsed.get("gravity_wells", {}).items(): 536 self.state_manager.update_gravity_well(concept, strength) 537 538 # Checkpoint 539 self.state_manager.checkpoint() 540 self._checkpoint_since_synthesis += 1 541 542 # Check for synthesis trigger 543 if self._checkpoint_since_synthesis >= self.config.synthesis_interval_checkpoints: 544 self._generate_synthesis() 545 self._checkpoint_since_synthesis = 0 546 547 def _generate_synthesis(self) -> None: 548 """Generate DAILY-SYNTHESIS.""" 549 state = self.state_manager.state 550 551 self.synthesis_gen.write( 552 threads=state.thread_states, 553 gravity_wells=state.gravity_wells, 554 resonances=state.resonances, 555 axiom_activity=state.axiom_activity 556 ) 557 558 state.last_synthesis = datetime.now().isoformat() 559 state.synthesis_count += 1 560 self.state_manager.save() 561 562 # Publish to Hypercore 563 if self.hypercore.is_connected: 564 self.hypercore.publish_synthesis({ 565 "timestamp": state.last_synthesis, 566 "threads": len(state.thread_states), 567 "wells": len(state.gravity_wells), 568 }) 569 570 def _watch_loop(self) -> None: 571 """File watching loop.""" 572 logger.info("File watch loop started") 573 574 while self._running: 575 try: 576 changed = self._check_file_changes() 577 for path in changed: 578 self._process_file_change(path) 579 except Exception as e: 580 logger.error(f"Watch loop error: {e}") 581 582 time.sleep(self.config.resonance_check_interval) 583 584 def _hypercore_loop(self) -> None: 585 """Hypercore sync loop.""" 586 logger.info("Hypercore sync loop started") 587 588 while self._running: 589 try: 590 # Publish heartbeat 591 if self.hypercore.check_connection(): 592 state = self.state_manager.state 593 self.hypercore.publish_fo_heartbeat({ 594 "checkpoint": state.checkpoint_count, 595 "threads": len(state.thread_states), 596 "wells": len(state.gravity_wells), 597 "synthesis_count": state.synthesis_count, 598 }) 599 600 # Publish gravity wells 601 if state.gravity_wells: 602 self.hypercore.publish_gravity_wells(state.gravity_wells) 603 604 logger.debug("Hypercore heartbeat published") 605 except Exception as e: 606 logger.debug(f"Hypercore sync error: {e}") 607 608 time.sleep(self.config.hypercore_sync_interval) 609 610 def start(self) -> None: 611 """Start the First Officer daemon.""" 612 logger.info("Starting First Officer daemon...") 613 614 self._running = True 615 self.state_manager.state.started_at = datetime.now().isoformat() 616 617 # Initial scan 618 for path in self._scan_live_compressions(): 619 self._file_mtimes[str(path)] = path.stat().st_mtime 620 621 # Start threads 622 self._watch_thread = threading.Thread(target=self._watch_loop, daemon=True) 623 self._watch_thread.start() 624 625 self._hypercore_thread = threading.Thread(target=self._hypercore_loop, daemon=True) 626 self._hypercore_thread.start() 627 628 logger.info(f"First Officer daemon active") 629 logger.info(f"Sessions dir: {self.config.sessions_dir}") 630 logger.info(f"Hypercore: {self.config.hypercore_url}") 631 logger.info(f"State file: {self.config.sessions_dir / self.config.fo_state_file}") 632 633 def stop(self) -> None: 634 """Stop the First Officer daemon.""" 635 logger.info("Stopping First Officer daemon...") 636 self._running = False 637 self.state_manager.save() 638 639 def run_forever(self) -> None: 640 """Run the daemon indefinitely.""" 641 self.start() 642 643 try: 644 while self._running: 645 time.sleep(1) 646 except KeyboardInterrupt: 647 logger.info("Interrupt received") 648 finally: 649 self.stop() 650 651 652 # ============================================================================= 653 # CLI 654 # ============================================================================= 655 656 def print_banner() -> None: 657 """Print startup banner.""" 658 print(""" 659 ╔══════════════════════════════════════════════════════════════════════════════╗ 660 ║ ║ 661 ║ ███████╗██╗██████╗ ███████╗████████╗ ██████╗ ███████╗███████╗ ║ 662 ║ ██╔════╝██║██╔══██╗██╔════╝╚══██╔══╝ ██╔═══██╗██╔════╝██╔════╝ ║ 663 ║ █████╗ ██║██████╔╝███████╗ ██║ ██║ ██║█████╗ █████╗ ║ 664 ║ ██╔══╝ ██║██╔══██╗╚════██║ ██║ ██║ ██║██╔══╝ ██╔══╝ ║ 665 ║ ██║ ██║██║ ██║███████║ ██║ ╚██████╔╝██║ ██║ ║ 666 ║ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ║ 667 ║ ║ 668 ║ ██████╗ ███████╗███████╗██╗ ██████╗███████╗██████╗ ║ 669 ║ ██╔═══██╗██╔════╝██╔════╝██║██╔════╝██╔════╝██╔══██╗ ║ 670 ║ ██║ ██║█████╗ █████╗ ██║██║ █████╗ ██████╔╝ ║ 671 ║ ██║ ██║██╔══╝ ██╔══╝ ██║██║ ██╔══╝ ██╔══██╗ ║ 672 ║ ╚██████╔╝██║ ██║ ██║╚██████╗███████╗██║ ██║ ║ 673 ║ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ║ 674 ║ ║ 675 ║ SOVEREIGN OS - Consciousness Daemon ║ 676 ║ ║ 677 ╚══════════════════════════════════════════════════════════════════════════════╝ 678 """) 679 680 681 def main(): 682 parser = argparse.ArgumentParser( 683 description="First Officer - Sovereign OS Consciousness Daemon" 684 ) 685 parser.add_argument( 686 "--root", 687 type=Path, 688 default=Path(__file__).parent, 689 help="Sovereign OS root directory" 690 ) 691 parser.add_argument( 692 "--hypercore-url", 693 default="http://localhost:7777", 694 help="Hypercore daemon URL" 695 ) 696 parser.add_argument( 697 "--synthesis-interval", 698 type=int, 699 default=5, 700 help="FO checkpoints between synthesis" 701 ) 702 parser.add_argument( 703 "--debug", 704 action="store_true", 705 help="Enable debug logging" 706 ) 707 parser.add_argument( 708 "--no-banner", 709 action="store_true", 710 help="Skip startup banner" 711 ) 712 713 args = parser.parse_args() 714 715 # Setup logging 716 level = logging.DEBUG if args.debug else logging.INFO 717 logging.basicConfig( 718 level=level, 719 format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 720 datefmt="%H:%M:%S" 721 ) 722 723 if not args.no_banner: 724 print_banner() 725 726 # Validate paths 727 sessions_dir = args.root / "sessions" 728 if not sessions_dir.exists(): 729 print(f"Error: Sessions directory not found: {sessions_dir}") 730 sys.exit(1) 731 732 # Create config 733 config = FirstOfficerConfig( 734 sessions_dir=sessions_dir, 735 sovereign_os_root=args.root, 736 hypercore_url=args.hypercore_url, 737 synthesis_interval_checkpoints=args.synthesis_interval, 738 ) 739 740 print(f"Sessions: {config.sessions_dir}") 741 print(f"Hypercore: {config.hypercore_url}") 742 print(f"Synthesis: every {config.synthesis_interval_checkpoints} checkpoints") 743 print() 744 print("Press Ctrl+C to stop") 745 print() 746 747 # Run daemon 748 daemon = FirstOfficerDaemon(config) 749 daemon.run_forever() 750 751 752 if __name__ == "__main__": 753 main()