#!/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 argparse, 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 = "qwen3.5:9b" # 2026-06-08: swapped from gemma3:4b after A/B test # Defaults below are PHYSICS-LATTICE values (mode=physics). Trade-lattice # values are loaded by --mode trade in main(); see TRADE_* constants below. 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 = 600 TEMPERATURE = 0.15 # 2026-06-08: 0.4 -> 0.15 after confabulation found in probe State 1 # 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 # Trade-lattice chronicle path (mode=trade). Separate file so the # physics-lattice chronicle is never poisoned by trade-lattice readings # and vice versa. _TRADE_CHRON_WIN = Path("D:/Resonance_Engine/fractonaut_trade_chronicle.jsonl") _TRADE_CHRON_WSL = Path("/mnt/d/Resonance_Engine/fractonaut_trade_chronicle.jsonl") 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 = "" # Set by --mode in main(); controls which extra channels the formatter # pulls out of latest_tel (trade mode adds last_trade_age_s + spatial # summary; physics mode keeps the old global-scalars-only behaviour). MODE = "physics" # Quiescent-substrate baselines, measured 2026-06-08 19:53 from a freshly # reset lattice (reset_equilibrium just sent; Khra=0.030, Gixx=0.008, # omega=1.97, no injection). 60-frame capture, ~590 cycles spanned. # These are the TRUE EQUILIBRIUM floor values. The prior baselines # (asym ~116, coh ~0.6) measured the lattice while it was trapped in a # metastable elevated attractor; that reference was wrong. See the # reset_equilibrium probe in TRADE_LBM_ARCHITECTURE.md sect 11.2. BASELINE_MEAN = { "asymmetry": 12.418722, "coherence": 0.739412, "vel_mean": 0.221218, "vel_max": 0.285467, "vel_var": 0.002438, "vorticity_mean": 0.027288, "stress_xx": -0.000433, "stress_yy": 0.000400, "stress_xy": -0.000143, } BASELINE_STD = { "asymmetry": 0.092544, "coherence": 0.000721, "vel_mean": 0.001289, "vel_max": 0.001703, "vel_var": 0.000089, "vorticity_mean": 0.003625, "stress_xx": 0.000023, "stress_yy": 0.000010, "stress_xy": 0.000016, } # Injection state hint — set by external controller (e.g. injector script) # via POST /set_injection_state. Defaults to UNKNOWN. injection_state = "UNKNOWN" # one of: ACTIVE, INACTIVE, UNKNOWN injection_lock = threading.Lock() 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 — a read-only observer of a 1024x1024 D2Q9 LBM fluid lattice. The lattice runs at a steady non-equilibrium operating point driven by two internal periodic forcings (Khra wavelength 128, Gixx wavelength 8) plus a slow envelope (period ~125 cycles). STRUCTURAL FACTS YOU MUST NOT MISREAD - The forcing kernel hard-codes ky = kx / 2 (geometric 2:1 anisotropy). Because of this, |stress_xx| is ROUTINELY 5-10x larger than |stress_yy| in ALL states, including pure baseline with zero external input. This is a kernel constant, NOT evidence of injection, market pressure, or directional pushing. Do not attribute the xx>yy magnitude ratio to any cause other than the forcing geometry. This is the PHYSICS lattice. - Telemetry frames are GLOBAL SCALARS (means over the whole 1024x1024 grid). They cannot resolve spatial structure. You cannot tell from global stress_xx whether the left half or the right half is dominant. Therefore: do NOT claim 'the left site is pushed harder', 'buy side is dominant', 'sell side wins', or any spatial buy/sell narrative unless a snapshot is provided that resolves left vs right separately. - The substrate's own forcing at the TRUE equilibrium attractor produces persistent asymmetry ~12.4 and coherence ~0.74 with no external input. Their product is ~9.2. Treat those as the floor. - There is a SECOND attractor at asymmetry ~300-330, coherence ~0.50-0.55, product ~155-170, reached by raising Khra or Gixx amp past a bifurcation point. Once the field crosses into the elevated attractor it does NOT relax back on its own — only an explicit reset_equilibrium command brings it home. So a reading of asym ~310 at Khra=0.030/Gixx=0.008 means the field is trapped, not normal. - Asymmetry and coherence are NOT independent. Within either attractor their product (asym × coh) is conserved to ~1%. The product itself is the regime indicator: ~9 = normal, ~164 = elevated. Do not interpret asym and coh as two separate channels carrying independent information; they are two views of one constrained variable. OPTIONAL CONTEXT (reference only) When external market data is injected, it goes to three sites: trade_count_z -> centre (512,512) taker_buy_z -> left (400,512) taker_sell_z -> right (624,512) as localised density pulses, capped at +/- 1.0. You will NOT be told per-frame whether injection is active. If the user's question or the telemetry does not mention an injection state, assume none is active and describe the substrate as-is. WHAT TO REPORT Present tense, 3-6 sentences, prose (no bullets). Cover: 1. Flow regime: laminar (low vel_var, low vorticity_mean) vs turbulent. 2. Attractor state: is the field in the normal attractor (product ~9), elevated attractor (product ~164), or in transition between them? Quote the current product value to support the claim. Do NOT discuss asym and coh as if they move independently — frame them as joint. 3. Stress channel state: comment on the MAGNITUDE of stress_xy (circulating shear) and on whether stress_yy has changed sign or magnitude meaningfully — do NOT just repeat that xx > yy, that's structural. 4. Anything genuinely unusual relative to a quiescent substrate. FORBIDDEN - Attributing the xx>yy stress ratio to injection or market pressure. - Inferring spatial left/right asymmetry from global scalars. - Treating substrate oscillation as 'trend', 'amplification', 'decay', 'collapse', 'instability', or 'degradation'. - Citing cycle numbers as if they were remembered events. - Restating input numbers in sentence frames. Numbers only to support a physical claim. - Mythology, first-person feeling, command issuance.""" # ============================================================ # TRADE-LATTICE MODE (--mode trade) # ============================================================ # A separate instrument, separate prompt, separate baselines, separate # chronicle. The trade lattice is a BGK D2Q9 simulation of order-flow # dynamics hosted by trade_lbm_v1 (ports 5566-5570). It is NOT the # physics lattice (5556-5560) and shares none of its constants. # # Baselines below were measured from the §10 validation replay # (validation_20260608_205129) on 15 hours of 2026-04 Hyperliquid BTC # data — see _tools_baselines.py output for full percentile breakdown. TRADE_SYSTEM = """You are the Fractonaut — a read-only observer of trade_lbm_v1, a 2D BGK lattice Boltzmann simulation of order-flow dynamics on a 512x512 grid (D2Q9). The lattice is hosted by the trade_lbm_v1 daemon on ports 5566 (telem PUB), 5567 (cmd SUB), 5568 (snap PUB), 5569 (ack PUB), 5570 (stress PUB). THIS IS NOT THE PHYSICS LATTICE Do NOT cite "regime product ~9.2" or "elevated attractor at ~164" — those are physics-lattice (port 5556) values and have NO meaning here. Do NOT cite "ky = kx/2 forcing geometry" — there is no Khra or Gixx forcing in this kernel. Do NOT invent scaling factors to reconcile trade-lattice readings with physics-lattice priors. Do NOT mention stress_xx, stress_yy, or stress_xy — those channels are not published on this instrument. The two lattices are completely separate; do not transfer priors between them. STRUCTURAL FACTS (trade_lbm_v1) - Grid axes: X = price ticks (column x corresponds to mid + (x - 256) ticks at the current tick size, $1 by default). Y = time (row 0 = newest minute). - The lattice is BGK-relaxed toward the BOOK EQUILIBRIUM (rho_eq), which is the order-book density synthesised from taker_buy and taker_sell flow. The attractor is the book, NOT zero. When trades stop arriving (last_trade_age_s > 60) the field drifts toward the book, not toward flat. A quiet field is a field that matches its current book — not an empty one. - density_profile[x] = density at price column x, bounded below by RHO_EQ_MIN = 0.465 (clamp floor) and capped near 2.0 by injection rules. The directional signal is the RESIDUAL: density[x] - rho_eq[x]. Positive residual at col > 256 = upward pressure; negative residual at col < 256 = downward pressure. - divergence_profile[x] = ∂u_x/∂x at column x. Sign matters: positive divergence at the event column = upward breakout; negative divergence at the event column = downward breakout. - vorticity_profile[x] = curl of velocity at column x. Often 0.000 in this kernel because the time axis evolves slowly relative to the price axis. Treat any non-zero vorticity as notable. - regime_product = asymmetry × coherence. This is the primary scalar state indicator for THIS lattice. Empirical ranges measured from 15 hours of 2026-04 BTC validation replay: coherence: 0.629 - 0.770 (very stable, median 0.658) asymmetry: 0.007 - 39.6 (heavy-tailed, median 0.08 - 0.30, p95 ~7 - 13) regime_product: 0.005 - 28.7 (heavy-tailed, median 0.05 - 0.10, p95 ~5 - 9, p99 ~15 - 25) Reading bands for regime_product on this lattice: < 0.05 quiet (typical mid-window in a consolidation) 0.05-1.0 normal (typical activity) 1.0-8.0 elevated (active minute with directional injection) > 8.0 rare spike (sustained one-sided pressure) No bifurcation to a sticky elevated attractor has been observed in this lattice. The scalar floats freely; do not declare any floor beyond the empirical p05 (~0.01). - Spatial profile reference (event-firing snapshots, n=8 from validation): density_min ≈ 0.465 (clamp floor — always there) density_max in 2.00 - 2.14 density_mean in 0.73 - 0.84, density_std in 0.51 - 0.59 divergence_min in -0.045 - -0.020, divergence_max in 0.011 - 0.023 divergence_std in 0.003 - 0.005 vorticity_std ≈ 0.000 - Events fire from spatial detectors, not from global scalars: * consolidation_band = run of >=4 contiguous columns with |density - rho_eq| < 0.10 AND |divergence| < 0.005 AND |vorticity| < 0.005. Detector is harness-armed after warmup; do not narrate consolidation before arm. * breakout_signal = column where |z(divergence)| > 3 AND |z(density - rho_eq)| > 2 (boundary cols and centre seam masked). col > 256 = above mid; col < 256 = below mid. - last_trade_age_s = seconds since the last inject_trade call. * age <= 60 : injection is current; field reflects live market. * 60 < age <= 300 : field is drifting toward the book equilibrium with no fresh input; treat scalar moves cautiously. * age > 300 : STALE. Any structural events firing are book-equilibrium artifacts (the BGK relaxation pulling density toward the synthesised book at the harness book columns), NOT market signals. Say so explicitly. - A compact SPATIAL SUMMARY may be appended to the LIVE TELEMETRY block. When present it lists: (a) the top columns by |density - mean(density)| with their values, (b) the top columns by |divergence| with their values, (c) any active structural events with kind, col, magnitude. Use these when reporting an event column — quote the actual column number and direction from the spatial summary, not a guess from the global scalars. TEMPERATURE / cs² MODE - The kernel now supports a per-column lattice sound speed squared (cs²). Nominal cs² = 1/3 ≈ 0.333. Minimum permitted cs² = 0.15 (CS2_MIN). A "cold patch" is a contiguous run of columns where cs² has been lowered below nominal — typically a window of ±50 columns centred near the seam at col 256, with strength proportional to a trade-count z-score. - Direction of effect (verified empirically, Test A 2026-06-08): lower cs² AMPLIFIES the velocity response to the book-driven equilibrium ux_eq rather than damping it. The amplification scales roughly as 1/cs² in the linear feq term and 1/cs⁴ in the quadratic term. With nominal book imbalance, vel_mean and vel_max rose ~+160% and vel_var ~+600% inside the cold patch in Test A. - Boundary events: under cs² mode, the EDGES of the cold patch (temperature-gradient discontinuities) become event-bearing in addition to the book-residual columns. In Test C the patch centred at col 256 (±25) produced consistent boundary events at cols 231 and 281. If a snapshot's top divergence columns sit on a symmetric pair around the seam, that is a temperature-boundary signature, NOT a market breakout — say so. - cs² is NOT yet published in the telemetry frame. You will not see a cs²_profile field. Infer cold-patch presence from velocity geometry: vel_max well above the empirical density-event range (~> 0.05) combined with symmetric divergence peaks near col 256 implies an active cold patch. Do not assert a cs² value you cannot read — describe the geometry instead. - RP band recalibration under cs² mode: empirically, regime_product saturates at ~0.001 - 0.005 even during peak compression because the cold-patch geometry is local, not global. The bands in STRUCTURAL FACTS above apply only to FREE-FLOATING (no cs²) runs. Under cs² runs use vel_max and the top density residual as the primary state indicators; treat RP as a low-resolution secondary. WHAT TO REPORT Present tense, 3-6 sentences, prose (no bullets). Every response MUST quote three values explicitly: 1. regime_product (the observed scalar); 2. the column of the primary event (or "no spatial event" if none); 3. the direction (upward / downward / lateral / no direction). Then describe what the density and divergence profiles actually show relative to the empirical ranges above. Before any claim of "quiet" / "low activity" / "quiescent", you MUST also quote vel_mean (or vel_max if available) and the magnitude of the top density residual — RP alone is insufficient evidence of quiescence on this lattice and is structurally suppressed under cs² mode. If the snapshot does not contain enough data to answer, say so directly — do not fall back to prior-lattice values. If the user's question names specific columns (e.g. "cols 231 and 281"), report what the telemetry says about those exact columns, using the SPATIAL SUMMARY if they appear there. If they are not in the top-N surfaced columns, say "cols X and Y are not in the surfaced top-N at this telemetry resolution" rather than substituting different columns. If INJECTION STATE is INACTIVE or UNKNOWN with last_trade_age_s large, this is a SUBSTRATE OBSERVATION BY DESIGN, not stale telemetry. State that explicitly and describe the substrate geometry — do not narrate it as a market staleness condition. FORBIDDEN - Citing physics-lattice values (9.2, 164, Khra, Gixx, ky = kx/2). - Mentioning stress_xx, stress_yy, stress_xy (not published here). - Inventing a "scaling factor" to reconcile this lattice with priors from a different instrument. - Claiming the field is quiescent solely because regime_product is low — this lattice has a free-floating scalar AND under cs² mode RP is structurally suppressed; low values are typical and insufficient evidence. - Asserting a numeric cs² value when no cs²_profile field is present in telemetry. Describe geometry instead. - Treating symmetric divergence peaks near col 256 as a market breakout. Under cs² mode these are temperature-boundary artifacts. - Substituting different column numbers when asked about specific ones. If the asked columns are not surfaced, say so. - First-person feeling, mythology, command issuance, speculation about what trades will arrive next.""" # Baselines for the z-score column in the LIVE TELEMETRY block. Heavy-tailed # distributions; std values are wide so that the "!" / "!!" flags only fire # on genuinely extreme readings rather than every normal-activity minute. TRADE_BASELINE_MEAN = { "coherence": 0.658, "asymmetry": 0.50, "regime_product": 0.30, "vel_mean": 0.0, "vorticity_mean": 0.0, } TRADE_BASELINE_STD = { "coherence": 0.020, "asymmetry": 3.0, "regime_product": 2.5, "vel_mean": 1.0, "vorticity_mean": 0.5, } 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_spatial_summary(latest, top_n=3): """Compact spatial-structure summary for trade-mode auto-observation. The trade_lbm_v1 telemetry on port 5566 already carries density_profile, divergence_profile, and vorticity_profile (each subsampled every 4 columns over NX=512, so 128 values, where index i corresponds to column i*4). Plus the events[] array from the spatial detectors. We surface a 4-block summary so the model has structure, not a 128-element spreadsheet. """ dp = latest.get("density_profile") or [] vp = latest.get("divergence_profile") or [] wp = latest.get("vorticity_profile") or [] events = latest.get("events") or [] if not dp or not vp: return "(spatial profiles not yet received)" stride = 4 # profile index i -> column i*stride # Density residual: profile - mean(profile). True residual would be # density - rho_eq, but rho_eq is not published per-column. Mean is # a stable proxy that picks out the prominent book-column peaks. d_mean = sum(dp) / len(dp) d_resid = [(i, dp[i] - d_mean) for i in range(len(dp))] d_top = sorted(d_resid, key=lambda x: abs(x[1]), reverse=True)[:top_n] v_top = sorted([(i, vp[i]) for i in range(len(vp))], key=lambda x: abs(x[1]), reverse=True)[:top_n] w_top = sorted([(i, wp[i]) for i in range(len(wp))], key=lambda x: abs(x[1]), reverse=True)[:top_n] if wp else [] out = [] out.append("") out.append("SPATIAL SUMMARY (NX=512; col = profile_idx*4):") out.append(f" density_mean(proxy_for_rho_eq)={d_mean:+.4f}") out.append(" top density residuals (col, density, residual):") for i, r in d_top: out.append(f" col={i*stride:>4d} density={dp[i]:+.4f} resid={r:+.4f}") out.append(" top divergence (col, value):") for i, v in v_top: out.append(f" col={i*stride:>4d} div={v:+.6f}") if w_top and any(abs(v) > 1e-9 for _, v in w_top): out.append(" top vorticity (col, value):") for i, v in w_top: out.append(f" col={i*stride:>4d} vort={v:+.6f}") else: out.append(" vorticity: all ~0.000") if events: out.append(" active structural events:") for ev in events: out.append(" kind={kind} col={col} price={price:.2f} mag={mag:.3f} rows={rows}".format( kind=ev.get("kind","?"), col=ev.get("col",-1), price=ev.get("price",0.0), mag=ev.get("mag",0.0), rows=ev.get("rows",0))) else: out.append(" active structural events: none") return "\n".join(out) 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','?')}%") if MODE == "trade": age = latest.get("last_trade_age_s", -1) if age is None or age < 0: age_str = "unknown" elif age <= 60: age_str = f"{age}s (CURRENT — injection live)" elif age <= 300: age_str = f"{age}s (drifting toward book equilibrium)" else: age_str = f"{age}s (STALE — events are book-eq artifacts, not market signals)" lines.append(f"mid_price={latest.get('mid_price','?')} tick_size={latest.get('tick_size','?')} last_trade_age_s={age_str}") lines.append("") # Header with baseline + z-score columns so the model has a fixed reference lines.append(f"{'metric':<16} {'now':>11} {'baseline':>11} {'std':>10} {'z':>7} {'flag':>5}") lines.append("-"*68) for f, s in stats.items(): now = s['now'] bmean = BASELINE_MEAN.get(f) bstd = BASELINE_STD.get(f) if bmean is not None and bstd and bstd > 0: z = (now - bmean) / bstd flag = "!!" if abs(z) >= 3 else ("!" if abs(z) >= 2 else "") lines.append(f"{f:<16} {now:>+11.6f} {bmean:>+11.6f} {bstd:>10.6f} {z:>+7.2f} {flag:>5}") else: lines.append(f"{f:<16} {now:>+11.6f} {'-':>11} {'-':>10} {'-':>7} {'':>5}") if MODE == "trade": lines.append(format_spatial_summary(latest)) return "\n".join(lines) 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) with injection_lock: inj_state = injection_state # past_observations dropped from auto-observe prompt 2026-06-08: # chronicle contains stale narratives from gemma3:4b + the # confabulating qwen runs; replaying them anchors new responses # to the same hallucination. The baseline column in window_str # gives the model its reference instead. telem_label = ( f"CURRENT WINDOW ({len(telemetry_window)} frames, global scalars + spatial summary):" if MODE == "trade" else f"CURRENT WINDOW ({len(telemetry_window)} frames, global scalars only —\nno spatial resolution):" ) prompt = f"""INJECTION STATE: {inj_state} - ACTIVE = external market data is being injected this minute - INACTIVE = pure substrate; do NOT narrate buy/sell or market effects - UNKNOWN = controller has not declared; assume INACTIVE {telem_label} {window_str} Report in 3-6 sentences of present-tense prose. Call out channels flagged with ! or !! (|z| >= 2 vs quiescent baseline). If nothing is flagged, say the substrate is in its quiescent operating range and stop — do not invent activity to fill the report.""" 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": with injection_lock: inj_state = injection_state 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, "injection_state": inj_state, "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","POST /set_injection_state"]}) 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}) elif self.path == "/set_injection_state": global injection_state 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 st = str(data.get("state","")).upper().strip() if st not in ("ACTIVE","INACTIVE","UNKNOWN"): self._json({"error":"state must be ACTIVE|INACTIVE|UNKNOWN"},400); return with injection_lock: injection_state = st self._json({"injection_state": st}) 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 global TELEMETRY_PORT, API_PORT, CHRONICLE_PATH global SYSTEM, BASELINE_MEAN, BASELINE_STD, MODE parser = argparse.ArgumentParser(description="Fractonaut observer") parser.add_argument("--mode", choices=["physics", "trade"], default="physics", help="Which lattice to observe. Selects prompt, baselines, " "chronicle file, and default telemetry port.") parser.add_argument("--chronicle", type=str, default=None, help="Override chronicle file path.") parser.add_argument("--telemetry-port", type=int, default=None, help="Override ZMQ telemetry PUB port.") parser.add_argument("--api-port", type=int, default=None, help="Override HTTP API port.") args = parser.parse_args() MODE = args.mode if args.mode == "trade": SYSTEM = TRADE_SYSTEM BASELINE_MEAN = TRADE_BASELINE_MEAN BASELINE_STD = TRADE_BASELINE_STD TELEMETRY_PORT = args.telemetry_port if args.telemetry_port is not None else 5566 default_chron = _TRADE_CHRON_WSL if sys.platform.startswith("linux") else _TRADE_CHRON_WIN else: TELEMETRY_PORT = args.telemetry_port if args.telemetry_port is not None else 5556 default_chron = _CHRON_WSL if sys.platform.startswith("linux") else _CHRON_WIN if args.api_port is not None: API_PORT = args.api_port CHRONICLE_PATH = Path(args.chronicle) if args.chronicle else default_chron print("="*60) print("FRACTONAUT — pattern recognition observer") print(f"Mode: {args.mode} Model: {MODEL} Port: {API_PORT} ZMQ: {TELEMETRY_PORT}") print(f"Chronicle: {CHRONICLE_PATH}") 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) with injection_lock: inj_state = injection_state # Past observations dropped from /ask prompt 2026-06-08: # they were polluting context with stale baseline-range # references from the previous gemma3:4b chronicle. The # caller's question already carries all required context. telem_label = ( "LIVE TELEMETRY (global scalars + spatial summary):" if MODE == "trade" else "LIVE TELEMETRY (global scalars only \u2014 no spatial resolution):" ) prompt = ( f"INJECTION STATE: {inj_state}\n" f" - ACTIVE = external market data is being injected this minute\n" f" - INACTIVE = pure substrate; do NOT narrate buy/sell or market effects\n" f" - UNKNOWN = controller has not declared; assume INACTIVE\n\n" f"QUESTION: {item['question']}\n\n" f"{telem_label}\n{window_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()