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
This commit is contained in:
+16
-5
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
Executable
+78
@@ -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 <<EOF
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"witness": "$WITNESS_NAME",
|
||||
"target": "$TARGET_URL",
|
||||
"target_up": true,
|
||||
"master_coherence": $MASTER_C,
|
||||
"emissary_coherence": $EMISSARY_C,
|
||||
"sync_aligned": $SYNC_ALIGNED
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
else
|
||||
echo "❌ Target is DOWN"
|
||||
|
||||
ERROR_MSG=$(echo "$HEALTH" | grep -o '"message":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error")
|
||||
echo " Error: $ERROR_MSG"
|
||||
|
||||
OBSERVATION=$(cat <<EOF
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"witness": "$WITNESS_NAME",
|
||||
"target": "$TARGET_URL",
|
||||
"target_up": false,
|
||||
"error": "$ERROR_MSG"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
# Save observation
|
||||
FILENAME="witness_${WITNESS_NAME// /-}.json"
|
||||
echo "$OBSERVATION" > "$FILENAME"
|
||||
echo ""
|
||||
echo "📝 Saved observation to $FILENAME"
|
||||
+280
@@ -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())
|
||||
Reference in New Issue
Block a user