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'
],
{
'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_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_CONFIG_MACRO_DIRS' => 1,
'AM_SET_CURRENT_AUTOMAKE_VERSION' => 1,
'_AM_AUTOCONF_VERSION' => 1,
'_m4_warn' => 1,
'AM_AUTOMAKE_VERSION' => 1,
'_AM_DEPENDENCIES' => 1,
'_AM_SUBST_NOTMAKE' => 1,
'_AM_SET_OPTIONS' => 1,
'_AM_PROG_CC_C_O' => 1,
'include' => 1,
'AM_MISSING_HAS_RUN' => 1,
'_AC_AM_CONFIG_HEADER_HOOK' => 1,
'_AM_PROG_TAR' => 1,
'AM_OUTPUT_DEPENDENCY_COMMANDS' => 1,
'AM_SET_LEADING_DOT' => 1,
'_AM_AUTOCONF_VERSION' => 1,
'_AM_OUTPUT_DEPENDENCY_COMMANDS' => 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' ),
bless( [
@ -92,65 +92,65 @@
'configure.ac'
],
{
'AC_FC_PP_DEFINE' => 1,
'AC_REQUIRE_AUX_FILE' => 1,
'_AM_SUBST_NOTMAKE' => 1,
'AM_PROG_CXX_C_O' => 1,
'AC_PROG_LIBTOOL' => 1,
'_m4_warn' => 1,
'AM_PROG_MKDIR_P' => 1,
'AC_FC_FREEFORM' => 1,
'AC_FC_SRCEXT' => 1,
'AM_AUTOMAKE_VERSION' => 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,
'AC_LIBSOURCE' => 1,
'm4_pattern_forbid' => 1,
'AM_NLS' => 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,
'AC_FC_PP_SRCEXT' => 1,
'AH_OUTPUT' => 1,
'AC_CANONICAL_SYSTEM' => 1,
'AM_SILENT_RULES' => 1,
'LT_CONFIG_LTDL_DIR' => 1,
'AC_CONFIG_SUBDIRS' => 1,
'LT_SUPPORTED_TAG' => 1,
'AC_FC_FREEFORM' => 1,
'm4_pattern_allow' => 1,
'AC_CONFIG_AUX_DIR' => 1,
'_LT_AC_TAGCONFIG' => 1,
'AC_CONFIG_LIBOBJ_DIR' => 1,
'_AM_COND_IF' => 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,
'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,
'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' )
);

View File

@ -6,7 +6,7 @@ SRC="$NEUROTRON_HOME/src"
export PYTHONHOME="/usr"
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
exec "$PYTHON" -m neurotron "$@"

View File

@ -4,9 +4,18 @@ PYTHON="/usr/bin/python3"
NEUROTRON_HOME="/opt/kernel/neurotron"
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 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
exec "$PYTHON" -m neurotron "$@"
exec "$PYTHON" -m neurotron "$@"

View File

@ -21,35 +21,69 @@ from neurotron.cortex import Cortex
def dashboard_loop(ctx: Cortex):
"""
Renderização minimalista do estado atual.
Apenas mostra informações de alto nível.
Tudo mais pertence ao logbus (à direita do ecrã).
Dashboard com scroll real.
Mantém:
- Linha 1: estado
- Linha 2: separador
- Linhas 3..102: janela de 100 linhas do logbus
"""
start = time.time()
while True:
uptime = int(time.time() - start)
h = uptime // 3600
m = (uptime % 3600) // 60
s = uptime % 60
MAX_LINES = 40 # janela visível
LOG_START_ROW = 3 # linha onde os logs começam
mode = ctx.mode.upper()
tick = ctx.tick
# limpar ecrã e esconder cursor
sys.stdout.write("\033[2J\033[H\033[?25l")
sys.stdout.flush()
# linha de estado (esquerda)
line = (
f"UP: {h:02}:{m:02}:{s:02} "
f"TICK: {tick:0.2f}s "
f"MODO: {mode:10}"
)
try:
while True:
uptime = int(time.time() - start)
h = uptime // 3600
m = (uptime % 3600) // 60
s = uptime % 60
# escreve sempre na coluna fixa
sys.stdout.write("\033[1;1H" + line + "\033[K")
mode = (ctx.mode or "").upper()
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()
time.sleep(0.2)
# =============================================================================
# CICLO COGNITIVO
# =============================================================================
@ -68,13 +102,14 @@ def cognitive_loop(ctx: Cortex):
except Exception as e:
logbus.error(f"Fatal no ciclo cognitivo: {repr(e)}")
tb = traceback.format_exc()
for line in tb.splitlines():
logbus.error(line)
ctx.shutdown("fatal exception")
raise
# =============================================================================
# MAIN
# =============================================================================

View File

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

View File

@ -1,106 +1,229 @@
# neurotron/disk_agent.py
import os
import subprocess
from pathlib import Path
from neurotron.neuron import Neuron
from neurotron.logbus import logbus
DISK_CANDIDATES = ["/dev/vda", "/dev/sda", "/dev/vdb"]
MOUNT_POINT = "/mnt/nfdos"
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"
def __init__(self, ctx):
super().__init__(ctx)
self.state = "unknown" # unknown → no_disk → found → mounted
self.last_msg = None
self.state = "unknown" # unknown → found → fs_ok → mounted
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
# --------------------------------
def _emit_once(self, msg):
# ------------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------------
def _emit_once(self, msg: str):
if msg != self.last_msg:
logbus.disk(msg)
self.last_msg = msg
def _run(self, cmd):
def _run_ok(self, cmd: list[str]) -> bool:
try:
subprocess.run(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
check=True,
)
return True
except Exception:
return False
# --------------------------------
# OBSERVE
# --------------------------------
def _run_capture(self, cmd: list[str]) -> dict:
"""
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):
# corre só 1x por 10 ticks
# Evita spam excessivo
if self.cooldown > 0:
self.cooldown -= 1
return
self.cooldown = 10
# 1) Procurar disco
dev = None
for d in DISK_CANDIDATES:
if Path(d).exists():
dev = d
break
# ESTADO: já montado → nada para fazer
if self.state == "mounted":
return
if not dev:
self._emit_once("Nenhum disco encontrado — modo VOLATILE")
# 1) Encontrar disco base
disks = self._find_real_disks()
if not disks:
self._emit_once("Nenhum disco de bloco encontrado — VOLATILE")
self.state = "no_disk"
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":
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"
return
# 3) Tentar identificar filesystem
try:
blkid = subprocess.run(
["blkid", dev],
text=True,
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 = ""
# 2) Verificar se device existe mesmo (evitar "Can't lookup blockdev")
if not Path(self.target).exists():
self._emit_once(f"{self.target}: ainda não existe em /dev — aguardando…")
self.cooldown = 5
return
if blkid:
self._emit_once("Sistema de ficheiros válido encontrado")
else:
self._emit_once("Disco virgem detectado — não formatado")
# 3) ESTADO: found → verificar filesystem
if self.state == "found":
res_blkid = self._run_capture(["blkid", self.target])
blkid_out = res_blkid["out"]
# 4) Montar
Path(MOUNT_POINT).mkdir(parents=True, exist_ok=True)
if "TYPE=" in blkid_out:
self._emit_once("Filesystem existente detectado")
self.state = "fs_ok"
return
if self._run(["mount", dev, MOUNT_POINT]):
if self.state != "mounted":
self._emit_once(f"Montado em {MOUNT_POINT}")
# Não há TYPE= → precisamos formatar
self._emit_once("Formatação filesystem (mke2fs) — journaling ON")
# 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"
# 5) Trocar Neurotron → modo persistente
if self.ctx.mode != "persistent":
self.ctx.mode = "persistent"
logbus.info("Modo alterado → PERSISTENT")
# 6) Estrutura básica
for d in ["data", "logs", "dna"]:
Path(MOUNT_POINT, d).mkdir(exist_ok=True)
else:
self._emit_once("Falha ao montar — mantendo VOLATILE")
else:
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
from neurotron.neuron import Neuron
from neurotron.logbus import logbus
@ -7,21 +8,28 @@ class EchoAgent(Neuron):
def __init__(self, ctx):
super().__init__(ctx)
self.last_cpu = None
self.last = None
def think(self):
msg = self.consume("vitals")
if not msg:
return
cpu = msg.get("cpu_percent")
if cpu is None:
return
"""
O EchoAgent apenas ecoa valores vitais para debug.
Deve ser 100% seguro: cpu/mem podem ser '?' quando /proc
ainda não está estável. Nunca deve formatar com {:.2f}.
"""
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
from collections import deque
from datetime import datetime
import sys
import threading
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):
"""Escreve logs com timestamp e nível padronizado."""
def __init__(self, maxlen: int = 1000):
self._buffer = deque(maxlen=maxlen)
# -------------------------
# núcleo
# -------------------------
def _emit(self, level: str, msg: str):
ts = datetime.utcnow().strftime("%H:%M:%S")
line = f"[{ts}] [{level}] {msg}\n"
line = f"[{ts}] [{level}] {msg}"
with self.lock:
sys.stdout.write(line)
sys.stdout.flush()
# guarda em memória para o dashboard
self._buffer.append(line)
# atalhos
def info(self, msg): self.emit("info", msg)
def warn(self, msg): self.emit("warn", msg)
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)
# escreve em stdout (uma linha)
sys.stdout.write(line + "\n")
sys.stdout.flush()
# -------------------------
# 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()

View File

@ -1,15 +1,30 @@
# neurotron/vitalsigns_agent.py
from neurotron.neuron import Neuron
from neurotron.logbus import logbus
class VitalSigns(Neuron):
"""
Observa sinais vitais (CPU, MEM, LOAD)
e publica em "vitals".
"""
name = "VitalSigns"
def observe(self):
def __init__(self, ctx):
super().__init__(ctx)
self.last = None
def think(self):
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