Auto-commit via make git (triggered by NFDOS)

This commit is contained in:
neoricalex 2025-11-24 08:00:07 +01:00
parent 254b0dae19
commit 9be53dc380
9 changed files with 498 additions and 226 deletions

View File

@ -35,49 +35,49 @@
'configure.ac' 'configure.ac'
], ],
{ {
'AM_CONDITIONAL' => 1,
'AC_DEFUN' => 1,
'AM_SUBST_NOTMAKE' => 1,
'AM_SILENT_RULES' => 1,
'm4_pattern_forbid' => 1,
'AC_CONFIG_MACRO_DIR' => 1,
'AM_PROG_INSTALL_STRIP' => 1,
'AM_SANITY_CHECK' => 1,
'AM_PROG_CC_C_O' => 1,
'AM_PYTHON_CHECK_VERSION' => 1,
'AM_PROG_INSTALL_SH' => 1,
'AM_INIT_AUTOMAKE' => 1, 'AM_INIT_AUTOMAKE' => 1,
'_AM_IF_OPTION' => 1,
'AC_CONFIG_MACRO_DIR_TRACE' => 1,
'm4_pattern_allow' => 1,
'm4_include' => 1,
'_AM_MANGLE_OPTION' => 1,
'AM_SET_DEPDIR' => 1,
'_AM_PROG_CC_C_O' => 1,
'AU_DEFUN' => 1,
'include' => 1,
'AM_MISSING_PROG' => 1,
'AM_PATH_PYTHON' => 1,
'AM_MAKE_INCLUDE' => 1,
'_AM_SET_OPTION' => 1,
'_AM_OUTPUT_DEPENDENCY_COMMANDS' => 1,
'AM_AUX_DIR_EXPAND' => 1,
'AM_RUN_LOG' => 1,
'AM_DEP_TRACK' => 1, 'AM_DEP_TRACK' => 1,
'_AM_CONFIG_MACRO_DIRS' => 1, '_AM_SET_OPTIONS' => 1,
'AM_SET_CURRENT_AUTOMAKE_VERSION' => 1, '_AM_PROG_CC_C_O' => 1,
'_AM_AUTOCONF_VERSION' => 1, 'include' => 1,
'_m4_warn' => 1,
'AM_AUTOMAKE_VERSION' => 1,
'_AM_DEPENDENCIES' => 1,
'_AM_SUBST_NOTMAKE' => 1,
'AM_MISSING_HAS_RUN' => 1, 'AM_MISSING_HAS_RUN' => 1,
'_AC_AM_CONFIG_HEADER_HOOK' => 1, '_AM_AUTOCONF_VERSION' => 1,
'_AM_PROG_TAR' => 1, '_AM_OUTPUT_DEPENDENCY_COMMANDS' => 1,
'AM_OUTPUT_DEPENDENCY_COMMANDS' => 1,
'AM_SET_LEADING_DOT' => 1,
'AC_DEFUN_ONCE' => 1, 'AC_DEFUN_ONCE' => 1,
'_AM_SET_OPTIONS' => 1 'AM_SANITY_CHECK' => 1,
'_AC_AM_CONFIG_HEADER_HOOK' => 1,
'AU_DEFUN' => 1,
'AM_PATH_PYTHON' => 1,
'_AM_SUBST_NOTMAKE' => 1,
'AM_MISSING_PROG' => 1,
'AM_PROG_INSTALL_SH' => 1,
'm4_pattern_forbid' => 1,
'm4_include' => 1,
'AC_DEFUN' => 1,
'AM_PYTHON_CHECK_VERSION' => 1,
'_AM_CONFIG_MACRO_DIRS' => 1,
'_AM_DEPENDENCIES' => 1,
'm4_pattern_allow' => 1,
'AM_SILENT_RULES' => 1,
'AM_SET_LEADING_DOT' => 1,
'AM_PROG_CC_C_O' => 1,
'AC_CONFIG_MACRO_DIR' => 1,
'AM_AUX_DIR_EXPAND' => 1,
'_AM_IF_OPTION' => 1,
'AM_RUN_LOG' => 1,
'AM_AUTOMAKE_VERSION' => 1,
'AC_CONFIG_MACRO_DIR_TRACE' => 1,
'AM_MAKE_INCLUDE' => 1,
'AM_SUBST_NOTMAKE' => 1,
'_m4_warn' => 1,
'AM_OUTPUT_DEPENDENCY_COMMANDS' => 1,
'_AM_SET_OPTION' => 1,
'AM_CONDITIONAL' => 1,
'_AM_PROG_TAR' => 1,
'AM_SET_CURRENT_AUTOMAKE_VERSION' => 1,
'AM_SET_DEPDIR' => 1,
'AM_PROG_INSTALL_STRIP' => 1,
'_AM_MANGLE_OPTION' => 1
} }
], 'Autom4te::Request' ), ], 'Autom4te::Request' ),
bless( [ bless( [
@ -92,65 +92,65 @@
'configure.ac' 'configure.ac'
], ],
{ {
'AC_FC_PP_DEFINE' => 1, 'AM_PROG_MKDIR_P' => 1,
'AC_REQUIRE_AUX_FILE' => 1, 'AC_FC_FREEFORM' => 1,
'_AM_SUBST_NOTMAKE' => 1, 'AC_FC_SRCEXT' => 1,
'AM_PROG_CXX_C_O' => 1,
'AC_PROG_LIBTOOL' => 1,
'_m4_warn' => 1,
'AM_AUTOMAKE_VERSION' => 1, 'AM_AUTOMAKE_VERSION' => 1,
'AC_CANONICAL_TARGET' => 1, 'AC_CANONICAL_TARGET' => 1,
'AC_CONFIG_FILES' => 1,
'AC_FC_SRCEXT' => 1,
'AC_CONFIG_LINKS' => 1,
'AM_PROG_F77_C_O' => 1,
'AC_DEFINE_TRACE_LITERAL' => 1,
'AM_ENABLE_MULTILIB' => 1,
'AM_EXTRA_RECURSIVE_TARGETS' => 1,
'_AM_COND_ENDIF' => 1,
'_AM_MAKEFILE_INCLUDE' => 1,
'AM_PROG_AR' => 1,
'AC_CANONICAL_HOST' => 1,
'AC_FC_PP_SRCEXT' => 1,
'AM_PATH_GUILE' => 1,
'AM_MAINTAINER_MODE' => 1,
'_LT_AC_TAGCONFIG' => 1,
'sinclude' => 1,
'AM_GNU_GETTEXT' => 1,
'AC_CONFIG_AUX_DIR' => 1,
'AC_CANONICAL_SYSTEM' => 1,
'_AM_COND_IF' => 1,
'm4_include' => 1,
'AC_CANONICAL_BUILD' => 1,
'AH_OUTPUT' => 1,
'AM_XGETTEXT_OPTION' => 1,
'AC_INIT' => 1,
'LT_INIT' => 1,
'include' => 1,
'AM_PROG_MKDIR_P' => 1,
'AM_PROG_CC_C_O' => 1, 'AM_PROG_CC_C_O' => 1,
'AC_LIBSOURCE' => 1, 'AC_FC_PP_SRCEXT' => 1,
'm4_pattern_forbid' => 1, 'AH_OUTPUT' => 1,
'AM_NLS' => 1, 'AC_CANONICAL_SYSTEM' => 1,
'm4_sinclude' => 1,
'_AM_COND_ELSE' => 1,
'AC_CONFIG_LIBOBJ_DIR' => 1,
'AM_GNU_GETTEXT_INTL_SUBDIR' => 1,
'AM_CONDITIONAL' => 1,
'AC_SUBST' => 1,
'AM_PROG_FC_C_O' => 1,
'AM_MAKEFILE_INCLUDE' => 1,
'AM_SILENT_RULES' => 1, 'AM_SILENT_RULES' => 1,
'LT_CONFIG_LTDL_DIR' => 1, 'AC_CONFIG_AUX_DIR' => 1,
'AC_CONFIG_SUBDIRS' => 1, '_LT_AC_TAGCONFIG' => 1,
'LT_SUPPORTED_TAG' => 1, 'AC_CONFIG_LIBOBJ_DIR' => 1,
'AC_FC_FREEFORM' => 1, '_AM_COND_IF' => 1,
'm4_pattern_allow' => 1,
'AM_POT_TOOLS' => 1, 'AM_POT_TOOLS' => 1,
'_AM_MAKEFILE_INCLUDE' => 1,
'AC_LIBSOURCE' => 1,
'AM_CONDITIONAL' => 1,
'AM_EXTRA_RECURSIVE_TARGETS' => 1,
'AM_PROG_F77_C_O' => 1,
'_AM_COND_ENDIF' => 1,
'_m4_warn' => 1,
'AM_MAINTAINER_MODE' => 1,
'AM_GNU_GETTEXT_INTL_SUBDIR' => 1,
'AC_FC_PP_DEFINE' => 1,
'_AM_SUBST_NOTMAKE' => 1,
'AM_MAKEFILE_INCLUDE' => 1,
'm4_sinclude' => 1,
'AM_NLS' => 1,
'AC_SUBST' => 1,
'AM_PROG_MOC' => 1, 'AM_PROG_MOC' => 1,
'AC_CONFIG_HEADERS' => 1, 'AM_PROG_CXX_C_O' => 1,
'AC_CANONICAL_HOST' => 1,
'AC_CANONICAL_BUILD' => 1,
'AC_CONFIG_LINKS' => 1,
'AC_CONFIG_FILES' => 1,
'include' => 1,
'LT_CONFIG_LTDL_DIR' => 1,
'AC_SUBST_TRACE' => 1, 'AC_SUBST_TRACE' => 1,
'AM_INIT_AUTOMAKE' => 1 'AM_PROG_FC_C_O' => 1,
'sinclude' => 1,
'AM_INIT_AUTOMAKE' => 1,
'AM_PATH_GUILE' => 1,
'AC_PROG_LIBTOOL' => 1,
'm4_pattern_allow' => 1,
'AC_CONFIG_SUBDIRS' => 1,
'AC_DEFINE_TRACE_LITERAL' => 1,
'AC_REQUIRE_AUX_FILE' => 1,
'AC_INIT' => 1,
'm4_pattern_forbid' => 1,
'AM_PROG_AR' => 1,
'AM_ENABLE_MULTILIB' => 1,
'm4_include' => 1,
'AM_GNU_GETTEXT' => 1,
'LT_INIT' => 1,
'AM_XGETTEXT_OPTION' => 1,
'_AM_COND_ELSE' => 1,
'AC_CONFIG_HEADERS' => 1,
'LT_SUPPORTED_TAG' => 1
} }
], 'Autom4te::Request' ) ], 'Autom4te::Request' )
); );

View File

@ -6,7 +6,7 @@ SRC="$NEUROTRON_HOME/src"
export PYTHONHOME="/usr" export PYTHONHOME="/usr"
export PYTHONPATH="$SRC:/usr/lib/python3.13:/usr/lib/python3.13/site-packages" export PYTHONPATH="$SRC:/usr/lib/python3.13:/usr/lib/python3.13/site-packages"
export PATH="/usr/bin:/bin:/sbin:/usr/sbin:$NEUROTRON_HOME/bin:$PATH" export PATH="/sbin:/bin:/usr/sbin:/usr/bin:$PATH"
# Arrancar o cérebro principal como módulo do package # Arrancar o cérebro principal como módulo do package
exec "$PYTHON" -m neurotron "$@" exec "$PYTHON" -m neurotron "$@"

View File

@ -4,9 +4,18 @@ PYTHON="/usr/bin/python3"
NEUROTRON_HOME="/opt/kernel/neurotron" NEUROTRON_HOME="/opt/kernel/neurotron"
SRC="$NEUROTRON_HOME/src" SRC="$NEUROTRON_HOME/src"
# Garante diretórios básicos
mkdir -p /proc /sys /dev
# Montar proc, sysfs e devtmpfs (idempotente, falha silenciosa se já montado)
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sys /sys 2>/dev/null || true
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
# Ambiente Python minimalista
export PYTHONHOME="/usr" export PYTHONHOME="/usr"
export PYTHONPATH="$SRC:/usr/lib/python3.13:/usr/lib/python3.13/site-packages" export PYTHONPATH="$SRC:/usr/lib/python3.13:/usr/lib/python3.13/site-packages"
export PATH="/usr/bin:/bin:/sbin:/usr/sbin:$NEUROTRON_HOME/bin:$PATH" export PATH="/sbin:/bin:/usr/sbin:/usr/bin:$PATH"
# Arrancar o cérebro principal como módulo do package # Arrancar o cérebro principal como módulo do package
exec "$PYTHON" -m neurotron "$@" exec "$PYTHON" -m neurotron "$@"

View File

@ -21,35 +21,69 @@ from neurotron.cortex import Cortex
def dashboard_loop(ctx: Cortex): def dashboard_loop(ctx: Cortex):
""" """
Renderização minimalista do estado atual. Dashboard com scroll real.
Apenas mostra informações de alto nível. Mantém:
Tudo mais pertence ao logbus (à direita do ecrã). - Linha 1: estado
- Linha 2: separador
- Linhas 3..102: janela de 100 linhas do logbus
""" """
start = time.time() start = time.time()
while True: MAX_LINES = 40 # janela visível
uptime = int(time.time() - start) LOG_START_ROW = 3 # linha onde os logs começam
h = uptime // 3600
m = (uptime % 3600) // 60
s = uptime % 60
mode = ctx.mode.upper() # limpar ecrã e esconder cursor
tick = ctx.tick sys.stdout.write("\033[2J\033[H\033[?25l")
sys.stdout.flush()
# linha de estado (esquerda) try:
line = ( while True:
f"UP: {h:02}:{m:02}:{s:02} " uptime = int(time.time() - start)
f"TICK: {tick:0.2f}s " h = uptime // 3600
f"MODO: {mode:10}" m = (uptime % 3600) // 60
) s = uptime % 60
# escreve sempre na coluna fixa mode = (ctx.mode or "").upper()
sys.stdout.write("\033[1;1H" + line + "\033[K") tick = ctx.tick
if mode == "PERSISTENT":
mode_str = "\033[1;34mPERSISTENT\033[0m"
elif mode == "DIAGNOSTIC":
mode_str = "\033[1;33mDIAGNOSTIC\033[0m"
else:
mode_str = mode
header = (
f"UP: {h:02}:{m:02}:{s:02} "
f"TICK: {tick:0.2f}s "
f"MODO: {mode_str}"
)
# Header
sys.stdout.write("\033[1;1H" + header + "\033[K")
sys.stdout.write("\033[2;1H" + "" * 80 + "\033[K")
# ----------------------------------------------
# SCROLL WINDOW (esta é a magia ✨)
# ----------------------------------------------
logs = logbus.tail(MAX_LINES)
row = LOG_START_ROW
for line in logs:
truncated = line[:256]
sys.stdout.write(f"\033[{row};1H{truncated}\033[K")
row += 1
# Limpar linhas abaixo caso sobrem
# sys.stdout.write(f"\033[{row};1H\033[J")
# sys.stdout.flush()
time.sleep(0.1)
finally:
sys.stdout.write("\033[?25h")
sys.stdout.flush() sys.stdout.flush()
time.sleep(0.2)
# ============================================================================= # =============================================================================
# CICLO COGNITIVO # CICLO COGNITIVO
# ============================================================================= # =============================================================================
@ -68,13 +102,14 @@ def cognitive_loop(ctx: Cortex):
except Exception as e: except Exception as e:
logbus.error(f"Fatal no ciclo cognitivo: {repr(e)}") logbus.error(f"Fatal no ciclo cognitivo: {repr(e)}")
tb = traceback.format_exc() tb = traceback.format_exc()
for line in tb.splitlines(): for line in tb.splitlines():
logbus.error(line) logbus.error(line)
ctx.shutdown("fatal exception") ctx.shutdown("fatal exception")
raise raise
# ============================================================================= # =============================================================================
# MAIN # MAIN
# ============================================================================= # =============================================================================

View File

@ -8,8 +8,8 @@ from time import sleep
from neurotron.logbus import logbus from neurotron.logbus import logbus
from neurotron.disk_agent import DiskAgent from neurotron.disk_agent import DiskAgent
from neurotron.echo_agent import EchoAgent
from neurotron.vitalsigns_agent import VitalSigns from neurotron.vitalsigns_agent import VitalSigns
from neurotron.echo_agent import EchoAgent
from .hippocampus import Hippocampus from .hippocampus import Hippocampus
from .perception import Perception from .perception import Perception
@ -26,12 +26,18 @@ from .neurotron_config import (
TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS, TELEMETRY_MAXLEN, TELEMETRY_FLUSH_EVERY_TICKS,
) )
class Cortex: class Cortex:
def __init__(self, runtime_dir, log_dir, tick_seconds=NEUROTRON_TICK): def __init__(self, runtime_dir, log_dir, tick_seconds=NEUROTRON_TICK):
self.runtime_dir = Path(runtime_dir) self.runtime_dir = Path(runtime_dir)
self.log_dir = Path(log_dir) self.log_dir = Path(log_dir)
self.tick = float(tick_seconds) # tick pode vir como string → convertemos sempre
try:
self.tick = float(tick_seconds)
except:
self.tick = 1.0
self.mode = NEUROTRON_MODE self.mode = NEUROTRON_MODE
self._tick_count = 0 self._tick_count = 0
@ -43,7 +49,6 @@ class Cortex:
self.bus = defaultdict(lambda: deque(maxlen=32)) self.bus = defaultdict(lambda: deque(maxlen=32))
self.telemetry = deque(maxlen=TELEMETRY_MAXLEN) self.telemetry = deque(maxlen=TELEMETRY_MAXLEN)
# ordem é importante
self.neurons = [ self.neurons = [
DiskAgent(self), DiskAgent(self),
VitalSigns(self), VitalSigns(self),
@ -82,11 +87,17 @@ class Cortex:
# ---------------------------------------- # ----------------------------------------
def observe(self): def observe(self):
for n in self.neurons: for n in self.neurons:
n.observe() try:
n.observe()
except Exception as e:
logbus.error(f"{n.name}.observe: {e}")
def think(self): def think(self):
for n in self.neurons: for n in self.neurons:
n.think() try:
n.think()
except Exception as e:
logbus.error(f"{n.name}.think: {e}")
def act(self): def act(self):
action = self.bus_consume("actions") action = self.bus_consume("actions")
@ -96,14 +107,15 @@ class Cortex:
# echo (debug) # 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", "")])
if res.get("stdout"): msg = res.get("stdout", "").strip()
logbus.info(f"[echo] {res['stdout'].strip()}") if msg:
logbus.info(f"[echo] {msg}")
def rest(self): def rest(self):
if HEARTBEAT_ENABLED: if HEARTBEAT_ENABLED:
self._heartbeat() self._heartbeat()
sleep(self.tick) sleep(self._safe_float(self.tick, fallback=1.0))
self._tick_count += 1 self._tick_count += 1
if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0: if self._tick_count % NEUROTRON_DIAG_EVERY_TICKS == 0:
@ -112,15 +124,26 @@ class Cortex:
if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0: if self._tick_count % TELEMETRY_FLUSH_EVERY_TICKS == 0:
self._flush_telemetry() self._flush_telemetry()
# ----------------------------------------
# helpers
# ----------------------------------------
@staticmethod
def _safe_float(x, fallback=0.0):
try:
return float(x)
except:
return fallback
# ---------------------------------------- # ----------------------------------------
# heartbeat # heartbeat
# ---------------------------------------- # ----------------------------------------
def _heartbeat(self): def _heartbeat(self):
snap = self.perception.snapshot() snap = self.perception.snapshot()
cpu = snap.get("cpu_percent")
mem = snap.get("mem_percent") cpu = self._safe_float(snap.get("cpu_percent"), 0.0)
mem = self._safe_float(snap.get("mem_percent"), 0.0)
load = snap.get("loadavg") load = snap.get("loadavg")
load1 = load[0] if load else None load1 = self._safe_float(load[0] if load else 0.0, 0.0)
self.telemetry.append({ self.telemetry.append({
"ts": time.time(), "ts": time.time(),
@ -130,25 +153,38 @@ class Cortex:
"tick": self.tick, "tick": self.tick,
}) })
logbus.heart(f"cpu={cpu}% mem={mem}% tick={self.tick:.2f}s") # 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 # diag / homeostase
# ---------------------------------------- # ----------------------------------------
def _run_diag(self): def _run_diag(self):
state, snap = self.diagnostic.run_exam() state, snap = self.diagnostic.run_exam()
logbus.diag(f"estado={state} cpu={snap['cpu']} mem={snap['mem']} load1={snap['load1']}")
old = self.tick cpu = snap.get("cpu")
mem = snap.get("mem")
load1 = snap.get("load1")
logbus.diag(f"estado={state} cpu={cpu} mem={mem} load1={load1}")
old = self._safe_float(self.tick, 1.0)
if state == "CRITICAL": if state == "CRITICAL":
self.tick = min(NEUROTRON_TICK_MAX, self.tick + NEUROTRON_TICK_STEP) self.tick = min(NEUROTRON_TICK_MAX, old + NEUROTRON_TICK_STEP)
elif state == "ALERT": elif state == "ALERT":
self.tick = min(NEUROTRON_TICK_MAX, self.tick + NEUROTRON_TICK_STEP / 2) self.tick = min(NEUROTRON_TICK_MAX, old + NEUROTRON_TICK_STEP / 2)
elif state == "STABLE": elif state == "STABLE":
self.tick = max(NEUROTRON_TICK_MIN, self.tick - NEUROTRON_TICK_STEP / 2) self.tick = max(NEUROTRON_TICK_MIN, old - NEUROTRON_TICK_STEP / 2)
if old != self.tick: if self.tick != old:
logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s") try:
logbus.info(f"tick ajustado {old:.2f}s → {self.tick:.2f}s")
except:
logbus.info(f"tick ajustado {old}{self.tick}")
# ---------------------------------------- # ----------------------------------------
# telemetria # telemetria
@ -170,4 +206,3 @@ class Cortex:
q = self.bus[ch] q = self.bus[ch]
return q.popleft() if q else None return q.popleft() if q else None

View File

@ -1,106 +1,229 @@
# neurotron/disk_agent.py # neurotron/disk_agent.py
import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from neurotron.neuron import Neuron from neurotron.neuron import Neuron
from neurotron.logbus import logbus from neurotron.logbus import logbus
DISK_CANDIDATES = ["/dev/vda", "/dev/sda", "/dev/vdb"]
MOUNT_POINT = "/mnt/nfdos" MOUNT_POINT = "/mnt/nfdos"
class DiskAgent(Neuron): class DiskAgent(Neuron):
"""
DiskAgent minimalista e pragmático:
- Descobre o primeiro disco "real" em /sys/block (vda, sda, nvme0n1, etc)
- Se existir uma partição (ex: /dev/vda1), usa essa.
- Caso contrário, usa o disco inteiro (ex: /dev/vda).
- Verifica se existe filesystem:
- se sim monta
- se não mkfs.ext4 e depois monta
- Ao montar com sucesso:
- muda ctx.mode "persistent"
- cria /mnt/nfdos/{data,logs,dna}
"""
name = "DiskAgent" name = "DiskAgent"
def __init__(self, ctx): def __init__(self, ctx):
super().__init__(ctx) super().__init__(ctx)
self.state = "unknown" # unknown → no_disk → found → mounted self.state = "unknown" # unknown → found → fs_ok → mounted
self.last_msg = None
self.cooldown = 0 self.cooldown = 0
self.last_msg = None
self.dev = None # disco base (ex: /dev/vda)
self.target = None # device efetivo (ex: /dev/vda1 ou /dev/vda)
# -------------------------------- # ------------------------------------------------------------------
# HELPER # HELPERS
# -------------------------------- # ------------------------------------------------------------------
def _emit_once(self, msg):
def _emit_once(self, msg: str):
if msg != self.last_msg: if msg != self.last_msg:
logbus.disk(msg) logbus.disk(msg)
self.last_msg = msg self.last_msg = msg
def _run(self, cmd): def _run_ok(self, cmd: list[str]) -> bool:
try: try:
subprocess.run( subprocess.run(
cmd, cmd,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True check=True,
) )
return True return True
except Exception: except Exception:
return False return False
# -------------------------------- def _run_capture(self, cmd: list[str]) -> dict:
# OBSERVE """
# -------------------------------- Executa comando e captura stdout/stderr + returncode.
Não levanta exceção, devolve estrutura amigável.
"""
try:
proc = subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return {
"ok": (proc.returncode == 0),
"rc": proc.returncode,
"out": (proc.stdout or "").strip(),
"err": (proc.stderr or "").strip(),
}
except Exception as e:
return {
"ok": False,
"rc": -1,
"out": "",
"err": repr(e),
}
def _find_real_disks(self):
"""
Procura discos reais em /sys/block:
- aceita: vda, sda, hda, nvme0n1, etc.
- ignora: loop*, ram*, dm-*, sr*
Converte para /dev/<name> se existir.
"""
disks = []
sysblock = Path("/sys/block")
if not sysblock.exists():
return disks
for dev in sysblock.iterdir():
name = dev.name
if name.startswith(("ram", "loop", "dm-", "sr")):
continue
path = Path("/dev") / name
if path.exists():
disks.append(str(path))
return disks
def _pick_target(self, base_dev: str) -> str:
"""
Escolhe o device real a usar:
- se existir <dev>1 (ex: /dev/vda1), usa essa partição;
- caso contrário, usa o disco inteiro.
"""
# tentar partição "1"
p1 = f"{base_dev}1"
if Path(p1).exists():
return p1
# se for NVMe, a convenção é ex: /dev/nvme0n1p1
if "nvme" in base_dev:
p_nvme = base_dev + "p1"
if Path(p_nvme).exists():
return p_nvme
# fallback → disco inteiro
return base_dev
# ------------------------------------------------------------------
# CICLO
# ------------------------------------------------------------------
def observe(self): def observe(self):
# corre só 1x por 10 ticks # Evita spam excessivo
if self.cooldown > 0: if self.cooldown > 0:
self.cooldown -= 1 self.cooldown -= 1
return return
self.cooldown = 10 self.cooldown = 10
# 1) Procurar disco # ESTADO: já montado → nada para fazer
dev = None if self.state == "mounted":
for d in DISK_CANDIDATES: return
if Path(d).exists():
dev = d
break
if not dev: # 1) Encontrar disco base
self._emit_once("Nenhum disco encontrado — modo VOLATILE") disks = self._find_real_disks()
if not disks:
self._emit_once("Nenhum disco de bloco encontrado — VOLATILE")
self.state = "no_disk" self.state = "no_disk"
return return
# 2) Novo disco encontrado dev = disks[0]
target = self._pick_target(dev)
self.dev = dev
self.target = target
# ESTADO: unknown → found
if self.state == "unknown": if self.state == "unknown":
self._emit_once(f"Disco detectado: {dev}") if target == dev:
self._emit_once(f"Disco detectado: {dev} (sem partições visíveis, usando disco inteiro)")
else:
self._emit_once(f"Disco detectado: {dev} (usando {target})")
self.state = "found" self.state = "found"
return
# 3) Tentar identificar filesystem # 2) Verificar se device existe mesmo (evitar "Can't lookup blockdev")
try: if not Path(self.target).exists():
blkid = subprocess.run( self._emit_once(f"{self.target}: ainda não existe em /dev — aguardando…")
["blkid", dev], self.cooldown = 5
text=True, return
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
).stdout.strip()
except FileNotFoundError:
# blkid não existe → fallback gentle
logbus.debug("[disk] blkid não encontrado — fallback ativo")
blkid = ""
if blkid: # 3) ESTADO: found → verificar filesystem
self._emit_once("Sistema de ficheiros válido encontrado") if self.state == "found":
else: res_blkid = self._run_capture(["blkid", self.target])
self._emit_once("Disco virgem detectado — não formatado") blkid_out = res_blkid["out"]
# 4) Montar if "TYPE=" in blkid_out:
Path(MOUNT_POINT).mkdir(parents=True, exist_ok=True) self._emit_once("Filesystem existente detectado")
self.state = "fs_ok"
return
if self._run(["mount", dev, MOUNT_POINT]): # Não há TYPE= → precisamos formatar
if self.state != "mounted": self._emit_once("Formatação filesystem (mke2fs) — journaling ON")
self._emit_once(f"Montado em {MOUNT_POINT}")
# BusyBox mke2fs não aceita -t, e o e2fsprogs aceita.
cmd = ["/sbin/mke2fs", "-t", "ext4", "-F", self.target]
res_mkfs = self._run_capture(cmd)
if not res_mkfs["ok"]:
logbus.error(f"[mkfs] Falhou (rc={res_mkfs['rc']})")
if res_mkfs["err"]:
logbus.error(f"[mkfs.err] {res_mkfs['err']}")
if res_mkfs["out"]:
logbus.error(f"[mkfs.out] {res_mkfs['out']}")
self._emit_once("Erro na formatação — retry mais tarde")
self.cooldown = 20
return
self._emit_once("Filesystem criado (mke2fs OK) 🎉")
self.state = "fs_ok"
return
# 4) ESTADO: fs_ok → montar
if self.state == "fs_ok":
Path(MOUNT_POINT).mkdir(parents=True, exist_ok=True)
# --- montar FS ---
opts = "rw,noatime"
cmd = ["mount", "-o", opts, self.target, MOUNT_POINT]
res_mount = self._run_capture(cmd)
if res_mount["ok"]:
self._emit_once(f"Montado em {MOUNT_POINT} ({self.target})")
self.state = "mounted" self.state = "mounted"
# 5) Trocar Neurotron → modo persistente
if self.ctx.mode != "persistent": if self.ctx.mode != "persistent":
self.ctx.mode = "persistent" self.ctx.mode = "persistent"
logbus.info("Modo alterado → PERSISTENT") logbus.info("Modo alterado → PERSISTENT")
# 6) Estrutura básica
for d in ["data", "logs", "dna"]: for d in ["data", "logs", "dna"]:
Path(MOUNT_POINT, d).mkdir(exist_ok=True) Path(MOUNT_POINT, d).mkdir(exist_ok=True)
else: else:
self._emit_once("Falha ao montar — mantendo VOLATILE") logbus.error(f"[mount] Falhou (rc={res_mount['rc']})")
if res_mount["err"]:
logbus.error(f"[mount.err] {res_mount['err']}")
if res_mount["out"]:
logbus.error(f"[mount.out] {res_mount['out']}")
self._emit_once("Falha ao montar — retry mais tarde")
self.cooldown = 15
return

View File

@ -1,4 +1,5 @@
# neurotron/echo_agent.py # neurotron/echo_agent.py
from neurotron.neuron import Neuron from neurotron.neuron import Neuron
from neurotron.logbus import logbus from neurotron.logbus import logbus
@ -7,21 +8,28 @@ class EchoAgent(Neuron):
def __init__(self, ctx): def __init__(self, ctx):
super().__init__(ctx) super().__init__(ctx)
self.last_cpu = None self.last = None
def think(self): def think(self):
msg = self.consume("vitals") """
if not msg: O EchoAgent apenas ecoa valores vitais para debug.
return Deve ser 100% seguro: cpu/mem podem ser '?' quando /proc
ainda não está estável. Nunca deve formatar com {:.2f}.
cpu = msg.get("cpu_percent") """
if cpu is None:
return snap = self.ctx.perception.snapshot()
cpu = snap.get("cpu_percent")
mem = snap.get("mem_percent")
# Se não forem números, convertemos para string limpa
cpu_str = f"{cpu}" if isinstance(cpu, (int, float)) else str(cpu)
mem_str = f"{mem}" if isinstance(mem, (int, float)) else str(mem)
msg = f"CPU={cpu_str}% MEM={mem_str}%"
# Evita spam
if msg != self.last:
logbus.info(f"[echo] {msg}")
self.last = msg
# só fala se variar > 5%
if self.last_cpu is None or abs(cpu - self.last_cpu) >= 5:
self.publish("actions", {
"action": "echo",
"text": f"CPU {cpu:.1f}%"
})
self.last_cpu = cpu

View File

@ -1,29 +1,76 @@
# neurotron/logbus.py # neurotron/logbus.py
from collections import deque
from datetime import datetime from datetime import datetime
import sys import sys
import threading
class LogBus: class LogBus:
def __init__(self): """
self.lock = threading.Lock() Canal central de logs do Neurotron.
- escreve tudo em stdout (texto simples, sem Rich)
- mantém um buffer em memória para o dashboard (tail)
"""
def emit(self, level, msg): def __init__(self, maxlen: int = 1000):
"""Escreve logs com timestamp e nível padronizado.""" self._buffer = deque(maxlen=maxlen)
# -------------------------
# núcleo
# -------------------------
def _emit(self, level: str, msg: str):
ts = datetime.utcnow().strftime("%H:%M:%S") ts = datetime.utcnow().strftime("%H:%M:%S")
line = f"[{ts}] [{level}] {msg}\n" line = f"[{ts}] [{level}] {msg}"
with self.lock: # guarda em memória para o dashboard
sys.stdout.write(line) self._buffer.append(line)
sys.stdout.flush()
# atalhos # escreve em stdout (uma linha)
def info(self, msg): self.emit("info", msg) sys.stdout.write(line + "\n")
def warn(self, msg): self.emit("warn", msg) sys.stdout.flush()
def error(self, msg): self.emit("error", msg)
def disk(self, msg): self.emit("disk", msg)
def diag(self, msg): self.emit("diag", msg)
def heart(self, msg): self.emit("heart", msg)
def debug(self, msg): self.emit("debug", msg)
# -------------------------
# API pública de logs
# -------------------------
def info(self, msg: str):
self._emit("info", msg)
def warn(self, msg: str):
self._emit("warn", msg)
def error(self, msg: str):
self._emit("error", msg)
def debug(self, msg: str):
self._emit("debug", msg)
def disk(self, msg: str):
self._emit("disk", msg)
def heart(self, msg: str):
# heartbeat mini e compacto
self._emit("heart", msg)
def diag(self, msg: str):
self._emit("diag", msg)
# -------------------------
# Integração com código antigo (emit)
# -------------------------
def emit(self, msg: str):
# fallback para chamadas antigas tipo logbus.emit("...")
self.info(msg)
# -------------------------
# Para o dashboard
# -------------------------
def tail(self, n: int = 150):
"""Devolve as últimas n linhas de log como lista de strings."""
if n <= 0:
return []
buf = list(self._buffer)
return buf[-n:]
# instância global
logbus = LogBus() logbus = LogBus()

View File

@ -1,15 +1,30 @@
# neurotron/vitalsigns_agent.py # neurotron/vitalsigns_agent.py
from neurotron.neuron import Neuron from neurotron.neuron import Neuron
from neurotron.logbus import logbus
class VitalSigns(Neuron): class VitalSigns(Neuron):
"""
Observa sinais vitais (CPU, MEM, LOAD)
e publica em "vitals".
"""
name = "VitalSigns" name = "VitalSigns"
def observe(self): def __init__(self, ctx):
super().__init__(ctx)
self.last = None
def think(self):
snap = self.ctx.perception.snapshot() snap = self.ctx.perception.snapshot()
self.publish("vitals", snap)
self.ctx.memory.remember("observe.vitals", snap) cpu = snap.get("cpu_percent")
mem = snap.get("mem_percent")
load = snap.get("loadavg") or ["?", "?", "?"]
cpu_str = f"{cpu}" if isinstance(cpu, (int, float)) else str(cpu)
mem_str = f"{mem}" if isinstance(mem, (int, float)) else str(mem)
load1 = load[0]
msg = f"Vitals CPU={cpu_str}% MEM={mem_str}% load1={load1}"
# evita spam
if msg != self.last:
logbus.debug(msg)
self.last = msg