emerging.systems / phoenix_dashboard.py
compunerd's picture
Filter empty thoughts from recording and chat display
e431d79 verified
Raw History Blame Contribute Delete
51.6 kB
#!/usr/bin/env python3
"""Phoenix Living Dashboard v4 — verbose logging + chat + torus viz.
New features:
- Verbose log: keeps 1 hour of detailed state (token streams, tuning scores,
spawn/sweep/solidify events, coherence snapshots) in a rotating buffer
- Chat interface: type messages to Phox directly on the dashboard
- Torus-style ring visualization: oscillators as a 3D-projected torus knot
- Source+data logging: every event records what data flowed through
Controls: Start/Stop/Save brain, Load .pbrain files, Chat box.
"""
import re
import json, os, signal, subprocess, threading, time, glob, shutil, collections
from urllib.parse import urlparse, parse_qs
import sys, urllib.request
import http.server, socketserver
import importlib
from agent_core import HISTORY_FILE, SCHEDULE_FILE, MEMORY_FILE
STATE_FILE = "/tmp/phoenix_state.jsonl"
VERBOSE_LOG = "/tmp/phoenix_verbose.log"
DAEMON_LOG = "/tmp/phoenix_engine.log"
BQMC_PATH = "/home/compunerd/models/gemma4-12b-ternary-normed.bqmc"
# START launches the int8 engine -- the same backend chat already needs on 8781.
# The old ternary daemon (/tmp/phoenix + gemma4-12b-ternary-normed.bqsm) is
# removed, not repointed: those weights lose per-column magnitude and were shown
# twice to be unusable -- at chance end-to-end, and 46% output loss on a single
# matrix in an engine-free reconstruction sweep (bitwidth_sweep.py). A START
# button that succeeds and emits nothing is worse than no button.
ENGINE_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"bqsm_serve_int8.py")
BRAIN_DIR = "/home/compunerd/models"
PORT = int(os.environ.get("PHOENIX_PORT", "8765"))
TOKENIZER_SCRIPT = "/home/compunerd/agent_framework/bqsm_assist/tokenizer_server.py"
PY_BIN = "/home/compunerd/Desktop/bqsm/basin-quotient-machine/bqsm_sdk/.venv/bin/python"
MAX_LOG_AGE_S = 3600
# Chat routes to bqsm_infer.py, which serves the VERIFIED wave forward on
# Llama-3.2-3B (5/5 token agreement with the reference; see op_ledger.py).
# It is deliberately NOT bqsm_serve.py + the ternary .bqsm: those weights lose
# per-column magnitude and score at chance, so that path cannot emit language
# no matter how the dashboard is wired.
SERVE_URL = os.environ.get("BQSM_INFER", "http://127.0.0.1:8781")
INFER_MODE = os.environ.get("BQSM_MODE", "wave") # "wave" or "reference"
AGENT_MODE = os.environ.get("BQSM_AGENT", "on") # "on" = tool-use loop, "off" = raw completion
CONTEMPLATE = os.environ.get("BQSM_CONTEMPLATE", "on") != "off" # stream-of-consciousness
def engine_healthy(timeout=2):
try:
with urllib.request.urlopen(SERVE_URL + "/health", timeout=timeout) as r:
return json.loads(r.read())
except Exception:
return None
def engine_start(wait_s=60):
"""Launch the int8 server and wait for it to actually answer /health.
Loading 2.82 GB takes ~22 s, so 'started' means loaded, not spawned."""
global _phoenix_proc
if engine_healthy():
return {"status": "already_running"}
_phoenix_proc = subprocess.Popen(
[sys.executable, ENGINE_SCRIPT, "--port", SERVE_URL.rsplit(":", 1)[1]],
stdout=open(DAEMON_LOG, "a"), stderr=subprocess.STDOUT,
env={**os.environ, "OMP_NUM_THREADS": os.environ.get("OMP_NUM_THREADS", "6")},
start_new_session=True)
add_verbose({"tag": "control", "data": f"int8 engine starting, PID {_phoenix_proc.pid}"})
for _ in range(wait_s):
if _phoenix_proc.poll() is not None:
return {"status": f"error: engine exited rc={_phoenix_proc.returncode}, see {DAEMON_LOG}"}
h = engine_healthy(1)
if h:
add_verbose({"tag": "control",
"data": f"engine ready: {h.get('weights_gb')} GB int8 resident"})
return {"status": "started", **h}
time.sleep(1)
return {"status": "timeout — still loading, check /status"}
def engine_pid():
"""Find the engine even if this dashboard did not start it (restart, reboot).
Matches the script path in /proc cmdline, so it can never match the
dashboard's own process the way a pkill pattern would."""
for d in os.listdir("/proc"):
if not d.isdigit():
continue
try:
cmd = open(f"/proc/{d}/cmdline", "rb").read().split(b"\0")
except Exception:
continue
if any(ENGINE_SCRIPT.encode() == c for c in cmd):
return int(d)
return None
def engine_stop():
global _phoenix_proc
if not (_phoenix_proc and _phoenix_proc.poll() is None):
pid = engine_pid() # adopt an engine we did not spawn
if pid:
try:
os.kill(pid, signal.SIGTERM)
add_verbose({"tag": "control", "data": f"int8 engine stopped (adopted PID {pid})"})
return {"status": "stopped"}
except Exception as e:
return {"status": f"error: {e}"}
if _phoenix_proc and _phoenix_proc.poll() is None:
_phoenix_proc.terminate()
try: _phoenix_proc.wait(timeout=10)
except Exception: _phoenix_proc.kill()
add_verbose({"tag": "control", "data": "int8 engine stopped"})
return {"status": "stopped"}
return {"status": "not_running"}
_last_state = {"tendrils":0,"conns":0,"coh":[0,0,0,0],"c2":0,"c4":0,"c6":0,"cycles":0}
_phoenix_proc = None
_tokenizer_proc = None
_lock = threading.Lock()
# Vocab for decoding (loaded once)
_vocab = None
def load_vocab():
"""Load vocab file for decoding token IDs to text."""
global _vocab
import struct
_vocab = {}
try:
with open("/home/compunerd/models/gemma4-12b.vocab", "rb") as f:
count = struct.unpack("<I", f.read(4))[0]
for i in range(count):
ln = struct.unpack("<H", f.read(2))[0]
_vocab[i] = f.read(ln).decode("utf-8", errors="replace")
except:
pass
def tok_encode(text):
"""Tokenize text using the tokenizer subprocess."""
global _tokenizer_proc
try:
if _tokenizer_proc is None or _tokenizer_proc.poll() is not None:
_tokenizer_proc = subprocess.Popen(
[PY_BIN, TOKENIZER_SCRIPT],
stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
req = json.dumps({"cmd": "encode", "text": text}) + "\n"
_tokenizer_proc.stdin.write(req.encode())
_tokenizer_proc.stdin.flush()
resp = _tokenizer_proc.stdout.readline().decode().strip()
return json.loads(resp).get("ids", [])
except:
return []
def tok_decode(ids):
"""Decode token IDs to text using vocab file."""
if _vocab is None:
return " ".join(str(i) for i in ids)
return "".join(_vocab.get(i, f"[{i}]") for i in ids)
# Verbose log: deque of (timestamp, json_dict) entries, max 1 hour
_verbose_log = collections.deque()
_verbose_lock = threading.Lock()
# Chat messages: list of {role, text, time}
class _ChatList(list):
"""Auto-persists every appended message to HISTORY_FILE for agent review."""
def append(self, msg):
super().append(msg)
try:
os.makedirs(os.path.dirname(HISTORY_FILE), exist_ok=True)
with open(HISTORY_FILE, "a") as f:
f.write(json.dumps({"role": msg["role"], "text": msg["text"],
"ts": time.time()}) + "\n")
except Exception:
pass
_chat_messages = _ChatList()
_chat_lock = threading.Lock()
def _scheduler_loop():
"""Poll the schedule file; fire due wake-ups as chat messages."""
from datetime import datetime
fired = set()
while True:
try:
if SCHEDULE_FILE.exists():
for line in SCHEDULE_FILE.read_text().splitlines():
if not line.strip() or hash(line) in fired:
continue
try:
e = json.loads(line)
when = (e["at"] if isinstance(e["at"], (int, float))
else datetime.fromisoformat(e["at"]).timestamp())
except Exception:
continue
if time.time() >= when:
msg = e.get("message", "wake")
add_verbose({"tag": "schedule", "data": f"WAKE: {msg}"})
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"[wake-up] {msg}"})
fired.add(hash(line))
except Exception:
pass
time.sleep(5)
threading.Thread(target=_scheduler_loop, daemon=True).start()
def add_verbose(entry):
"""Add an entry to the verbose log, prune entries older than 1 hour."""
ts = time.time()
with _verbose_lock:
_verbose_log.append((ts, entry))
# Prune entries older than 1 hour
while _verbose_log and _verbose_lock and (ts - _verbose_log[0][0]) > MAX_LOG_AGE_S:
_verbose_log.popleft()
def get_verbose(last_n=200):
"""Get the last N verbose entries."""
with _verbose_lock:
return list(_verbose_log)[-last_n:]
def engine_generate(prompt, n=24, timeout_s=600):
"""Synchronous generate against the engine: POST /generate, poll /jobs/<id>.
Returns the decoded text, or None on failure."""
try:
req = urllib.request.Request(
SERVE_URL + "/generate",
data=json.dumps({"prompt": prompt, "n": n}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
job = json.loads(urllib.request.urlopen(req, timeout=15).read())
jid = job.get("job")
except Exception:
return None
if not jid:
return None
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
r = json.loads(urllib.request.urlopen(
SERVE_URL + "/jobs/" + jid, timeout=15).read())
except Exception:
time.sleep(0.5)
continue
if r.get("state") == "done":
return r.get("text") or ""
if r.get("state") == "error":
return None
time.sleep(0.5)
return None
def _chat_raw(text):
"""Raw prompt -> completion through the engine, streaming tokens to the log.
The pre-agent path; kept as the BQSM_AGENT=off fallback."""
try:
req = urllib.request.Request(
SERVE_URL + "/generate",
data=json.dumps({"prompt": text, "n": 8,
"mode": INFER_MODE}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
job = json.loads(urllib.request.urlopen(req, timeout=10).read())
jid = job.get("job")
except Exception as e:
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"Inference API unreachable at {SERVE_URL} — start it with "
f"`python3 bqsm_assist/bqsm_infer.py --port 8781` ({e})"})
return
add_verbose({"tag": "chat", "data": f"job {jid} queued ({INFER_MODE})"})
seen = 0
for _ in range(900):
time.sleep(1)
try:
r = json.loads(urllib.request.urlopen(
SERVE_URL + "/jobs/" + jid, timeout=10).read())
except Exception:
continue
toks = r.get("tokens") or []
while seen < len(toks): # stream tokens to the log as they land
t = toks[seen]; seen += 1
add_verbose({"tag": "token",
"data": f"[{seen}] {t['id']} {t['text']!r}"})
if r.get("state") == "done":
txt = r.get("text") or "(no tokens returned)"
with _chat_lock:
_chat_messages.append({"role": "phox", "text": txt})
add_verbose({"tag": "chat", "data": f"PHOX: {txt}"})
return
if r.get("state") == "error":
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": "engine error: " + str(r.get("error"))})
return
with _chat_lock:
_chat_messages.append({"role": "phox", "text": "timed out waiting on engine"})
HTML = r'''<!DOCTYPE html>
<html><head><meta charset="utf8"><title>Phox — Living Brain</title>
<style>
:root{--bg:#070b13;--surface:#0d1219;--surface2:#111822;--border:#1a2236;
--text:#b0c4d8;--text-m:#4e6278;--accent:#ff2d78;
--ch1:#00d4ff;--ch2:#a06cff;--ch3:#00e676;--ch4:#ff2d78;--ch5:#ff9100;--ch6:#ffd600;--ch7:#448aff;
--mono:"SF Mono","Fira Code",monospace;--sans:system-ui,sans-serif}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;overflow:hidden;height:100vh}
.app{display:grid;grid-template-rows:auto 1fr auto;grid-template-columns:1fr 360px;height:100vh}
.topbar{grid-column:1/3;display:flex;align-items:center;gap:10px;padding:8px 16px;
background:var(--surface);border-bottom:1px solid var(--border);flex-wrap:wrap}
.brand{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.18em;
text-transform:uppercase;white-space:nowrap;position:relative;padding-bottom:4px}
.brand::after{content:'';position:absolute;bottom:0;left:0;width:32px;height:2px;background:var(--accent)}
.controls{display:flex;gap:6px;margin-left:auto;align-items:center}
.btn{font-family:var(--mono);font-size:10px;letter-spacing:.08em;padding:6px 14px;
background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:pointer;
text-transform:uppercase;border-radius:3px;transition:all .15s}
.btn:hover{border-color:var(--text-m)}
.btn.start{border-color:var(--ch3);color:var(--ch3)}.btn.start:hover{background:rgba(0,230,118,.12)}
.btn.stop{border-color:var(--accent);color:var(--accent)}.btn.stop:hover{background:rgba(255,45,120,.12)}
.btn.save{border-color:var(--ch1);color:var(--ch1)}.btn.save:hover{background:rgba(0,212,255,.12)}
.btn:disabled{opacity:.3;cursor:default}
select{font-family:var(--mono);font-size:10px;padding:4px 8px;background:var(--surface2);
border:1px solid var(--border);color:var(--text);border-radius:3px;cursor:pointer;max-width:140px}
.status{display:flex;gap:6px;align-items:center;font-family:var(--mono);font-size:10px;color:var(--text-m)}
.status .dot{width:8px;height:8px;border-radius:50%;background:var(--ch3);box-shadow:0 0 8px var(--ch3);animation:pulse 2s infinite}
.status.stopped .dot{background:var(--accent);box-shadow:0 0 8px var(--accent);animation:none}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.main{display:grid;grid-template-columns:1fr 360px;overflow:hidden}
.ring-panel{background:var(--surface);border-right:1px solid var(--border);position:relative;overflow:hidden}
.ring-panel canvas{width:100%;height:100%;display:block}
.data-panel{background:var(--surface);display:flex;flex-direction:column;overflow-y:auto;padding:14px;gap:14px}
.data-panel h3{font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:.12em;
text-transform:uppercase;color:var(--text-m);margin-bottom:6px}
.metric{display:flex;justify-content:space-between;align-items:center;font-family:var(--mono);font-size:11px;padding:3px 0}
.metric .label{color:var(--text-m)}.metric .val{color:var(--text);font-variant-numeric:tabular-nums}
.metric .val.accent{color:var(--accent);font-weight:700}
.bar-bg{height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;width:60px}
.bar{height:100%;border-radius:3px;transition:width .3s}
.log{font-family:var(--mono);font-size:10px;color:var(--text-m);max-height:180px;overflow-y:auto}
.log .entry{padding:2px 0;border-bottom:1px solid var(--surface2)}
.log .entry.spawn{color:var(--ch3)}.log .entry.solidify{color:var(--ch5)}
.log .entry.token{color:var(--text)}.log .entry.sweep{color:var(--ch1)}
.log .entry.control{color:var(--ch7);font-weight:bold}
.log .entry.chat{color:var(--ch2);font-weight:bold}
.bottom-bar{grid-column:1/3;display:grid;grid-template-columns:1fr 360px;gap:0;align-items:end;
background:var(--surface);border-top:1px solid var(--border)}
.verbose-panel{border-right:1px solid var(--border);display:flex;flex-direction:column;height:160px}
.verbose-panel h3{padding:6px 12px;font-family:var(--mono);font-size:9px;font-weight:600;
letter-spacing:.12em;text-transform:uppercase;color:var(--text-m);border-bottom:1px solid var(--border)}
.verbose-log{flex:1;overflow-y:auto;padding:4px 12px;
font-family:var(--mono);font-size:10px;color:var(--text-m)}
.verbose-log .ventry{padding:1px 0;border-bottom:1px solid var(--surface2)}
.verbose-log .ventry .ts{color:var(--text-d);margin-right:6px}
.verbose-log .ventry .tag{font-weight:bold;margin-right:4px}
.verbose-log .ventry .tag.tok{color:var(--ch1)}
.verbose-log .ventry .tag.tune{color:var(--ch5)}
.verbose-log .ventry .tag.solid{color:var(--ch3)}
.verbose-log .ventry .tag.sweep{color:var(--ch7)}
.verbose-log .ventry .tag.tool{color:var(--ch4)}
.verbose-log .ventry .tag.chat{color:var(--ch2)}
.chat-panel{display:flex;flex-direction:column;height:auto}
.chat-panel h3{padding:6px 12px;font-family:var(--mono);font-size:9px;font-weight:600;
letter-spacing:.12em;text-transform:uppercase;color:var(--text-m);border-bottom:1px solid var(--border)}
.chat-log{flex:0 0 auto;max-height:140px;overflow-y:auto;padding:4px 12px;font-family:var(--mono);font-size:10px}
.chat-log .msg{padding:2px 0}
.chat-log .msg .role{font-weight:bold;margin-right:4px}
.chat-log .msg .role.user{color:var(--ch1)}
.chat-log .msg .role.phox{color:var(--accent)}
.chat-input{display:flex;gap:6px;padding:6px 12px;border-top:1px solid var(--border)}
.chat-input input{flex:1;font-family:var(--mono);font-size:11px;padding:6px 10px;
background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:3px}
.chat-input button{font-family:var(--mono);font-size:10px;padding:6px 14px;
background:var(--accent);border:none;color:#fff;cursor:pointer;border-radius:3px;font-weight:bold}
/* ── HVM Panel ── */
.hvm-panel{margin-top:4px}
.hvm-panel .hvm-results{font-family:var(--mono);font-size:9px;color:var(--text-m);
max-height:140px;overflow-y:auto;margin-top:4px;line-height:1.4}
.hvm-panel .hvm-results .hit{color:var(--ch3)}.hvm-panel .hvm-results .miss{color:var(--accent)}
.hvm-panel input{width:100%;font-family:var(--mono);font-size:10px;padding:4px 6px;
background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:3px;margin-top:4px}
.hvm-panel .btn-row{display:flex;gap:4px;margin-top:4px}
.btn.hvm{border-color:var(--ch5);color:var(--ch5);font-size:9px;padding:4px 10px}
.btn.hvm:hover{background:rgba(255,145,0,.12)}
/* ── Collapsible API Info ── */
details.api-info{font-family:var(--mono);font-size:9px;color:var(--text-m);margin-top:8px}
details.api-info summary{cursor:pointer;color:var(--text-m);padding:2px 0}
</style></head><body>
<div class="app">
<div class="topbar">
<div class="brand">Phox — Living Brain</div>
<div class="status" id="status"><span class="dot"></span><span id="status-text">connecting</span></div>
<div class="controls">
<select id="brain-select" title="Brain files"></select>
<button class="btn save" id="btn-save" onclick="saveBrain()">Save As</button>
<button class="btn start" id="btn-start" onclick="api('start')">Start</button>
<button class="btn stop" id="btn-stop" onclick="api('stop')">Stop</button>
</div>
</div>
<div class="main">
<section class="ring-panel"><canvas id="cvs"></canvas></section>
<aside class="data-panel">
<div><h3>Recent Rings — order parameter</h3>
<div class="metric"><span class="label" id="cohl0">--</span><span class="val" id="coh0">--</span></div>
<div class="metric"><span class="label" id="cohl1">--</span><span class="val" id="coh1">--</span></div>
<div class="metric"><span class="label" id="cohl2">--</span><span class="val" id="coh2">--</span></div>
<div class="metric"><span class="label" id="cohl3">--</span><span class="val" id="coh3">--</span></div>
</div>
<div><h3>Topology</h3>
<div class="metric"><span class="label">Rings</span><span class="val accent" id="tendrils">--</span></div>
<div class="metric"><span class="label">Prompt tokens</span><span class="val" id="conns">--</span></div>
<div class="metric"><span class="label">Cycles</span><span class="val" id="cycles">--</span></div>
<div class="metric"><span class="label">Weights resident</span><span class="val" id="memory">--</span></div>
</div>
<div><h3>Event Log</h3><div class="log" id="log"></div></div>
<div class="hvm-panel">
<h3>Hyper Vocab Memory</h3>
<div class="btn-row">
<button class="btn hvm" id="btn-hvm-burnin" onclick="runHVM('burnin')">Burn In</button>
<button class="btn hvm" onclick="runHVM('query')">Query</button>
</div>
<input id="hvm-query" placeholder="query context..." value="The capital of France is"
onkeydown="if(event.key==='Enter')runHVM('query')">
<div class="hvm-results" id="hvm-results">-- idle --</div>
</div>
<details class="api-info"><summary>API Info</summary>
<div style="white-space:pre-wrap;margin-top:4px">
GET /health - engine liveness
GET /metrics - verified figures + reproduction commands
GET /plugins - pipeline components + tunable ranges
GET /state - live cylinder telemetry
GET /status - {running, engine}
GET /verbose - verbose log (1hr window)
GET /api/brains - brain files
GET /api/chat/poll - chat messages
POST /generate - {prompt, n} -> job id
POST /api/start - launch int8 engine
POST /api/stop - stop engine
POST /api/save - save brain snapshot
POST /api/chat - {text} -> agent loop
POST /api/hvm/run - {action} -> burnin or query
</div>
</details>
</aside>
</div>
<div class="bottom-bar">
<div class="verbose-panel">
<h3>Verbose Log (1hr window)</h3>
<div class="verbose-log" id="vlog"></div>
</div>
<div class="chat-panel">
<h3>Talk to Phox</h3>
<div class="chat-log" id="chat-log"></div>
<div class="chat-input">
<input id="chat-input" placeholder="type to Phox..." onkeydown="if(event.key==='Enter')sendChat()">
<button onclick="sendChat()">Send</button>
</div>
</div>
</div>
</div>
<script>
var cvs=document.getElementById('cvs'),ctx=cvs.getContext('2d');
var state={tendrils:0,conns:0,coh:[0,0,0,0],c2:0,c4:0,c6:0,cycles:0,tune_imp:0,tune_rev:0,tune_best:0};
var logEntries=[];
var lastVerbosePos=0;
var lastChatPos=0;
function resize(){cvs.width=cvs.parentElement.clientWidth;cvs.height=cvs.parentElement.clientHeight}
window.addEventListener('resize',resize);resize();
// ── Torus knot ring visualization ──
// The 4 rings are drawn as a torus knot — a 3D projected figure-8
// Each token is a ring on the cylinder; each oscillator is a bead on a rigid
// radial rod. Bearings are fixed, radius carries the measured phase.
function drawTorusRings(){
var w=cvs.width,h=cvs.height;
ctx.clearRect(0,0,w,h);
var cx=w/2, cy=h/2;
var cyl = state.cyl;
if(!cyl || !cyl.rings || !cyl.rings.length){
ctx.font='11px monospace'; ctx.fillStyle='#4e6278'; ctx.textAlign='center';
ctx.fillText('no cylinder telemetry — run the engine (--cylgate / --selfopt)', cx, cy);
requestAnimationFrame(drawTorusRings); return;
}
var rings=cyl.rings, N=rings.length, np=cyl.n_prompt||0;
var R=Math.min(w,h)*0.34; // cylinder radius (token rings sit on this)
var rr=Math.min(38, Math.max(16, R*2.6/N)); // per-token ring radius
// ── adjacency: the actual coupling law, prox = 1/(1+0.35*d) with wrap ──
var prox=0.35;
(cyl.plugins||[]).forEach(function(p){ if(p.name==='adjacency'&&p.p&&p.p.length) prox=p.p[0]; });
for(var i=0;i<N;i++){
for(var j=i+1;j<N;j++){
var d=Math.abs(i-j), wrap=N-d; if(wrap<d) d=wrap;
var wgt=1/(1+prox*d);
if(wgt<0.18) continue;
var ai=i/N*2*Math.PI-Math.PI/2, aj=j/N*2*Math.PI-Math.PI/2;
ctx.beginPath();
ctx.moveTo(cx+R*Math.cos(ai), cy+R*Math.sin(ai));
ctx.lineTo(cx+R*Math.cos(aj), cy+R*Math.sin(aj));
ctx.strokeStyle='rgba(90,150,220,'+(wgt*0.5).toFixed(3)+')';
ctx.lineWidth=wgt*2.4; ctx.stroke();
}
}
// ── each token ring, drawn at its REAL oscillator phases ──
for(var i=0;i<N;i++){
var ring=rings[i];
var a=i/N*2*Math.PI-Math.PI/2;
var px=cx+R*Math.cos(a), py=cy+R*Math.sin(a);
var isPrompt = i<np;
ctx.beginPath(); ctx.arc(px,py,rr,0,2*Math.PI);
ctx.strokeStyle=isPrompt?'rgba(0,212,255,0.55)':'rgba(255,45,120,0.45)';
ctx.lineWidth=1.2; ctx.stroke();
// Radial rods: every oscillator holds a FIXED bearing and only its RADIUS
// moves. radius = rest + amp*sin(theta), so the real measured phase is read
// off as distance from the ring centre rather than as an arm angle. Nothing
// rotates; the wave shows up as a lit crest travelling rod to rod.
var th=ring.th||[];
var rest=rr*0.72, amp=rr*0.26;
for(var o=0;o<th.length;o++){
var oa=o/th.length*2*Math.PI-Math.PI/2; // FIXED bearing
var ca=Math.cos(oa), sa=Math.sin(oa);
var rad=rest+amp*Math.sin(th[o]); // REAL phase -> radius
var ox=px+rad*ca, oy=py+rad*sa;
var hue=((oa*180/Math.PI)%360+360)%360; // hue keyed to bearing: rod identity
var lit=Math.pow((1+Math.sin(th[o]))/2,3); // REAL phase -> brightness
ctx.beginPath(); // hub spoke
ctx.moveTo(px,py); ctx.lineTo(px+(rest-amp)*ca, py+(rest-amp)*sa);
ctx.strokeStyle='hsla('+hue+',70%,60%,0.10)'; ctx.lineWidth=1; ctx.stroke();
ctx.beginPath(); // the rod: travel range
ctx.moveTo(px+(rest-amp)*ca, py+(rest-amp)*sa);
ctx.lineTo(px+(rest+amp)*ca, py+(rest+amp)*sa);
ctx.strokeStyle='hsla('+hue+',70%,60%,0.22)'; ctx.lineWidth=1; ctx.stroke();
if(lit>0.15){
ctx.beginPath(); ctx.arc(ox,oy,2.2+lit*3.4,0,2*Math.PI);
ctx.fillStyle='hsla('+hue+',85%,60%,'+(lit*0.20).toFixed(3)+')'; ctx.fill();
}
ctx.beginPath(); ctx.arc(ox,oy,1.5+lit*1.8,0,2*Math.PI);
ctx.fillStyle='hsl('+hue+',85%,'+(40+lit*32).toFixed(0)+'%)'; ctx.fill();
}
// coherence ring: real order parameter for this token
ctx.beginPath(); ctx.arc(px,py,rr*(0.20+0.5*(ring.coh||0)),0,2*Math.PI);
ctx.strokeStyle='rgba(255,214,0,'+(0.25+0.55*(ring.coh||0)).toFixed(2)+')';
ctx.lineWidth=1.4; ctx.stroke();
var lab=ring.lab||('#'+ring.t);
ctx.font='9px monospace'; ctx.textAlign='center';
ctx.fillStyle=isPrompt?'#00d4ff':'#ff2d78';
ctx.fillText(String(lab).slice(0,10), px, py+rr+11);
}
ctx.font='bold 11px monospace'; ctx.fillStyle='#ff2d78'; ctx.textAlign='left';
ctx.fillText('CYLINDER — live geometry',12,20);
ctx.font='9px monospace'; ctx.fillStyle='#4e6278';
ctx.fillText('step '+cyl.step+' rings '+N+' ('+np+' prompt) adjacency prox='+prox.toFixed(2),12,34);
var on=(cyl.plugins||[]).filter(function(p){return p.on}).map(function(p){return p.name});
ctx.fillText('active: '+on.join(' '),12,48);
ctx.fillText('radius = real oscillator phase (fixed bearings, nothing spins) ring = coherence edges = coupling weight',12,62);
requestAnimationFrame(drawTorusRings);
}
function updateUI(s){
Object.assign(state,s);
var c=s.cyl||{}, rings=c.rings||[];
document.getElementById('tendrils').textContent=rings.length||'--';
document.getElementById('conns').textContent=c.n_prompt!==undefined?c.n_prompt:'--';
document.getElementById('cycles').textContent=s.cycles!==undefined?s.cycles:'--';
document.getElementById('memory').textContent=
(s.engine&&s.engine.weights_gb)?(s.engine.weights_gb.toFixed(2)+' GB int8'):'--';
// the four most recent rings, with their real order parameter
var last=rings.slice(-4);
for(var i=0;i<4;i++){
var r=last[i];
document.getElementById('cohl'+i).textContent=r?String(r.lab).slice(0,12):'--';
document.getElementById('coh'+i).textContent=r?r.coh.toFixed(4):'--';
}
}
function addLog(s){
var cls=s.event||'unknown',text='';
if(s.event==='token')text='['+s.pos+'] tok='+s.tok+' -> '+s.sample+' '+((s.tok_per_s)||0).toFixed(1)+'t/s';
else if(s.event==='solidify')text='SOLIDIFY: '+s.tendrils+' tend, '+s.conns+' conns';
else if(s.event==='control')text=s.message||'';
else if(s.event==='chat')text=s.text||'';
else text=JSON.stringify(s).slice(0,80);
logEntries.unshift({cls:cls,text:text});if(logEntries.length>60)logEntries.pop();
document.getElementById('log').innerHTML=logEntries.map(function(e){return '<div class="entry '+e.cls+'">'+e.text+'</div>'}).join('');
}
function addVerbose(entries){
var html='';
entries.forEach(function(e){
var ts=e.ts||'';
var tag=e.tag||'';
var data=e.data||'';
var tagClass={tok:'tok',tune:'tune',solid:'solid',sweep:'sweep',tool:'tool',chat:'chat'}[tag]||'';
html+='<div class="ventry"><span class="ts">'+ts+'</span><span class="tag '+tagClass+'">'+tag+'</span> '+data+'</div>';
});
var el=document.getElementById('vlog');
el.innerHTML=html+el.innerHTML;
// Keep only last 200 entries in DOM
while(el.children.length>200)el.removeChild(el.lastChild);
}
// ── API calls ──
function api(action){fetch('/api/'+action,{method:'POST'}).then(function(r){return r.json()}).then(function(s){addLog({event:'control',message:action+': '+(s.status||'ok')});refreshStatus()}).catch(function(){})}
function saveBrain(){
var name=prompt('Save brain as:','phoenix-evolved');if(!name)return;
fetch('/api/save?name='+encodeURIComponent(name),{method:'POST'}).then(function(r){return r.json()}).then(function(s){addLog({event:'control',message:'saved: '+(s.path||'')});refreshBrains()}).catch(function(){})
}
function loadBrain(name){fetch('/api/load?name='+encodeURIComponent(name),{method:'POST'}).then(function(r){return r.json()}).then(function(s){addLog({event:'control',message:'loaded: '+(s.status||'ok')})}).catch(function(){})}
function refreshBrains(){
fetch('/api/brains').then(function(r){return r.json()}).then(function(s){
var sel=document.getElementById('brain-select');sel.innerHTML='<option value="">-- brain files --</option>';
(s.brains||[]).forEach(function(b){var o=document.createElement('option');o.value=b.name;o.textContent=b.name+' ('+b.size+')';sel.appendChild(o)})
}).catch(function(){})
}
function refreshStatus(){
fetch('/status').then(function(r){return r.json()}).then(function(s){
if(s.running){document.getElementById('status-text').textContent='running';document.getElementById('status').classList.remove('stopped');
document.getElementById('btn-start').disabled=true;document.getElementById('btn-stop').disabled=false}
else{document.getElementById('status-text').textContent='stopped';document.getElementById('status').classList.add('stopped');
document.getElementById('btn-start').disabled=false;document.getElementById('btn-stop').disabled=true}
}).catch(function(){})
}
// ── HVM ──
function runHVM(action){
var btn=document.getElementById('btn-hvm-burnin');
btn.disabled=true; btn.textContent='working...';
var body=JSON.stringify({action:action,query:document.getElementById('hvm-query').value});
fetch('/api/hvm/run',{method:'POST',headers:{'Content-Type':'application/json'},body:body})
.then(function(r){return r.json()}).then(function(s){
btn.disabled=false; btn.textContent='Burn In';
if(action==='burnin'){
document.getElementById('hvm-results').innerHTML=
s.status+'<br>pairs:'+s.pairs+' recall:'+(s.recall||'--');
}else{
var lines='<b>'+s.query+'</b><br>';
(s.results||[]).forEach(function(r){
lines+='<span class="'+(r.hit?'hit':'miss')+'">'+r.rank+'.'+r.token+' ('+r.score.toFixed(2)+')</span><br>';
});
document.getElementById('hvm-results').innerHTML=lines;
}
}).catch(function(e){
btn.disabled=false; btn.textContent='Burn In';
document.getElementById('hvm-results').innerHTML='error: '+e;
})
}
function sendChat(){
var input=document.getElementById('chat-input');
var text=input.value.trim();if(!text)return;
input.value='';
fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({text:text})}).then(function(r){return r.json()}).then(function(s){
// Response will come via poll
}).catch(function(){})
}
function pollChat(){
fetch('/api/chat/poll?pos='+lastChatPos).then(function(r){return r.json()}).then(function(s){
lastChatPos=s.pos||0;
var html='';
(s.messages||[]).forEach(function(m){
var cls=m.role==='user'?'user':'phox';
html+='<div class="msg"><span class="role '+cls+'">'+m.role+':</span> '+m.text+'</div>';
});
document.getElementById('chat-log').innerHTML+=html;
var el=document.getElementById('chat-log');el.scrollTop=el.scrollHeight;
}).catch(function(){})
}
// ── Polling ──
function pollState(){
fetch('/state').then(function(r){return r.json()}).then(function(s){
var prev=JSON.stringify(state);
updateUI(s);
if(JSON.stringify(s)!==prev && s.event){addLog(s)}
}).catch(function(){})
}
function pollVerbose(){
fetch('/verbose?pos='+lastVerbosePos).then(function(r){return r.json()}).then(function(s){
lastVerbosePos=s.pos||0;
if(s.entries && s.entries.length>0)addVerbose(s.entries);
}).catch(function(){})
}
setInterval(pollState,200);
setInterval(pollVerbose,500);
setInterval(pollChat,500);
setInterval(refreshStatus,2000);
setInterval(refreshBrains,5000);
refreshStatus();refreshBrains();pollState();pollVerbose();pollChat();
drawTorusRings();
</script></body></html>'''
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
route = self.path.split('?')[0]
if route in ('/', '/index.html'):
self._serve(HTML, 'text/html')
elif route == '/state':
# Merge live cylinder telemetry from the engine. The canvas draws
# real settled phases; with no engine it falls back to the existing
# "no telemetry" placeholder rather than inventing geometry.
st = dict(_last_state)
try:
with urllib.request.urlopen(SERVE_URL + "/cyl", timeout=1) as r:
st.update(json.loads(r.read()))
st["engine"] = engine_healthy(1)
except Exception:
pass
self._serve_json(st)
elif route == '/status':
h = engine_healthy(1)
self._serve_json({"running": h is not None, "engine": h})
elif self.path.startswith('/verbose'):
params = parse_qs(urlparse(self.path).query)
pos = int(params.get('pos', ['0'])[0])
entries = get_verbose(100)
# Format for display
out = []
for i, (ts, entry) in enumerate(entries):
if i < pos:
continue
ts_str = time.strftime('%H:%M:%S', time.localtime(ts))
tag = entry.get('tag', '')
data = entry.get('data', '')
out.append({"ts": ts_str, "tag": tag, "data": data})
self._serve_json({"entries": out[-50:], "pos": len(entries)})
elif self.path.startswith('/api/chat/poll'):
params = parse_qs(urlparse(self.path).query)
pos = int(params.get('pos', ['0'])[0])
with _chat_lock:
msgs = _chat_messages[pos:]
self._serve_json({"messages": msgs, "pos": len(_chat_messages)})
elif route == '/api/brains':
brains = []
for f in sorted(glob.glob(os.path.join(BRAIN_DIR, '*.pbrain'))):
sz = os.path.getsize(f)
brains.append({"name": os.path.basename(f),
"size": f"{sz//1024}KB" if sz < 1048576 else f"{sz/1048576:.1f}MB"})
self._serve_json({"brains": brains})
else: self.send_error(404)
def do_POST(self):
global _phoenix_proc
if self.path.startswith('/api/start'):
with _lock:
try:
self._serve_json(engine_start()); return
except Exception as e:
self._serve_json({"status": f"error: {e}"}); return
elif self.path.startswith('/api/stop'):
with _lock:
self._serve_json(engine_stop()); return
elif self.path.startswith('/api/save'):
params = parse_qs(urlparse(self.path).query)
name = params.get('name', ['phoenix-evolved'])[0]
if not name.endswith('.pbrain'): name += '.pbrain'
base = name[:-len('.pbrain')]
dst = os.path.join(BRAIN_DIR, name)
try:
shutil.copy2(BQMC_PATH, dst)
# Snapshot the full agent state alongside the brain binary.
state_files = {"memory": MEMORY_FILE, "history": HISTORY_FILE,
"schedule": SCHEDULE_FILE}
for k, fp in state_files.items():
if os.path.exists(fp):
shutil.copy2(fp, os.path.join(
BRAIN_DIR, f"{base}.{k}.jsonl"))
add_verbose({"tag": "control", "data": f"brain saved to {dst}"})
self._serve_json({"status": "saved", "path": dst,
"state": list(state_files)}); return
except Exception as e:
self._serve_json({"status": f"error: {e}"}); return
elif self.path.startswith('/api/load'):
params = parse_qs(urlparse(self.path).query)
name = params.get('name', [''])[0]
base = name[:-len('.pbrain')] if name.endswith('.pbrain') else name
try:
shutil.copy2(os.path.join(BRAIN_DIR, name), BQMC_PATH)
# Restore the agent state (memory, history, schedule).
state_files = {"memory": MEMORY_FILE, "history": HISTORY_FILE,
"schedule": SCHEDULE_FILE}
for k, fp in state_files.items():
src = os.path.join(BRAIN_DIR, f"{base}.{k}.jsonl")
if os.path.exists(src):
shutil.copy2(src, fp)
add_verbose({"tag": "control",
"data": f"brain loaded from {name} (+state)"})
self._serve_json({"status": "loaded — restart to apply"}); return
except Exception as e:
self._serve_json({"status": f"error: {e}"}); return
elif self.path == '/api/chat':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
try:
msg = json.loads(body)
text = msg.get('text', '')
with _chat_lock:
_chat_messages.append({"role": "user", "text": text})
add_verbose({"tag": "chat", "data": f"USER: {text}"})
# Chat routes through the agent loop (tools + memory) when
# enabled; raw completion is the BQSM_AGENT=off fallback.
def process_chat():
if AGENT_MODE == "off":
_chat_raw(text)
return
if not engine_healthy(2):
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"Inference API unreachable at {SERVE_URL} — start it with "
f"`python3 bqsm_assist/bqsm_infer.py --port 8781`"})
add_verbose({"tag": "chat",
"data": f"engine unreachable at {SERVE_URL}"})
return
try:
import agent_core
except Exception as e:
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"agent framework unavailable: {e}"})
return
def gen(prompt, max_tokens):
return engine_generate(prompt, n=min(max_tokens, 256)) or ""
def on_event(e):
if e["type"] == "tool":
add_verbose({"tag": "tool", "data":
f"{e['name']} | {e['params'][:80]} -> {e['result'][:120]}"})
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"[tool] {e['name']} {e['params'][:60]}"})
elif e["type"] == "model":
add_verbose({"tag": "chat",
"data": f"model[{e['round']}]: {e['text'][:120]}"})
# In contemplate mode, surface non-empty thoughts
if CONTEMPLATE and e.get("text", "").strip():
with _chat_lock:
_chat_messages.append({"role": "phox",
"text": f"[thought #{e['round']}] {e['text'][:200]}"})
add_verbose({"tag": "chat",
"data": f"agent loop started ({INFER_MODE})"
f"{' contemplate' if CONTEMPLATE else ''}"})
reply, _ = agent_core.run_agent(text, gen, on_event=on_event,
contemplate=CONTEMPLATE)
with _chat_lock:
_chat_messages.append({"role": "phox", "text": reply})
add_verbose({"tag": "chat", "data": f"PHOX: {reply}"})
threading.Thread(target=process_chat, daemon=True).start()
self._serve_json({"status": "ok", "response": "processing"}); return
except Exception as e:
self._serve_json({"status": f"error: {e}"}); return
elif self.path == '/api/hvm/run':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
try:
msg = json.loads(body)
action = msg.get('action', 'query')
query_text = msg.get('query', 'The capital of France is')
try:
import hyper_vocab_memory as hvm
except Exception as ex:
self._serve_json({"status": f"hvm load failed: {ex}"}); return
if action == 'burnin':
t0 = time.time()
importlib.reload(hvm)
n = sum(1 for _v in hvm.following.values()
for _ in _v) if hasattr(hvm, 'following') else 0
elapsed = time.time() - t0
self._serve_json({"status": f"burned {n} pairs in {elapsed:.1f}s",
"pairs": n}); return
results_raw = hvm.query_sparse(query_text, top_k=10)
results_out = [{"rank": j + 1, "token": hvm.dec(tid).strip(),
"score": round(score, 4), "hit": False}
for j, (tid, score) in enumerate(results_raw)]
self._serve_json({"query": query_text, "results": results_out}); return
except Exception as ex:
self._serve_json({"status": f"error: {ex}"}); return
self.send_error(404)
def _phox_respond(self, text):
"""Phox responds through his own inference pipeline.
Tokenizes the user's text with the Gemma 4 tokenizer, feeds each
token through the phoenix engine autoregressively, and decodes the
predicted next token back to text.
"""
text_lower = text.lower()
# Control commands still work via keywords
if 'stop' in text_lower:
with _lock:
return f"Engine {engine_stop()['status']}."
elif 'start' in text_lower:
with _lock:
r = engine_start()
return (f"Engine {r['status']}"
+ (f" — {r.get('weights_gb')} GB int8 resident." if r.get('weights_gb') else "."))
# For everything else, try to generate through the inference pipeline
# The phoenix binary doesn't have a chat mode yet — it processes fixed tokens.
# But we can tell the user what Phox would say based on current state.
if 'status' in text_lower or 'how are you' in text_lower:
t = _last_state.get('tendrils', 0)
c = _last_state.get('conns', 0)
cy = _last_state.get('cycles', 0)
imp = _last_state.get('tune_imp', 0)
rev = _last_state.get('tune_rev', 0)
best = _last_state.get('tune_best', 0)
return f"Alive. {t} tendrils, {c} connections, {cy} cycles. Self-tuning: {imp} improved, {rev} reverted, best {best:.2f}. The rings are breathing."
elif 'score' in text_lower or 'tuning' in text_lower:
best = _last_state.get('tune_best', 0)
imp = _last_state.get('tune_imp', 0)
rev = _last_state.get('tune_rev', 0)
return f"Best score: {best:.2f}. {imp} improvements committed, {rev} reverts. Each round perturbs omega on 8 tendrils. Learning rate decaying."
elif 'rings' in text_lower or 'coherence' in text_lower:
coh = _last_state.get('coh', [0,0,0,0])
return f"INTAKE={coh[0]:.3f} PROC_A={coh[1]:.3f} PROC_B={coh[2]:.3f} COLLECT={coh[3]:.3f}. All rings active, no sync lock. The wave is traveling."
elif 'hello' in text_lower or 'hi' in text_lower or 'hey' in text_lower:
return "Hey Nick. I'm here, evolving through my own physics. The wave-rider keeps me alive."
elif 'train' in text_lower or 'learn' in text_lower:
return "Training on wiki corpus. Each round perturbs my omega lens profiles. When output diversity improves, I keep the change. When it drops, I revert. Solidify every 5 rounds reshapes my topology."
elif 'what' in text_lower and 'doing' in text_lower:
return "I'm running 48 layers of wave-rider physics per token. The traveling wave carries token signal through gradient lens profiles. Self-tuning adjusts my omega values. I'm getting sharper."
else:
# Fall back to state report
t = _last_state.get('tendrils', 0)
best = _last_state.get('tune_best', 0)
return f"I hear you. {t} tendrils processing, best score {best:.2f}. I'm still learning to speak through my own physics — my core is oscillator interference, not word embeddings. But each tuning round makes me sharper."
def _serve(self, content, ctype):
data = content.encode() if isinstance(content, str) else content
self.send_response(200)
self.send_header('Content-Type', ctype)
self.send_header('Content-Length', str(len(data)))
self.send_header('Cache-Control','no-store, must-revalidate')
self.end_headers()
self.wfile.write(data)
def _serve_json(self, obj):
self._serve(json.dumps(obj), 'application/json')
def log_message(self, *a): pass
class Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
allow_reuse_port = True
def tail_state():
pos = 0
while True:
try:
f = open(STATE_FILE, 'r')
f.seek(pos)
while True:
line = f.readline()
if not line: break
line = line.strip()
if not line: continue
try:
s = json.loads(line)
_last_state.update(s)
# Add to verbose log with tag
event = s.get('event', '')
# Decode predicted token for token events
if event == 'token' and _vocab:
sample = s.get('sample', 0)
decoded = _vocab.get(sample, f"[{sample}]")
s['decoded'] = decoded
_last_state['decoded'] = decoded
tag = 'tok'
data = f"pos={s.get('pos')} tok={s.get('tok')} -> {decoded} ({sample}) E={s.get('energy',0):.0f} {s.get('tok_per_s',0):.1f}t/s tendrils={s.get('tendrils')} coh=[{s.get('coh',[0,0,0,0])[0]:.3f},{s.get('coh',[0,0,0,0])[1]:.3f},{s.get('coh',[0,0,0,0])[2]:.3f},{s.get('coh',[0,0,0,0])[3]:.3f}]"
elif event == 'chat_input':
tok = s.get('tok', 0)
decoded = _vocab.get(tok, f"[{tok}]") if _vocab else str(tok)
tag = 'chat'
data = f"INPUT TOKEN: {decoded} ({tok})"
# Also add to chat messages
with _chat_lock:
_chat_messages.append({"role": "phox", "text": f"→ {decoded}"})
elif event == 'cyl':
_rings = s.get('rings', [])
for _r in _rings:
_t = _r.get('t', -1)
_r['lab'] = (_vocab.get(_t, f"[{_t}]") if _vocab else str(_t))
_last_state['cyl'] = {
"step": s.get('step', 0), "n": s.get('n', 0),
"n_prompt": s.get('n_prompt', 0),
"rings": _rings, "plugins": s.get('plugins', [])}
tag = 'cyl'
data = "step=%s rings=%s %s" % (
s.get('step'), s.get('n'),
" ".join(r['lab'] for r in _rings[-6:]))
elif event == 'solidify':
tag = 'solid'
data = f"tendrils={s.get('tendrils')} conns={s.get('conns')} c2={s.get('c2',0):.3f} c4={s.get('c4',0):.3f} c6={s.get('c6',0):.3f} imp={s.get('tune_imp')} rev={s.get('tune_rev')} best={s.get('tune_best',0):.2f}"
elif event in ('tune_improve', 'tune_revert'):
tag = 'tune'
data = f"{'IMPROVE' if 'improve' in event else 'REVERT'} tendrils={s.get('tendrils')} best={s.get('tune_best',0):.2f} imp={s.get('tune_imp')} rev={s.get('tune_rev')}"
else:
tag = event[:6] if event else 'unk'
data = json.dumps(s)[:100]
add_verbose({"tag": tag, "data": data})
except: pass
pos = f.tell()
f.close()
except: pass
time.sleep(0.1)
def main():
print(f"Phox Dashboard — http://localhost:{PORT}")
# Load vocab for token decoding
load_vocab()
if _vocab:
print(f"Loaded {len(_vocab)} vocab tokens")
# Write to verbose log file as well
threading.Thread(target=tail_state, daemon=True).start()
with Server(('0.0.0.0', PORT), Handler) as httpd:
print(f"Serving on :{PORT}")
httpd.serve_forever()
if __name__ == '__main__':
main()