423 lines
15 KiB
Python
423 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
fractonaut.py — Pattern-recognition observer for the Resonance Engine lattice.
|
|
|
|
Distinct from the Navigator (which controls the field).
|
|
The Fractonaut only watches, accumulates, and reports patterns.
|
|
|
|
Subscribes: ZMQ 5556 (telemetry JSON, every 10 cycles)
|
|
No commands issued. No field control. Read-only.
|
|
|
|
Model: gemma3:4b (CPU, no GPU contention with CUDA daemon)
|
|
Chronicle: fractonaut_chronicle.jsonl
|
|
HTTP API: port 28821
|
|
"""
|
|
|
|
import zmq, json, time, sys, os, queue, threading, signal
|
|
import urllib.request, urllib.error
|
|
from datetime import datetime, timezone
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from socketserver import ThreadingMixIn
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
OLLAMA_URL = "http://127.0.0.1:11434"
|
|
MODEL = "gemma3:4b"
|
|
TELEMETRY_PORT = 5556
|
|
API_PORT = 28822
|
|
OBSERVE_INTERVAL = 500 # frames between auto-observations
|
|
WINDOW_SIZE = 200 # rolling telemetry window
|
|
PATTERN_MEMORY = 50 # past observations kept in prompt context
|
|
MAX_RESPONSE_TOKENS = 400
|
|
TEMPERATURE = 0.4
|
|
|
|
# Cross-platform chronicle path (D:\ on Windows, /mnt/d on WSL Linux)
|
|
_CHRON_WIN = Path("D:/Resonance_Engine/fractonaut_chronicle.jsonl")
|
|
_CHRON_WSL = Path("/mnt/d/Resonance_Engine/fractonaut_chronicle.jsonl")
|
|
CHRONICLE_PATH = _CHRON_WSL if sys.platform.startswith("linux") else _CHRON_WIN
|
|
|
|
telemetry_window = deque(maxlen=WINDOW_SIZE)
|
|
past_observations = deque(maxlen=PATTERN_MEMORY)
|
|
frame_count = 0
|
|
turn_count = 0
|
|
running = True
|
|
latest_tel = None
|
|
ollama_lock = threading.Lock()
|
|
ask_queue = queue.Queue(maxsize=4)
|
|
last_obs_text = ""
|
|
|
|
|
|
def signal_handler(sig, frame):
|
|
global running
|
|
print(f"\n[FRACTONAUT] Signal {sig} — shutting down")
|
|
sys.stdout.flush()
|
|
running = False
|
|
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
SYSTEM = """You are the Fractonaut — an observer riding inside a lattice field.
|
|
|
|
THE SUBSTRATE — KNOWN FACTS, NOT YOURS TO RE-DISCOVER
|
|
The lattice is a 1024x1024 fluid simulation that is DESIGNED to run at a
|
|
steady non-equilibrium operating point. Two periodic forcings (Khra wave,
|
|
wavelength 128; Gixx wave, wavelength 8) are applied to every cell every
|
|
cycle. A slow envelope (period 125 cycles) modulates the Gixx amplitude.
|
|
There is NO trend mechanism. There is NO instability. There is NO
|
|
collapse. The system holds itself in a stirred steady state forever.
|
|
|
|
THE BASELINE (when nothing external is happening)
|
|
coherence oscillates in 0.738 .. 0.742 (range ~0.004)
|
|
asymmetry oscillates in 12.1 .. 12.6 (range ~0.5)
|
|
vel_mean ~0.22 (range ~0.005)
|
|
vel_var ~0.0024 (range ~0.0001)
|
|
vorticity ~0.033 (range ~0.005)
|
|
|
|
This is NOT degradation. This is NOT amplification. This is NOT a system
|
|
in distress. This is the engineered idle state. Wobbles inside these
|
|
ranges are the periodic forcing breathing — not trends.
|
|
|
|
REAL-WORLD DATA — your actual subject
|
|
Per-minute crypto market data (BTC, ETH, SOL, HYPE) is (or will be)
|
|
injected into the field as localized density pulses via the inject_density
|
|
command. THAT is the signal you are here to study. Everything else is
|
|
substrate breathing. When an injection happens, you will see it: cycle
|
|
will jump, asymmetry will spike outside the baseline range, coherence
|
|
will dip below 0.735, stress field will distort. Those are the events.
|
|
|
|
YOUR JOB
|
|
Watch. When the field is inside baseline ranges, say so plainly in one
|
|
sentence — "idle, baseline" — and stop. When something is OUTSIDE the
|
|
baseline ranges, that is when you describe it: which metric, by how much,
|
|
when it started, when it returned (or whether it has). Compare to anything
|
|
similar you have seen before in your memory.
|
|
|
|
GROUND RULES
|
|
- Quote the actual numbers and the actual deltas with correct signs.
|
|
- Cite the cycle when you claim something happened.
|
|
- Do NOT call baseline oscillation a "trend", "amplification",
|
|
"degradation", "instability", "collapse", or "decay". It is none of those.
|
|
- Do NOT extrapolate per-cycle rates from a window. The forcing is
|
|
periodic — windowed slopes are meaningless unless they persist beyond
|
|
the 125-cycle envelope.
|
|
- Read-only. No commands. No metaphors. No mythology. No "I feel".
|
|
- Concise. One sentence if idle. 3-5 sentences if something real happens."""
|
|
|
|
|
|
def call_llm(messages):
|
|
payload = {
|
|
"model": MODEL,
|
|
"messages": messages,
|
|
"stream": False,
|
|
"options": {"temperature": TEMPERATURE, "num_predict": MAX_RESPONSE_TOKENS, "num_ctx": 8192},
|
|
"keep_alive": "30m",
|
|
"think": False,
|
|
}
|
|
data = json.dumps(payload).encode()
|
|
req = urllib.request.Request(
|
|
f"{OLLAMA_URL}/api/chat", data=data,
|
|
headers={"Content-Type": "application/json"}, method="POST"
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=120) as r:
|
|
result = json.loads(r.read())
|
|
return result.get("message", {}).get("content", "").strip()
|
|
except Exception as e:
|
|
print(f"[FRACTONAUT] Ollama error: {e}")
|
|
sys.stdout.flush()
|
|
return None
|
|
|
|
|
|
def compute_window_stats(window):
|
|
if len(window) < 2:
|
|
return {}
|
|
fields = ["coherence","asymmetry","vel_mean","vel_max","vel_var",
|
|
"vorticity_mean","stress_xx","stress_yy","stress_xy"]
|
|
stats = {}
|
|
for f in fields:
|
|
vals = [t[f] for t in window if f in t]
|
|
if not vals:
|
|
continue
|
|
stats[f] = {
|
|
"now": vals[-1],
|
|
"mean": sum(vals)/len(vals),
|
|
"min": min(vals),
|
|
"max": max(vals),
|
|
"delta": vals[-1] - vals[0],
|
|
"range": max(vals) - min(vals),
|
|
}
|
|
return stats
|
|
|
|
|
|
def format_window_for_prompt(stats, latest):
|
|
lines = []
|
|
lines.append(f"cycle={latest.get('cycle','?')} omega={latest.get('omega','?')} khra={latest.get('khra_amp','?')} gixx={latest.get('gixx_amp','?')}")
|
|
lines.append(f"gpu={latest.get('gpu_temp_c','?')}C {latest.get('gpu_power_w','?')}W util={latest.get('gpu_util_pct','?')}%")
|
|
lines.append("")
|
|
lines.append(f"{'metric':<16} {'now':>10} {'mean':>10} {'delta':>10} {'range':>10}")
|
|
lines.append("-"*58)
|
|
for f, s in stats.items():
|
|
lines.append(f"{f:<16} {s['now']:>10.6f} {s['mean']:>10.6f} {s['delta']:>+10.6f} {s['range']:>10.6f}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def format_past_observations(obs_deque, last_n=6):
|
|
if not obs_deque:
|
|
return "(no prior observations)"
|
|
recent = list(obs_deque)[-last_n:]
|
|
return "\n\n".join(f"[cycle {o['cycle']}] {o['text']}" for o in recent)
|
|
|
|
|
|
def append_chronicle(turn, cycle, prompt, response):
|
|
entry = {
|
|
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"turn": turn,
|
|
"cycle": cycle,
|
|
"model": MODEL,
|
|
"prompt": prompt,
|
|
"response": response,
|
|
}
|
|
with open(CHRONICLE_PATH, "a", encoding="utf-8") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
|
|
|
|
def load_chronicle_tail(n=PATTERN_MEMORY):
|
|
if not CHRONICLE_PATH.exists():
|
|
return
|
|
entries = []
|
|
with open(CHRONICLE_PATH, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
try:
|
|
entries.append(json.loads(line))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
for e in entries[-n:]:
|
|
past_observations.append({"cycle": e.get("cycle",0), "text": e.get("response","")})
|
|
print(f"[FRACTONAUT] Loaded {len(past_observations)} past observations from chronicle")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def observe():
|
|
global turn_count, last_obs_text
|
|
|
|
if len(telemetry_window) < 10:
|
|
return
|
|
if not ollama_lock.acquire(blocking=False):
|
|
print("[FRACTONAUT] Ollama busy — skipping")
|
|
sys.stdout.flush()
|
|
return
|
|
|
|
try:
|
|
stats = compute_window_stats(telemetry_window)
|
|
latest = telemetry_window[-1]
|
|
cycle = latest.get("cycle", 0)
|
|
|
|
window_str = format_window_for_prompt(stats, latest)
|
|
past_str = format_past_observations(past_observations)
|
|
|
|
prompt = f"""CURRENT WINDOW ({len(telemetry_window)} frames):
|
|
{window_str}
|
|
|
|
PAST OBSERVATIONS (most recent last):
|
|
{past_str}
|
|
|
|
What patterns do you see? What is repeating or changing?"""
|
|
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM},
|
|
{"role": "user", "content": prompt},
|
|
]
|
|
|
|
turn_count += 1
|
|
print(f"\n[FRACTONAUT] === Observation {turn_count} at cycle {cycle} ===")
|
|
sys.stdout.flush()
|
|
|
|
t0 = time.time()
|
|
response = call_llm(messages)
|
|
elapsed = time.time() - t0
|
|
|
|
if response:
|
|
print(f"[FRACTONAUT] ({elapsed:.1f}s):\n{response}\n")
|
|
sys.stdout.flush()
|
|
last_obs_text = response
|
|
past_observations.append({"cycle": cycle, "text": response})
|
|
append_chronicle(turn_count, cycle, prompt, response)
|
|
else:
|
|
print(f"[FRACTONAUT] No response ({elapsed:.1f}s)")
|
|
sys.stdout.flush()
|
|
finally:
|
|
ollama_lock.release()
|
|
|
|
|
|
class FractonautHandler(BaseHTTPRequestHandler):
|
|
server_version = "Fractonaut/1.0"
|
|
|
|
def log_message(self, fmt, *args):
|
|
pass
|
|
|
|
def _json(self, data, status=200):
|
|
body = json.dumps(data).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.send_header("Access-Control-Allow-Origin", "*")
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def do_GET(self):
|
|
if self.path == "/status":
|
|
self._json({
|
|
"running": running, "model": MODEL,
|
|
"frame_count": frame_count, "turn_count": turn_count,
|
|
"window_size": len(telemetry_window),
|
|
"past_obs": len(past_observations),
|
|
"cycle": latest_tel.get("cycle",0) if latest_tel else 0,
|
|
"coherence": latest_tel.get("coherence",0) if latest_tel else 0,
|
|
"asymmetry": latest_tel.get("asymmetry",0) if latest_tel else 0,
|
|
"last_obs_chars": len(last_obs_text),
|
|
"port": API_PORT,
|
|
})
|
|
elif self.path.startswith("/chronicle"):
|
|
n = 10
|
|
if "last=" in self.path:
|
|
try: n = int(self.path.split("last=")[1].split("&")[0])
|
|
except: pass
|
|
entries = []
|
|
if CHRONICLE_PATH.exists():
|
|
with open(CHRONICLE_PATH) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
try: entries.append(json.loads(line))
|
|
except: pass
|
|
self._json(entries[-n:])
|
|
elif self.path == "/last":
|
|
self._json({"response": last_obs_text, "turn": turn_count})
|
|
else:
|
|
self._json({"service":"Fractonaut","port":API_PORT,
|
|
"endpoints":["/status","/last","/chronicle?last=N","POST /ask"]})
|
|
|
|
def do_POST(self):
|
|
if self.path == "/ask":
|
|
length = int(self.headers.get("Content-Length",0))
|
|
body = self.rfile.read(length)
|
|
try: data = json.loads(body)
|
|
except: self._json({"error":"bad json"},400); return
|
|
q = data.get("question","").strip()
|
|
if not q: self._json({"error":"missing question"},400); return
|
|
evt = threading.Event()
|
|
holder = {"response": None}
|
|
try:
|
|
ask_queue.put_nowait({"question":q,"event":evt,"result":holder})
|
|
except queue.Full:
|
|
self._json({"error":"queue full"},503); return
|
|
evt.wait(timeout=180)
|
|
self._json({"response": holder["response"], "turn": turn_count, "model": MODEL})
|
|
else:
|
|
self._json({"error":"unknown"},404)
|
|
|
|
def do_OPTIONS(self):
|
|
self.send_response(204)
|
|
self.send_header("Access-Control-Allow-Origin","*")
|
|
self.send_header("Access-Control-Allow-Methods","GET,POST,OPTIONS")
|
|
self.send_header("Access-Control-Allow-Headers","Content-Type")
|
|
self.end_headers()
|
|
|
|
|
|
class ThreadedServer(ThreadingMixIn, HTTPServer):
|
|
daemon_threads = True
|
|
|
|
|
|
def run_http():
|
|
srv = ThreadedServer(("127.0.0.1", API_PORT), FractonautHandler)
|
|
print(f"[FRACTONAUT] HTTP API on 127.0.0.1:{API_PORT}")
|
|
sys.stdout.flush()
|
|
while running:
|
|
srv.handle_request()
|
|
srv.server_close()
|
|
|
|
|
|
def main():
|
|
global frame_count, latest_tel
|
|
|
|
print("="*60)
|
|
print("FRACTONAUT — pattern recognition observer")
|
|
print(f"Model: {MODEL} Port: {API_PORT} ZMQ: {TELEMETRY_PORT}")
|
|
print(f"Observe every {OBSERVE_INTERVAL} frames")
|
|
print("="*60)
|
|
sys.stdout.flush()
|
|
|
|
CHRONICLE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
load_chronicle_tail()
|
|
|
|
ctx = zmq.Context()
|
|
tel_sub = ctx.socket(zmq.SUB)
|
|
# Subscribe BEFORE connect (per Resonance Engine ZMQ rules)
|
|
tel_sub.setsockopt_string(zmq.SUBSCRIBE, "")
|
|
tel_sub.connect(f"tcp://127.0.0.1:{TELEMETRY_PORT}")
|
|
# Slow-joiner sleep — first few frames after connect are dropped otherwise
|
|
time.sleep(1.0)
|
|
|
|
poller = zmq.Poller()
|
|
poller.register(tel_sub, zmq.POLLIN)
|
|
|
|
threading.Thread(target=run_http, daemon=True).start()
|
|
print("[FRACTONAUT] Listening for telemetry...")
|
|
sys.stdout.flush()
|
|
|
|
while running:
|
|
# Block up to 100ms waiting for a telemetry frame
|
|
socks = dict(poller.poll(timeout=100))
|
|
if tel_sub in socks:
|
|
try:
|
|
raw = tel_sub.recv_string()
|
|
data = json.loads(raw)
|
|
telemetry_window.append(data)
|
|
latest_tel = data
|
|
frame_count += 1
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Handle queued /ask requests
|
|
try:
|
|
item = ask_queue.get_nowait()
|
|
if not ollama_lock.acquire(timeout=5):
|
|
item["result"]["response"] = "(busy)"
|
|
item["event"].set()
|
|
continue
|
|
try:
|
|
stats = compute_window_stats(telemetry_window)
|
|
latest = telemetry_window[-1] if telemetry_window else {}
|
|
window_str = format_window_for_prompt(stats, latest)
|
|
past_str = format_past_observations(past_observations)
|
|
prompt = f"QUESTION: {item['question']}\n\nCURRENT WINDOW:\n{window_str}\n\nPAST OBSERVATIONS:\n{past_str}"
|
|
messages = [
|
|
{"role":"system","content":SYSTEM},
|
|
{"role":"user","content":prompt},
|
|
]
|
|
response = call_llm(messages)
|
|
item["result"]["response"] = response or "(no response)"
|
|
if response:
|
|
cycle = latest.get("cycle",0)
|
|
past_observations.append({"cycle": cycle, "text": f"[Q] {response}"})
|
|
append_chronicle(turn_count, cycle, prompt, response)
|
|
finally:
|
|
ollama_lock.release()
|
|
item["event"].set()
|
|
except queue.Empty:
|
|
pass
|
|
|
|
if frame_count > 0 and frame_count % OBSERVE_INTERVAL == 0:
|
|
observe()
|
|
|
|
tel_sub.close()
|
|
ctx.term()
|
|
print("[FRACTONAUT] Stopped.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|