Auto-commit via make git (triggered by NFDOS)
This commit is contained in:
parent
254b0dae19
commit
9be53dc380
@ -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' )
|
||||
);
|
||||
|
||||
@ -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 "$@"
|
||||
13
neurotron.in
13
neurotron.in
@ -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 "$@"
|
||||
|
||||
@ -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
|
||||
# =============================================================================
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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 já 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user