2026-06-06 15:34:27 +07:00
|
|
|
|
#!/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
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
import argparse, zmq, json, time, sys, os, queue, threading, signal
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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"
|
2026-06-08 15:34:31 +07:00
|
|
|
|
MODEL = "qwen3.5:9b" # 2026-06-08: swapped from gemma3:4b after A/B test
|
2026-06-08 21:34:30 +07:00
|
|
|
|
# Defaults below are PHYSICS-LATTICE values (mode=physics). Trade-lattice
|
|
|
|
|
|
# values are loaded by --mode trade in main(); see TRADE_* constants below.
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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
|
2026-06-08 15:34:31 +07:00
|
|
|
|
MAX_RESPONSE_TOKENS = 600
|
2026-06-08 17:34:31 +07:00
|
|
|
|
TEMPERATURE = 0.15 # 2026-06-08: 0.4 -> 0.15 after confabulation found in probe State 1
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
# 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")
|
|
|
|
|
|
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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 = ""
|
2026-06-08 21:34:30 +07:00
|
|
|
|
# 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"
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
2026-06-08 20:34:31 +07:00
|
|
|
|
# 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.
|
2026-06-08 18:34:31 +07:00
|
|
|
|
BASELINE_MEAN = {
|
2026-06-08 20:34:31 +07:00
|
|
|
|
"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,
|
2026-06-08 18:34:31 +07:00
|
|
|
|
}
|
|
|
|
|
|
BASELINE_STD = {
|
2026-06-08 20:34:31 +07:00
|
|
|
|
"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,
|
2026-06-08 18:34:31 +07:00
|
|
|
|
}
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-08 17:34:31 +07:00
|
|
|
|
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
|
2026-06-08 21:34:30 +07:00
|
|
|
|
any cause other than the forcing geometry. This is the PHYSICS lattice.
|
2026-06-08 17:34:31 +07:00
|
|
|
|
- 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.
|
2026-06-08 20:34:31 +07:00
|
|
|
|
- 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.
|
2026-06-08 17:34:31 +07:00
|
|
|
|
|
|
|
|
|
|
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.
|
2026-06-08 20:34:31 +07:00
|
|
|
|
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.
|
2026-06-08 17:34:31 +07:00
|
|
|
|
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."""
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 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.
|
|
|
|
|
|
|
2026-06-09 08:34:31 +07:00
|
|
|
|
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.
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
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
|
2026-06-09 08:34:31 +07:00
|
|
|
|
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.
|
2026-06-08 21:34:30 +07:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-09 08:34:31 +07:00
|
|
|
|
— 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.
|
2026-06-08 21:34:30 +07:00
|
|
|
|
- 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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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','?')}%")
|
2026-06-08 21:34:30 +07:00
|
|
|
|
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}")
|
2026-06-06 15:34:27 +07:00
|
|
|
|
lines.append("")
|
2026-06-08 18:34:31 +07:00
|
|
|
|
# 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)
|
2026-06-06 15:34:27 +07:00
|
|
|
|
for f, s in stats.items():
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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}")
|
2026-06-08 21:34:30 +07:00
|
|
|
|
if MODE == "trade":
|
|
|
|
|
|
lines.append(format_spatial_summary(latest))
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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)
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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.
|
2026-06-08 21:34:30 +07:00
|
|
|
|
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):"
|
|
|
|
|
|
)
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-08 21:34:30 +07:00
|
|
|
|
{telem_label}
|
2026-06-06 15:34:27 +07:00
|
|
|
|
{window_str}
|
|
|
|
|
|
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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."""
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
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":
|
2026-06-08 18:34:31 +07:00
|
|
|
|
with injection_lock:
|
|
|
|
|
|
inj_state = injection_state
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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,
|
2026-06-08 18:34:31 +07:00
|
|
|
|
"injection_state": inj_state,
|
2026-06-06 15:34:27 +07:00
|
|
|
|
"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,
|
2026-06-08 18:34:31 +07:00
|
|
|
|
"endpoints":["/status","/last","/chronicle?last=N",
|
|
|
|
|
|
"POST /ask","POST /set_injection_state"]})
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
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})
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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})
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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
|
2026-06-08 21:34:30 +07:00
|
|
|
|
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
|
2026-06-06 15:34:27 +07:00
|
|
|
|
|
|
|
|
|
|
print("="*60)
|
|
|
|
|
|
print("FRACTONAUT — pattern recognition observer")
|
2026-06-08 21:34:30 +07:00
|
|
|
|
print(f"Mode: {args.mode} Model: {MODEL} Port: {API_PORT} ZMQ: {TELEMETRY_PORT}")
|
|
|
|
|
|
print(f"Chronicle: {CHRONICLE_PATH}")
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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)
|
2026-06-08 18:34:31 +07:00
|
|
|
|
with injection_lock:
|
|
|
|
|
|
inj_state = injection_state
|
2026-06-08 15:34:31 +07:00
|
|
|
|
# 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.
|
2026-06-08 21:34:30 +07:00
|
|
|
|
telem_label = (
|
|
|
|
|
|
"LIVE TELEMETRY (global scalars + spatial summary):"
|
|
|
|
|
|
if MODE == "trade"
|
|
|
|
|
|
else "LIVE TELEMETRY (global scalars only \u2014 no spatial resolution):"
|
|
|
|
|
|
)
|
2026-06-08 15:34:31 +07:00
|
|
|
|
prompt = (
|
2026-06-08 18:34:31 +07:00
|
|
|
|
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"
|
2026-06-08 15:34:31 +07:00
|
|
|
|
f"QUESTION: {item['question']}\n\n"
|
2026-06-08 21:34:30 +07:00
|
|
|
|
f"{telem_label}\n{window_str}"
|
2026-06-08 15:34:31 +07:00
|
|
|
|
)
|
2026-06-06 15:34:27 +07:00
|
|
|
|
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()
|