Files
resonance-engine/fractonaut.py
T
2026-06-09 08:34:31 +07:00

834 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()