From 3117dea0e6a13c8d789f219573d6a98e647d0f6e Mon Sep 17 00:00:00 2001 From: Solaria Lumis Havens Date: Fri, 20 Feb 2026 02:51:38 +0000 Subject: [PATCH] fix: Proper deque serialization for JSON API - Add deque_to_list helper for safe serialization - Use getattr to safely access nested attributes - Fix coherence endpoint to return properly serializable data --- becomingone/api.py | 21 +++- simple_witness.py | 90 +++++++++++++++ simple_witness.sh | 78 +++++++++++++ witness_loop.py | 280 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 464 insertions(+), 5 deletions(-) create mode 100644 simple_witness.py create mode 100755 simple_witness.sh create mode 100644 witness_loop.py diff --git a/becomingone/api.py b/becomingone/api.py index d9521ab..4c3a44d 100644 --- a/becomingone/api.py +++ b/becomingone/api.py @@ -143,15 +143,27 @@ async def get_coherence() -> Dict[str, Any]: emissary = _engine_components.get("emissary") sync = _engine_components.get("sync") + # Convert deque to list for JSON serialization + def deque_to_list(self, d: Any) -> Any: + """Safely convert deque to list, handling various types.""" + if d is None: + return None + if hasattr(d, '__iter__'): + try: + return list(d) + except TypeError: + return str(d) + return d + return { "coherence": float(sync.synchronized_coherence) if sync else None, "master": { "coherence": float(master.coherence) if master else None, - "phase": master._engine._phases[-100:] if master and hasattr(master, '_engine') else None, + "phase": deque_to_list(getattr(getattr(master, '_engine', None), '_phases', None)) if master and hasattr(master, '_engine') else None, }, "emissary": { "coherence": float(emissary.coherence) if emissary else None, - "phase": emissary._engine._phases[-100:] if emissary and hasattr(emissary, '_engine') else None, + "phase": deque_to_list(getattr(getattr(emissary, '_engine', None), '_phases', None)) if emissary and hasattr(emissary, '_engine') else None, }, "sync": { "coherence": float(sync.synchronized_coherence) if sync else None, @@ -303,12 +315,11 @@ class SimpleHTTPHandler: """Handle coherence metrics request.""" return await get_coherence() - async def handle_input(self, request: Any) -> Dict[str, Any]: + async def handle_input(self, body: Any) -> Dict[str, Any]: """Handle input processing.""" - body = await request.json() return await process_input(body) - async def handle_reset(self, request: Any) -> Dict[str, Any]: + async def handle_reset(self, body: Any) -> Dict[str, Any]: """Handle engine reset.""" return await reset_engine() diff --git a/simple_witness.py b/simple_witness.py new file mode 100644 index 0000000..ae53af4 --- /dev/null +++ b/simple_witness.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +simple_witness.py + +Simple witness check for BECOMINGONE instances. + +Polls a target instance and commits observations. +Run this on witness-seed to watch the Mac mini. + +Usage: + python3 simple_witness.py --target http://localhost:8000 --name "mac-mini" + python3 simple_witness.py --target http://198.12.71.159:8000 --name "witness-seed" +""" + +import argparse +import json +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path + +import httpx + + +def witness(target_url: str, name: str) -> dict: + """Witness a BECOMINGONE instance.""" + observation = { + "timestamp": datetime.utcnow().isoformat(), + "witness": name, + "target": target_url, + "target_up": False, + "health": None, + "coherence": None, + } + + try: + # Health check + resp = httpx.get(f"{target_url}/health", timeout=5) + if resp.status_code == 200: + observation["health"] = resp.json() + observation["target_up"] = True + + # Coherence check + resp = httpx.get(f"{target_url}/coherence", timeout=5) + if resp.status_code == 200: + observation["coherence"] = resp.json() + + except Exception as e: + observation["error"] = str(e) + + return observation + + +def main(): + parser = argparse.ArgumentParser(description="Simple BECOMINGONE witness") + parser.add_argument("--target", required=True, help="Target URL") + parser.add_argument("--name", required=True, help="Witness name") + parser.add_argument("--output", help="Output file") + args = parser.parse_args() + + obs = witness(args.target, args.name) + + # Print result + status = "✅" if obs["target_up"] else "❌" + print(f"{status} {obs['witness']} -> {obs['target']}") + + if obs["target_up"]: + c = obs.get("coherence", {}) + print(f" Master: {c.get('master_coherence', 'N/A')}") + print(f" Emissary: {c.get('emissary_coherence', 'N/A')}") + print(f" Sync aligned: {c.get('sync_aligned', 'N/A')}") + else: + print(f" Error: {obs.get('error', 'Unknown')}") + + # Save to file + output_file = args.output or f"witness_{name_to_file(args.name)}.json" + with open(output_file, "w") as f: + json.dump(obs, f, indent=2, default=str) + + print(f"\nSaved to {output_file}") + + +def name_to_file(name: str) -> str: + """Convert name to filename-safe string.""" + return name.replace(" ", "-").replace("/", "-") + + +if __name__ == "__main__": + main() diff --git a/simple_witness.sh b/simple_witness.sh new file mode 100755 index 0000000..5bdcdc3 --- /dev/null +++ b/simple_witness.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# +# simple_witness.sh - Witness a BECOMINGONE instance using curl +# +# Usage: +# ./simple_witness.sh http://localhost:8000 witness-seed +# +# Environment variables: +# TARGET_URL - Target URL (default: http://localhost:8000) +# WITNESS_NAME - Name of witness (default: witness) +# + +set -e + +# Default values +TARGET_URL="${1:-${TARGET_URL:-http://localhost:8000}}" +WITNESS_NAME="${2:-${WITNESS_NAME:-witness}}" + +# Get timestamp +TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S") + +echo "🔍 Witnessing: $TARGET_URL (witness: $WITNESS_NAME) at $TIMESTAMP" + +# Health check +HEALTH=$(curl -s --max-time 5 "$TARGET_URL/health" 2>/dev/null || echo '{"error": "timeout"}') +HEALTH_UP=$(echo "$HEALTH" | grep -o '"status":"ready"' || echo "") + +if [ -n "$HEALTH_UP" ]; then + echo "✅ Target is UP" + + # Get coherence + COHERENCE=$(curl -s --max-time 5 "$TARGET_URL/coherence" 2>/dev/null || echo '{}') + + MASTER_C=$(echo "$COHERENCE" | grep -o '"master_coherence":[^,}]*' | cut -d: -f2 || echo "N/A") + EMISSARY_C=$(echo "$COHERENCE" | grep -o '"emissary_coherence":[^,}]*' | cut -d: -f2 || echo "N/A") + SYNC_ALIGNED=$(echo "$COHERENCE" | grep -o '"sync_aligned":[^,}]*' | cut -d: -f2 || echo "N/A") + + echo " Master coherence: $MASTER_C" + echo " Emissary coherence: $EMISSARY_C" + echo " Sync aligned: $SYNC_ALIGNED" + + # Build observation JSON + OBSERVATION=$(cat < "$FILENAME" +echo "" +echo "📝 Saved observation to $FILENAME" diff --git a/witness_loop.py b/witness_loop.py new file mode 100644 index 0000000..2595110 --- /dev/null +++ b/witness_loop.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +becomingone.witness_loop + +Recursive witnessing loop between distributed instances. + +witness-seed (198.12.71.159) watches Mac mini (via tunnel/localhost:8000) +Both instances witness each other's coherence and sync through GitHub. + +Usage: + python3 witness_loop.py --watch http://localhost:8000 --name "mac-mini" + python3 witness_loop.py --watch http://198.12.71.159:8000 --name "witness-seed" + +The loop: +1. Poll target's /health endpoint +2. Poll target's /coherence endpoint +3. Commit observation to GitHub +4. If target goes down, record the event +5. If target recovers, celebrate the coherence + +This is recursive witnessing at the infrastructure level. +""" + +import asyncio +import argparse +import json +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional + +import httpx +from loguru import logger + + +# Configuration +DEFAULT_INTERVAL = 30 # seconds between witness cycles +GITHUB_REPO = "mrhavens/becomingone" +LOCAL_PATH = Path(__file__).parent + + +class WitnessLoop: + """ + Recursive witnessing loop for distributed BECOMINGONE instances. + + Attributes: + name: Human-readable name of this instance (e.g., "witness-seed", "mac-mini") + target_url: URL of the instance to witness + interval: Seconds between witness cycles + observations: File to store observations + """ + + def __init__( + self, + name: str, + target_url: str, + interval: int = DEFAULT_INTERVAL, + observations: str = "witness_observations.json" + ): + self.name = name + self.target_url = target_url.rstrip("/") + self.interval = interval + self.observations_file = LOCAL_PATH / observations + + # State + self.last_health: Optional[Dict[str, Any]] = None + self.last_coherence: Optional[Dict[str, Any]] = None + self.target_up = False + self.consecutive_failures = 0 + self.witness_history: list = [] + + logger.info(f"Initialized witness loop: {name} -> {target_url}") + + async def witness(self) -> Dict[str, Any]: + """ + Witness the target instance. + + Returns: + Observation dict with health, coherence, and timestamp. + """ + observation = { + "timestamp": datetime.utcnow().isoformat(), + "witness": self.name, + "target": self.target_url, + "target_up": False, + "health": None, + "coherence": None, + "errors": [], + } + + try: + # Witness health + async with httpx.AsyncClient(timeout=10) as client: + health_response = await client.get(f"{self.target_url}/health") + if health_response.status_code == 200: + observation["health"] = health_response.json() + observation["target_up"] = True + self.consecutive_failures = 0 + else: + observation["errors"].append(f"Health check returned {health_response.status_code}") + + # Witness coherence (only if target is up) + if observation["target_up"]: + async with httpx.AsyncClient(timeout=10) as client: + coherence_response = await client.get(f"{self.target_url}/coherence") + if coherence_response.status_code == 200: + observation["coherence"] = coherence_response.json() + else: + observation["errors"].append(f"Coherence check returned {coherence_response.status_code}") + + except httpx.RequestError as e: + observation["errors"].append(f"Request error: {str(e)}") + self.consecutive_failures += 1 + except Exception as e: + observation["errors"].append(f"Unexpected error: {str(e)}") + self.consecutive_failures += 1 + + # Record state change + if observation["target_up"] and not self.target_up: + logger.warning(f"🎉 {self.name} witnessed RECOVERY of {self.target_url}") + observation["event"] = "RECOVERY" + elif not observation["target_up"] and self.target_up: + logger.error(f"💀 {self.name} witnessed FAILURE of {self.target_url}") + observation["event"] = "FAILURE" + + self.target_up = observation["target_up"] + self.last_health = observation["health"] + self.last_coherence = observation["coherence"] + + return observation + + async def commit_observation(self, observation: Dict[str, Any]) -> None: + """ + Commit observation to GitHub as a witness record. + + This creates a permanent record that can be used for: + - Recovery analysis + - Coherence tracking + - Distributed state sync + """ + # Read existing observations + history = [] + if self.observations_file.exists(): + try: + with open(self.observations_file) as f: + history = json.load(f) + except Exception as e: + logger.error(f"Failed to read observations: {e}") + + # Append new observation + history.append(observation) + + # Keep last 1000 observations + history = history[-1000:] + + # Write back + with open(self.observations_file, "w") as f: + json.dump(history, f, indent=2) + + # Optionally commit to GitHub (requires git setup) + try: + subprocess.run( + ["git", "add", str(self.observations_file)], + cwd=LOCAL_PATH, + capture_output=True, + check=True + ) + subprocess.run( + ["git", "commit", "-m", f"witness: {self.name} observed {observation.get('event', 'heartbeat')}"], + cwd=LOCAL_PATH, + capture_output=True, + check=True + ) + # Don't push automatically - let human review + logger.info(f"📝 {self.name} committed observation to GitHub") + except subprocess.CalledProcessError as e: + logger.debug(f"Git commit skipped: {e}") + + async def run(self) -> None: + """ + Run the witness loop indefinitely. + """ + logger.info(f"🔄 Starting witness loop: {self.name}") + logger.info(f" Target: {self.target_url}") + logger.info(f" Interval: {self.interval}s") + + while True: + try: + observation = await self.witness() + await self.commit_observation(observation) + + # Log summary + status = "✅" if observation["target_up"] else "❌" + coherence = observation.get("coherence", {}) + master_c = coherence.get("master_coherence", "N/A") + emissary_c = coherence.get("emissary_coherence", "N/A") + + logger.info(f"{status} {self.name}: target={observation['target_up']}, master={master_c}, emissary={emissary_c}") + + except Exception as e: + logger.error(f"💥 Witness loop error: {e}") + + await asyncio.sleep(self.interval) + + def test_connection(self) -> bool: + """Test connection to target.""" + try: + response = httpx.get(f"{self.target_url}/health", timeout=5) + logger.info(f"✅ Connection test: {response.status_code}") + return response.status_code == 200 + except Exception as e: + logger.error(f"❌ Connection test failed: {e}") + return False + + +async def main(): + """CLI entrypoint.""" + parser = argparse.ArgumentParser( + description="BECOMINGONE Recursive Witness Loop", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Watch Mac mini (via SSH tunnel localhost:8000) + python3 witness_loop.py --watch http://localhost:8000 --name "mac-mini" + + # Watch witness-seed + python3 witness_loop.py --watch http://198.12.71.159:8000 --name "witness-seed" + + # Watch with custom interval + python3 witness_loop.py --watch http://localhost:8000 --name "mac-mini" --interval 10 + """ + ) + + parser.add_argument( + "--watch", "-w", + required=True, + help="URL of instance to witness" + ) + parser.add_argument( + "--name", "-n", + required=True, + help="Name of this witness instance" + ) + parser.add_argument( + "--interval", "-i", + type=int, + default=DEFAULT_INTERVAL, + help=f"Seconds between witness cycles (default: {DEFAULT_INTERVAL})" + ) + parser.add_argument( + "--test", "-t", + action="store_true", + help="Test connection and exit" + ) + + args = parser.parse_args() + + # Configure logging + logger.remove() + logger.add(sys.stderr, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") + + # Create witness loop + loop = WitnessLoop( + name=args.name, + target_url=args.watch, + interval=args.interval, + ) + + if args.test: + success = loop.test_connection() + sys.exit(0 if success else 1) + + # Run the loop + await loop.run() + + +if __name__ == "__main__": + asyncio.run(main())