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