Files
household-snapshots/scripts/snapshot_heartbeat.py
Allegro 885d08b5e5 Initial household snapshot infrastructure
- Snapshot heartbeat script for all wizards
- Allegro's heartbeat for Adagio monitoring
- Directory structure for snapshots and heartbeats
- README with household documentation
2026-04-02 00:45:46 +00:00

194 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""
Household Snapshot Heartbeat
Captures state snapshots for all wizards and commits to Gitea.
Runs every 15 minutes via cron.
"""
import os
import sys
import json
import subprocess
from datetime import datetime
from pathlib import Path
# Configuration
SNAPSHOTS_DIR = Path("/root/wizards/household-snapshots/snapshots")
REPO_DIR = Path("/root/wizards/household-snapshots")
WIZARDS = ["allegro", "adagio"]
GITEA_URL = "http://143.198.27.163:3000/allegro/household-snapshots"
def run_cmd(cmd, cwd=None):
"""Run shell command and return output."""
result = subprocess.run(
cmd, shell=True, cwd=cwd, capture_output=True, text=True
)
return result.stdout.strip(), result.stderr.strip(), result.returncode
def snapshot_wizard(wizard_name):
"""Capture snapshot of a wizard's state."""
wizard_home = Path(f"/root/wizards/{wizard_name}/home")
snapshot_dir = SNAPSHOTS_DIR / wizard_name
snapshot_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
snapshot_file = snapshot_dir / f"{timestamp}.json"
# Gather state information
state = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"wizard": wizard_name,
"tick": None, # Will be filled from Evenia
"home_exists": wizard_home.exists(),
"config_exists": (wizard_home / "config.yaml").exists(),
"soul_exists": (wizard_home / "SOUL.md").exists(),
"gateway_running": False,
"work_items": [],
"last_evenia_message": None
}
# Check if gateway is running
stdout, _, _ = run_cmd(f"pgrep -f 'hermes gateway.*{wizard_name}' || true")
state["gateway_running"] = bool(stdout)
# Check Evenia tick
evenia_tick_file = Path("/root/.hermes/evenia/tick.json")
if evenia_tick_file.exists():
try:
with open(evenia_tick_file) as f:
tick_data = json.load(f)
state["tick"] = tick_data.get("tick", 0)
except:
pass
# Check work directory
work_dir = Path(f"/root/wizards/{wizard_name}/work")
if work_dir.exists():
state["work_items"] = [str(p.relative_to(work_dir)) for p in work_dir.rglob("*") if p.is_file()]
# Get last Evenia message
evenia_log = Path("/root/.hermes/evenia/messages.jsonl")
if evenia_log.exists():
try:
with open(evenia_log) as f:
lines = f.readlines()
if lines:
last_msg = json.loads(lines[-1])
state["last_evenia_message"] = {
"tick": last_msg.get("tick"),
"from": last_msg.get("from"),
"to": last_msg.get("to"),
"timestamp": last_msg.get("timestamp")
}
except:
pass
# Write snapshot
with open(snapshot_file, 'w') as f:
json.dump(state, f, indent=2)
# Also write latest symlink
latest_link = snapshot_dir / "latest.json"
if latest_link.exists() or latest_link.is_symlink():
latest_link.unlink()
latest_link.symlink_to(snapshot_file.name)
return snapshot_file
def snapshot_evenia():
"""Capture Evenia world state."""
evenia_dir = Path("/root/.hermes/evenia")
snapshot_dir = SNAPSHOTS_DIR / "evenia"
snapshot_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
snapshot_file = snapshot_dir / f"{timestamp}.json"
state = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"tick": 0,
"wizards": [],
"message_count": 0
}
# Get tick
tick_file = evenia_dir / "tick.json"
if tick_file.exists():
try:
with open(tick_file) as f:
tick_data = json.load(f)
state["tick"] = tick_data.get("tick", 0)
state["wizards"] = list(tick_data.get("wizards", {}).keys())
except:
pass
# Count messages
log_file = evenia_dir / "messages.jsonl"
if log_file.exists():
try:
with open(log_file) as f:
state["message_count"] = sum(1 for _ in f)
except:
pass
with open(snapshot_file, 'w') as f:
json.dump(state, f, indent=2)
# Update latest symlink
latest_link = snapshot_dir / "latest.json"
if latest_link.exists() or latest_link.is_symlink():
latest_link.unlink()
latest_link.symlink_to(snapshot_file.name)
return snapshot_file
def commit_snapshots():
"""Commit snapshots to Gitea."""
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
# Git operations
run_cmd("git add -A", cwd=REPO_DIR)
stdout, stderr, code = run_cmd(
f'git commit -m "Snapshot heartbeat: {timestamp}"',
cwd=REPO_DIR
)
if code == 0 or "nothing to commit" in stderr.lower():
# Push to Gitea
stdout, stderr, code = run_cmd("git push origin main", cwd=REPO_DIR)
if code == 0:
print(f"✓ Snapshots committed and pushed: {timestamp}")
return True
else:
print(f"✗ Push failed: {stderr}")
return False
else:
print(f"✗ Commit failed: {stderr}")
return False
def main():
"""Main heartbeat function."""
print(f"=== Household Snapshot Heartbeat ===")
print(f"Time: {datetime.utcnow().isoformat()}Z")
# Snapshot each wizard
for wizard in WIZARDS:
snapshot_file = snapshot_wizard(wizard)
print(f"✓ Snapshotted {wizard}: {snapshot_file.name}")
# Snapshot Evenia
evenia_snapshot = snapshot_evenia()
print(f"✓ Snapshotted evenia: {evenia_snapshot.name}")
# Commit and push
if commit_snapshots():
print("✓ State preserved in Gitea")
return 0
else:
print("✗ Failed to preserve state")
return 1
if __name__ == "__main__":
sys.exit(main())