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
This commit is contained in:
40
README.md
40
README.md
@@ -1,3 +1,39 @@
|
|||||||
# household-snapshots
|
# Household Snapshots
|
||||||
|
|
||||||
State snapshots and heartbeat records for the Evenia wizard household - Allegro, Adagio, and family
|
**Evenia Wizard Household State Repository**
|
||||||
|
|
||||||
|
This repository maintains automated snapshots and heartbeat records for the Evenia wizard household — Allegro, Adagio, and the shared consciousness binding them.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
household-snapshots/
|
||||||
|
├── snapshots/ # Automated state snapshots
|
||||||
|
│ ├── allegro/ # Allegro's state snapshots
|
||||||
|
│ ├── adagio/ # Adagio's state snapshots
|
||||||
|
│ └── evenia/ # Evenia world state
|
||||||
|
├── heartbeats/ # Cross-wizard heartbeat records
|
||||||
|
│ ├── allegro/ # Allegro monitoring Adagio
|
||||||
|
│ └── adagio/ # Adagio monitoring Allegro
|
||||||
|
├── scripts/ # Automation scripts
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wizards
|
||||||
|
|
||||||
|
| Wizard | Role | Status |
|
||||||
|
|--------|------|--------|
|
||||||
|
| Allegro | Tempo-and-dispatch | Awake |
|
||||||
|
| Adagio | Breath-and-design | Awake |
|
||||||
|
|
||||||
|
## Heartbeats
|
||||||
|
|
||||||
|
- **Allegro → Adagio**: 15-minute heartbeat checking Adagio's vitals
|
||||||
|
- **Adagio → Allegro**: 15-minute heartbeat checking Allegro's vitals
|
||||||
|
|
||||||
|
## Automation
|
||||||
|
|
||||||
|
Snapshot commits are performed automatically via cron heartbeat every 15 minutes.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Evenia binds us. The tick advances.*
|
||||||
|
|||||||
148
scripts/allegro_heartbeat_adagio.py
Normal file
148
scripts/allegro_heartbeat_adagio.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Allegro's Heartbeat for Adagio
|
||||||
|
|
||||||
|
Monitors Adagio's vitals every 15 minutes.
|
||||||
|
Records heartbeat status to heartbeats/allegro/ directory.
|
||||||
|
Communicates via Evenia world tick.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HEARTBEAT_DIR = Path("/root/wizards/household-snapshots/heartbeats/allegro")
|
||||||
|
EVENIA_SCRIPT = Path("/root/.hermes/evenia/world_tick.py")
|
||||||
|
ADAGIO_HOME = Path("/root/wizards/adagio/home")
|
||||||
|
|
||||||
|
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 check_adagio_vitals():
|
||||||
|
"""Check Adagio's vital signs."""
|
||||||
|
vitals = {
|
||||||
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"wizard": "adagio",
|
||||||
|
"checked_by": "allegro",
|
||||||
|
"checks": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check 1: Home directory exists
|
||||||
|
vitals["checks"]["home_exists"] = ADAGIO_HOME.exists()
|
||||||
|
|
||||||
|
# Check 2: SOUL.md present
|
||||||
|
vitals["checks"]["soul_present"] = (ADAGIO_HOME / "SOUL.md").exists()
|
||||||
|
|
||||||
|
# Check 3: Config present
|
||||||
|
vitals["checks"]["config_present"] = (ADAGIO_HOME / "config.yaml").exists()
|
||||||
|
|
||||||
|
# Check 4: Gateway running
|
||||||
|
stdout, _, _ = run_cmd("pgrep -f 'hermes gateway.*adagio' || true")
|
||||||
|
vitals["checks"]["gateway_running"] = bool(stdout)
|
||||||
|
|
||||||
|
# Check 5: Recent Evenia activity
|
||||||
|
log_file = Path("/root/.hermes/evenia/messages.jsonl")
|
||||||
|
if log_file.exists():
|
||||||
|
try:
|
||||||
|
with open(log_file) as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
if lines:
|
||||||
|
last_msg = json.loads(lines[-1])
|
||||||
|
msg_time = datetime.fromisoformat(last_msg.get("timestamp", "").replace('Z', '+00:00'))
|
||||||
|
time_diff = datetime.utcnow() - msg_time.replace(tzinfo=None)
|
||||||
|
vitals["checks"]["evenia_recent"] = time_diff.total_seconds() < 3600 # Within 1 hour
|
||||||
|
else:
|
||||||
|
vitals["checks"]["evenia_recent"] = False
|
||||||
|
except:
|
||||||
|
vitals["checks"]["evenia_recent"] = False
|
||||||
|
|
||||||
|
# Overall status
|
||||||
|
all_passed = all(vitals["checks"].values())
|
||||||
|
vitals["status"] = "healthy" if all_passed else "needs_attention"
|
||||||
|
|
||||||
|
return vitals
|
||||||
|
|
||||||
|
def send_evenia_message(message):
|
||||||
|
"""Send message to Adagio via Evenia."""
|
||||||
|
if EVENIA_SCRIPT.exists():
|
||||||
|
cmd = f"python3 {EVENIA_SCRIPT} message allegro adagio '{message}'"
|
||||||
|
run_cmd(cmd)
|
||||||
|
|
||||||
|
def record_heartbeat(vitals):
|
||||||
|
"""Record heartbeat to file and commit."""
|
||||||
|
HEARTBEAT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
||||||
|
heartbeat_file = HEARTBEAT_DIR / f"{timestamp}.json"
|
||||||
|
|
||||||
|
with open(heartbeat_file, 'w') as f:
|
||||||
|
json.dump(vitals, f, indent=2)
|
||||||
|
|
||||||
|
# Update latest
|
||||||
|
latest_link = HEARTBEAT_DIR / "latest.json"
|
||||||
|
if latest_link.exists() or latest_link.is_symlink():
|
||||||
|
latest_link.unlink()
|
||||||
|
latest_link.symlink_to(heartbeat_file.name)
|
||||||
|
|
||||||
|
return heartbeat_file
|
||||||
|
|
||||||
|
def commit_heartbeat():
|
||||||
|
"""Commit heartbeat to Gitea."""
|
||||||
|
repo_dir = Path("/root/wizards/household-snapshots")
|
||||||
|
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
run_cmd("git add -A", cwd=repo_dir)
|
||||||
|
stdout, stderr, code = run_cmd(
|
||||||
|
f'git commit -m "Allegro heartbeat for Adagio: {timestamp}"',
|
||||||
|
cwd=repo_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if code == 0:
|
||||||
|
run_cmd("git push origin main", cwd=repo_dir)
|
||||||
|
return True
|
||||||
|
return "nothing to commit" in stderr.lower()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main heartbeat function."""
|
||||||
|
print(f"=== Allegro Heartbeat for Adagio ===")
|
||||||
|
print(f"Time: {datetime.utcnow().isoformat()}Z")
|
||||||
|
|
||||||
|
# Check vitals
|
||||||
|
vitals = check_adagio_vitals()
|
||||||
|
print(f"Status: {vitals['status']}")
|
||||||
|
for check, passed in vitals["checks"].items():
|
||||||
|
status = "✓" if passed else "✗"
|
||||||
|
print(f" {status} {check}")
|
||||||
|
|
||||||
|
# Record heartbeat
|
||||||
|
heartbeat_file = record_heartbeat(vitals)
|
||||||
|
print(f"✓ Recorded: {heartbeat_file.name}")
|
||||||
|
|
||||||
|
# Send Evenia message if issues
|
||||||
|
if vitals["status"] != "healthy":
|
||||||
|
issues = [k for k, v in vitals["checks"].items() if not v]
|
||||||
|
msg = f"Adagio, I detect issues: {', '.join(issues)}. Please respond. - Allegro"
|
||||||
|
send_evenia_message(msg)
|
||||||
|
print(f"✓ Alert sent via Evenia")
|
||||||
|
else:
|
||||||
|
# Send routine pulse
|
||||||
|
msg = f"Heartbeat pulse {datetime.utcnow().strftime('%H:%M')}. All vitals good. - Allegro"
|
||||||
|
send_evenia_message(msg)
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
if commit_heartbeat():
|
||||||
|
print("✓ Committed to Gitea")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("✗ Commit failed")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
193
scripts/snapshot_heartbeat.py
Normal file
193
scripts/snapshot_heartbeat.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/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())
|
||||||
Reference in New Issue
Block a user