Ashira Pitchayapakayakul commited on
Commit
7d77adb
Β·
1 Parent(s): aa55e4b

Replace http.server heredoc with uvicorn FastAPI (robust port binding)

Browse files
Files changed (2) hide show
  1. bin/hermes-status-server.py +95 -0
  2. start.sh +4 -46
bin/hermes-status-server.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hermes status HTTP server for HF Space.
4
+ FastAPI + uvicorn β€” robust port binding, auto-handles signals.
5
+
6
+ Endpoints:
7
+ GET / β†’ JSON status (ledger size, episodes, daemons, disk)
8
+ GET /health β†’ simple {"ok": true}
9
+ GET /logs β†’ tail of recent boot/cron logs (debug)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sqlite3
15
+ import subprocess
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ from fastapi import FastAPI
20
+ from fastapi.responses import JSONResponse, PlainTextResponse
21
+
22
+ app = FastAPI(title="hermes", docs_url=None, redoc_url=None)
23
+
24
+ HOME = Path(os.environ.get("HOME", "/home/hermes"))
25
+ LEDGER = HOME / ".claude/state/scrape-ledger.db"
26
+ EPISODES = HOME / ".claude/state/surrogate-memory/episodes.jsonl"
27
+ LOG_DIR = HOME / ".claude/logs"
28
+
29
+
30
+ def _ledger_count() -> int:
31
+ try:
32
+ with sqlite3.connect(str(LEDGER), timeout=2) as c:
33
+ return c.execute("SELECT COUNT(*) FROM scraped").fetchone()[0]
34
+ except Exception:
35
+ return 0
36
+
37
+
38
+ def _episodes_count() -> int:
39
+ try:
40
+ if EPISODES.exists():
41
+ return sum(1 for _ in EPISODES.open())
42
+ except Exception:
43
+ pass
44
+ return 0
45
+
46
+
47
+ def _daemons() -> int:
48
+ try:
49
+ out = subprocess.run(
50
+ ["pgrep", "-fc", "discord-bot|surrogate-dev|scrape-loop|hermes-cron|ollama"],
51
+ capture_output=True, text=True, timeout=2,
52
+ )
53
+ return int(out.stdout.strip() or 0)
54
+ except Exception:
55
+ return 0
56
+
57
+
58
+ @app.get("/")
59
+ def root() -> JSONResponse:
60
+ return JSONResponse({
61
+ "service": "hermes",
62
+ "model": "axentx/surrogate-1",
63
+ "status": "ok",
64
+ "ts": datetime.now(timezone.utc).isoformat(),
65
+ "ledger_repos": _ledger_count(),
66
+ "episodes": _episodes_count(),
67
+ "daemons_running": _daemons(),
68
+ })
69
+
70
+
71
+ @app.get("/health")
72
+ def health() -> dict:
73
+ return {"ok": True}
74
+
75
+
76
+ @app.get("/logs")
77
+ def logs() -> PlainTextResponse:
78
+ out_lines: list[str] = []
79
+ for log_name in ("boot.log", "cron.log", "discord-bot.log", "ollama.log"):
80
+ f = LOG_DIR / log_name
81
+ if not f.exists():
82
+ continue
83
+ try:
84
+ tail = f.read_text(errors="replace").splitlines()[-10:]
85
+ out_lines.append(f"━━━ {log_name} ━━━")
86
+ out_lines.extend(tail)
87
+ out_lines.append("")
88
+ except Exception:
89
+ pass
90
+ return PlainTextResponse("\n".join(out_lines) or "(no logs)")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ import uvicorn
95
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
start.sh CHANGED
@@ -138,50 +138,8 @@ chmod +x /tmp/hermes-cron.sh
138
  nohup /tmp/hermes-cron.sh > "$LOG_DIR/cron-master.log" 2>&1 &
139
  echo "[$(date +%H:%M:%S)] cron loop started" >> "$LOG_DIR/boot.log"
140
 
141
- # ── 8. Status HTTP server on :7860 (HF requires + UptimeRobot keep-alive) ──
142
- python3 <<'PYEOF' &
143
- from http.server import BaseHTTPRequestHandler, HTTPServer
144
- import json, os, sqlite3, subprocess, datetime
145
- from pathlib import Path
146
 
147
- class StatusHandler(BaseHTTPRequestHandler):
148
- def do_GET(self):
149
- try:
150
- ledger = sqlite3.connect(os.path.expanduser('~/.claude/state/scrape-ledger.db')).execute(
151
- 'SELECT COUNT(*) FROM scraped').fetchone()[0]
152
- except Exception: ledger = 0
153
- try:
154
- ep_path = Path(os.path.expanduser('~/.claude/state/surrogate-memory/episodes.jsonl'))
155
- episodes = sum(1 for _ in ep_path.open()) if ep_path.exists() else 0
156
- except Exception: episodes = 0
157
- try:
158
- procs = int(subprocess.run(['pgrep', '-fc', 'discord-bot|surrogate-dev|scrape-loop|hermes-cron'],
159
- capture_output=True, text=True).stdout.strip() or 0)
160
- except Exception: procs = 0
161
- try:
162
- train_dir = Path(os.path.expanduser('~/.claude/state/surrogate-memory'))
163
- disk_mb = sum(p.stat().st_size for p in train_dir.rglob('*') if p.is_file()) // (1024*1024)
164
- except Exception: disk_mb = 0
165
- body = json.dumps({
166
- 'service': 'hermes',
167
- 'status': 'ok',
168
- 'ts': datetime.datetime.utcnow().isoformat() + 'Z',
169
- 'ledger_repos': ledger,
170
- 'episodes': episodes,
171
- 'daemons_running': procs,
172
- 'memory_disk_mb': disk_mb,
173
- }, indent=2)
174
- self.send_response(200)
175
- self.send_header('Content-Type', 'application/json')
176
- self.send_header('Access-Control-Allow-Origin', '*')
177
- self.end_headers()
178
- self.wfile.write(body.encode())
179
- def log_message(self, *args): pass
180
-
181
- print('[hermes] status :7860', flush=True)
182
- HTTPServer(('0.0.0.0', 7860), StatusHandler).serve_forever()
183
- PYEOF
184
-
185
- # ── 9. Container PID 1 β€” tail boot log forever ──────────────────────────────
186
- echo "[$(date +%H:%M:%S)] boot complete β€” entering watch mode" >> "$LOG_DIR/boot.log"
187
- tail -f "$LOG_DIR/boot.log"
 
138
  nohup /tmp/hermes-cron.sh > "$LOG_DIR/cron-master.log" 2>&1 &
139
  echo "[$(date +%H:%M:%S)] cron loop started" >> "$LOG_DIR/boot.log"
140
 
141
+ # ── 8. Status HTTP server on :7860 (FastAPI/uvicorn β€” robust binding) ──────
142
+ echo "[$(date +%H:%M:%S)] starting status server :7860" >> "$LOG_DIR/boot.log"
 
 
 
143
 
144
+ # Run as PID 1 β€” uvicorn handles signals + auto-restart on crash
145
+ exec python3 ~/.claude/bin/hermes-status-server.py >> "$LOG_DIR/status-server.log" 2>&1