834 lines
36 KiB
Python
834 lines
36 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 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()
|