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