Auto-commit via make git (triggered by NFDOS)

This commit is contained in:
neoricalex 2025-11-30 13:20:55 +01:00
parent 705fa58f3c
commit 88f340af14
2 changed files with 285 additions and 27 deletions

View File

@ -15,6 +15,7 @@ from .hippocampus import Hippocampus
from .perception import Perception from .perception import Perception
from .motor import Motor from .motor import Motor
from .autodiagnostic import AutoDiagnostic from .autodiagnostic import AutoDiagnostic
from .telemetry import TelemetryV5 # <-- NOVO
from .neurotron_config import ( from .neurotron_config import (
NEUROTRON_MODE, NEUROTRON_TICK, NEUROTRON_MODE, NEUROTRON_TICK,
@ -22,7 +23,6 @@ from .neurotron_config import (
NEUROTRON_DIAG_EVERY_TICKS, NEUROTRON_DIAG_EVERY_TICKS,
NEUROTRON_DATASET_PATH, NEUROTRON_DATASET_PATH,
HEARTBEAT_ENABLED, HEARTBEAT_ENABLED,
NEUROTRON_THRESHOLDS,
TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS, TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS,
) )
@ -32,7 +32,6 @@ class Cortex:
self.runtime_dir = Path(runtime_dir) self.runtime_dir = Path(runtime_dir)
self.log_dir = Path(log_dir) self.log_dir = Path(log_dir)
# tick pode vir como string → convertemos sempre
try: try:
self.tick = float(tick_seconds) self.tick = float(tick_seconds)
except: except:
@ -55,13 +54,16 @@ class Cortex:
EchoAgent(self), EchoAgent(self),
] ]
# ---- NOVO: Telemetria V5 ----
self.tele = TelemetryV5(event_callback=self._telemetry_event)
self._booted = False self._booted = False
self.telemetry_path = Path(NEUROTRON_DATASET_PATH) / "telemetry.json" self.telemetry_path = Path(NEUROTRON_DATASET_PATH) / "telemetry.json"
self.telemetry_path.parent.mkdir(parents=True, exist_ok=True) self.telemetry_path.parent.mkdir(parents=True, exist_ok=True)
# ---------------------------------------- # ----------------------------------------
# Boot # boot
# ---------------------------------------- # ----------------------------------------
def boot(self): def boot(self):
if self._booted: if self._booted:
@ -71,7 +73,7 @@ class Cortex:
self._booted = True self._booted = True
# ---------------------------------------- # ----------------------------------------
# Shutdown + Fatal # shutdown
# ---------------------------------------- # ----------------------------------------
def shutdown(self, reason=""): def shutdown(self, reason=""):
logbus.warn(f"Shutdown pedido — {reason}") logbus.warn(f"Shutdown pedido — {reason}")
@ -104,7 +106,6 @@ class Cortex:
if not action: if not action:
return return
# echo (debug)
if action.get("action") == "echo": if action.get("action") == "echo":
res = self.motor.run("echo", [action.get("text", "")]) res = self.motor.run("echo", [action.get("text", "")])
msg = res.get("stdout", "").strip() msg = res.get("stdout", "").strip()
@ -112,15 +113,26 @@ class Cortex:
logbus.info(f"[echo] {msg}") logbus.info(f"[echo] {msg}")
def rest(self): def rest(self):
# ------- Telemetria V5 ------
try:
tele = self.tele.step()
self.telemetry.append(tele)
except Exception as e:
logbus.error(f"telemetry_step: {e}")
# ------- Heartbeat visual ------
if HEARTBEAT_ENABLED: if HEARTBEAT_ENABLED:
self._heartbeat() self._heartbeat()
# ------- Tick ------
sleep(self._safe_float(self.tick, fallback=1.0)) sleep(self._safe_float(self.tick, fallback=1.0))
self._tick_count += 1 self._tick_count += 1
# ------- Diag -------
if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0: if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0:
self._run_diag() self._run_diag()
# ------- Persistência -------
if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0: if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0:
self._flush_telemetry() self._flush_telemetry()
@ -135,7 +147,7 @@ class Cortex:
return fallback return fallback
# ---------------------------------------- # ----------------------------------------
# heartbeat # heartbeat — apenas visual, não cognitivo
# ---------------------------------------- # ----------------------------------------
def _heartbeat(self): def _heartbeat(self):
snap = self.perception.snapshot() snap = self.perception.snapshot()
@ -145,22 +157,24 @@ class Cortex:
load = snap.get("loadavg") load = snap.get("loadavg")
load1 = self._safe_float(load[0] if load else 0.0, 0.0) load1 = self._safe_float(load[0] if load else 0.0, 0.0)
self.telemetry.append({
"ts": time.time(),
"cpu": cpu,
"mem": mem,
"load1": load1,
"tick": self.tick,
})
# log em modo seguro
try: try:
logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick:.2f}s") logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick:.2f}s")
except: except:
logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick}") logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick}")
# ---------------------------------------- # ----------------------------------------
# diag / homeostase # telemetria persistência
# ----------------------------------------
def _flush_telemetry(self):
try:
data = list(self.telemetry)
self.telemetry_path.write_text(json.dumps(data))
self.memory.remember("telemetry.flush", {})
except Exception as e:
logbus.error(f"telemetry_flush: {e}")
# ----------------------------------------
# diag + homeostase
# ---------------------------------------- # ----------------------------------------
def _run_diag(self): def _run_diag(self):
state, snap = self.diagnostic.run_exam() state, snap = self.diagnostic.run_exam()
@ -181,20 +195,13 @@ class Cortex:
self.tick = max(NEUROTRON_TICK_MIN, old - NEUROTRON_TICK_STEP / 2) self.tick = max(NEUROTRON_TICK_MIN, old - NEUROTRON_TICK_STEP / 2)
if self.tick != old: if self.tick != old:
try: logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s")
logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s")
except:
logbus.info(f"tick ajustado {old}{self.tick}")
# ---------------------------------------- # ----------------------------------------
# telemetria # callback eventos telemétricos (Hippocampus)
# ---------------------------------------- # ----------------------------------------
def _flush_telemetry(self): def _telemetry_event(self, event_name, payload):
try: self.memory.remember(f"telemetry.event.{event_name}", payload)
self.telemetry_path.write_text(json.dumps(list(self.telemetry)))
self.memory.remember("telemetry.flush", {})
except Exception as e:
logbus.error(f"telemetry error: {e}")
# ---------------------------------------- # ----------------------------------------
# bus interno # bus interno

251
src/neurotron/telemetry.py Normal file
View File

@ -0,0 +1,251 @@
"""
telemetry.py Telemetria V5 do Neurotron
-----------------------------------------
Responsável por:
Medições básicas (CPU, MEM, LOAD, IO)
Delta entre ciclos
Aceleração do delta
Temperatura virtual (fadiga cognitiva)
Jitter cognitivo (latência entre ciclos)
FS Health (blocos, erros, RO-mode)
Eventos telemétricos (V5)
Classificação de estados cognitivos
Este módulo é completamente independente:
o Cortex chama TelemetryV5.step() a cada ciclo.
"""
import time
import os
import psutil
class TelemetryV5:
# ----------------------------------------------------------
# Inicialização
# ----------------------------------------------------------
def __init__(self, event_callback=None):
"""
event_callback(event_name, payload_dict)
"""
self.event_callback = event_callback
self.last_raw = None
self.last_delta = None
self.last_ts = None
self.temperature = 0.0
self.jitter_avg = 0.0
# manter último estado cognitivo
self.last_state = "stable"
# ----------------------------------------------------------
# Coleta RAW (CPU, MEM, LOAD, IO)
# ----------------------------------------------------------
def collect_raw(self):
now = time.monotonic()
cpu = psutil.cpu_percent(interval=None)
mem = psutil.virtual_memory().percent
load = os.getloadavg()[0] # 1-min loadavg
# IO delta é manual, usamos psutil
io = psutil.disk_io_counters()
disk_total = io.read_bytes + io.write_bytes
return {
"ts": now,
"cpu": cpu,
"mem": mem,
"load": load,
"disk": disk_total,
}
# ----------------------------------------------------------
# Delta = diferença entre ciclos
# ----------------------------------------------------------
def compute_delta(self, raw):
if self.last_raw is None:
self.last_raw = raw
return {k: 0 for k in raw.keys() if k != "ts"}
delta = {
"cpu": raw["cpu"] - self.last_raw["cpu"],
"mem": raw["mem"] - self.last_raw["mem"],
"load": raw["load"] - self.last_raw["load"],
"disk": raw["disk"] - self.last_raw["disk"],
}
self.last_delta = delta
self.last_raw = raw
return delta
# ----------------------------------------------------------
# Aceleração = variação do delta
# ----------------------------------------------------------
def compute_accel(self, delta):
if self.last_delta is None:
return {k: 0 for k in delta.keys()}
accel = {k: delta[k] - self.last_delta[k] for k in delta.keys()}
return accel
# ----------------------------------------------------------
# Jitter Cognitivo
# ----------------------------------------------------------
def compute_jitter(self, now):
if self.last_ts is None:
self.last_ts = now
return 0.0
jitter = now - self.last_ts
self.last_ts = now
# média móvel
self.jitter_avg = (self.jitter_avg * 0.9) + (jitter * 0.1)
return jitter
# ----------------------------------------------------------
# Temperatura Virtual (fadiga cognitiva)
# ----------------------------------------------------------
def compute_temperature(self, raw, delta):
# modelo simplificado
score = (
raw["cpu"] * 0.6 +
raw["load"] * 0.3 +
abs(delta["cpu"]) * 0.2 +
raw["mem"] * 0.1
)
# decay + acumulação
self.temperature = max(0, self.temperature * 0.90 + score * 0.10)
return self.temperature
# ----------------------------------------------------------
# FS Health
# ----------------------------------------------------------
def compute_fs_health(self):
status = {
"read_only": False,
"io_errors": 0,
"free_percent": None
}
try:
st = psutil.disk_usage("/")
status["free_percent"] = st.free / st.total * 100
except Exception:
status["free_percent"] = None
# leitura de erros EXT4 se existir
ext4_err_path = "/sys/fs/ext4/sda1/errors_count"
if os.path.exists(ext4_err_path):
try:
with open(ext4_err_path) as f:
status["io_errors"] = int(f.read().strip())
except:
pass
# detetar RO
try:
with open("/tmp/fs_test_rw", "w") as f:
f.write("x")
except Exception:
status["read_only"] = True
return status
# ----------------------------------------------------------
# Classificação de Estado Cognitivo
# ----------------------------------------------------------
def classify_state(self, temp, jitter):
if temp < 25:
return "stable"
elif temp < 45:
return "warm"
elif temp < 65:
return "hot"
elif temp < 85:
return "critical"
else:
return "recovery"
# ----------------------------------------------------------
# Eventos TeleMétricos V5
# ----------------------------------------------------------
def detect_events(self, raw, delta, accel, temp, jitter, fs):
events = []
# picos
if delta["cpu"] > 15 or accel["cpu"] > 10:
events.append("temp_rising_fast")
# zona de stress
if raw["cpu"] > 85:
events.append("enter_stress_zone")
# suspeita de loop
if jitter > (self.jitter_avg * 3):
events.append("loop_suspect")
# fs
if fs["read_only"] or fs["io_errors"] > 0:
events.append("fs_warning")
# recuperação
if self.last_state == "critical" and temp < 50:
events.append("recovering")
# exportar via callback
if self.event_callback:
for e in events:
self.event_callback(e, {
"raw": raw,
"delta": delta,
"accel": accel,
"temp": temp,
"jitter": jitter,
"fs": fs,
})
return events
# ----------------------------------------------------------
# STEP — chamada a cada ciclo cognitivo
# ----------------------------------------------------------
def step(self):
"""
Faz um ciclo completo da Telemetria V5:
- raw delta accel
- jitter
- temp
- fs health
- estado
- eventos
"""
raw = self.collect_raw()
delta = self.compute_delta(raw)
accel = self.compute_accel(delta)
jitter = self.compute_jitter(raw["ts"])
temp = self.compute_temperature(raw, delta)
fs = self.compute_fs_health()
state = self.classify_state(temp, jitter)
events = self.detect_events(raw, delta, accel, temp, jitter, fs)
self.last_state = state
return {
"raw": raw,
"delta": delta,
"accel": accel,
"jitter": jitter,
"temp": temp,
"fs": fs,
"state": state,
"events": events,
}