gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1181Shell 82Rust 62Markdown 521515 Definitions

fix(servidor): la imagen ya se puede reiniciar, y el tope de 10 Claudes deja de ser una pared

Dos cosas que el operador venia arrastrando.

1. REINICIAR. Con arje-zero de PID 1, "reboot" no reinicia y no dice nada:
   termina mandandole una senal a PID 1 que arje no atrapa (atrapa SIGINT y solo
   SIGINT, medido en SigCgt de /proc/1/status), y el kernel descarta las senales
   sin handler dirigidas a PID 1. Ni error ni mensaje: vuelve el prompt. Esta
   caja llevaba siete dias y medio de uptime sin poder reiniciarse, ni por
   consola ni desde el panel del proveedor, y el camino ordenado de arje
   (apagar_maquina: cascade, kill(-1), sync, remount-ro, reboot(2)) existia
   entero y no estaba conectado a ningun boton.

   arje-botones.sh lo conecta sobre el ROOTFS, y servidor-image.sh lo llama justo
   donde ya repara el init: asi viaja en la imagen y no depende de que alguien se
   acuerde de correr un script a mano despues de instalar.

   Lo que NO arregla, medido y anotado: el boton del panel. En musl acpid es el
   applet de busybox, que encuentra su handler y no puede ejecutarlo (EMFILE en
   su spawn; a mano, en el mismo directorio, el mismo /bin/sh lo corre perfecto).
   Hasta que haya un acpid de verdad, del panel sirve solo "Reset".

2. EL TOPE DE CLAUDES. El "hay 10 sesiones abiertas -- cerra alguna" era un
   exit 1 con un numero elegido a dedo, y contaba TODAS las jaulas del usuario
   sin decir cuales. Hoy eso le mintio en la cara: tenia 4 tabs de un proyecto
   abiertos y 6 sesiones de OTRO que no estaba mirando, conto sus tabs, el script
   contaba global, y ninguno de los dos estaba equivocado. Tuvo que matar un
   Claude al azar para poder abrir el que necesitaba.

   Ahora avisa una vez con la evidencia medida -- RAM libre, swap, cuantas mas
   entran -- y LISTA cada jaula con su proyecto, que es lo que faltaba para poder
   decidir. Y deja pasar: el techo real es la RAM, no un literal. Queda un corte
   en 64 que no es politica sino defensa: sin el exit 1 de antes, un pgrep que
   contestara "ocupada" para siempre era un bucle infinito.

28cd6d029405533f826510a63d70078bd676cae2 · by Sergio · (27 d ago) · parent 85e86c2fd · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

3 files · +182 −9

diff --git a/scripts/servidor-image.sh b/scripts/servidor-image.sh
index e5fdaadb..b27a37c3 100755
--- a/scripts/servidor-image.sh
+++ b/scripts/servidor-image.sh
@@ -125,6 +125,15 @@ ln -sf /usr/bin/arje-zero "$RFS/sbin/init"
 echo "==> init    : $antes → $(readlink "$RFS/sbin/init")"
 [ -x "$RFS/usr/bin/arje-zero" ] || { echo "!! el rootfs no tiene /usr/bin/arje-zero" >&2; exit 1; }
 
+# ── 3 bis. los BOTONES de apagado, que sin esto quedan mudos ─────────────────────────────────────
+# Con arje-zero de PID 1 no alcanza con que el init sea el correcto: `reboot` termina mandándole una
+# señal que arje NO atrapa (atrapa SIGINT y sólo SIGINT), y el kernel descarta las señales sin
+# handler dirigidas a PID 1. O sea que la imagen arranca perfecta y después NO SE PUEDE REINICIAR:
+# vuelve el prompt, sin error y sin mensaje. Medido en `takana` el 2026-09-25 — siete días y medio
+# de uptime sin poder reiniciar, ni por `sudo reboot` ni desde el panel del proveedor. El camino
+# ordenado de arje (`apagar_maquina()`) existía entero y no estaba conectado a ningún botón.
+sh scripts/servidor/arje-botones.sh "$RFS"
+
 # ── 4. la clave ──────────────────────────────────────────────────────────────────────────────────
 install -d -m 700 "$RFS/root/.ssh"
 install -m 600 "$AUTHKEYS" "$RFS/root/.ssh/authorized_keys"
diff --git a/scripts/servidor/arje-botones.sh b/scripts/servidor/arje-botones.sh
new file mode 100755
index 00000000..35b8cb20
--- /dev/null
+++ b/scripts/servidor/arje-botones.sh
@@ -0,0 +1,136 @@
+#!/bin/sh
+# =============================================================================
+#  arje-botones.sh — que `reboot`, `poweroff`, `halt` y `shutdown` hagan algo
+#  en una imagen cuyo PID 1 es `arje-zero`. Se corre SOBRE UN ROOTFS, al armar
+#  la imagen, no sobre la máquina viva.
+#
+#  Uso:  ./scripts/servidor/arje-botones.sh <rootfs>
+#
+#  ── POR QUÉ EXISTE (2026-09-25) ─────────────────────────────────────────────
+#
+#  `servidor-image.sh` §3 ya se ocupa de que `/sbin/init` sea `arje-zero`. Esto
+#  es el paso siguiente y sin él la imagen queda con un defecto silencioso: con
+#  arje-zero de PID 1, **`reboot` no reinicia y no dice nada**.
+#
+#  La causa, medida en `takana` el 2026-09-25 (y antes en `momento`, 26-ago):
+#  el `reboot` de la caja —sea el de OpenRC, sea el applet de busybox— termina
+#  mandándole una señal a PID 1. arje-zero atrapa **SIGINT y sólo SIGINT**
+#  (`/proc/1/status` → `SigCgt: 0000000000000442`; los otros dos bits son el
+#  SIGBUS/SIGSEGV que instala el runtime de Rust). Y el kernel **descarta** las
+#  señales sin handler dirigidas a PID 1 — no es que falle: no pasa nada, sin
+#  error y sin mensaje. Es el peor modo de fallar, y costó una tarde cada vez.
+#
+#  Del otro lado, `arje-zero::apagar_maquina()` está implementado de verdad
+#  (cascade topológico, kill(-1), sync, remount-ro, reboot(2)) y se dispara
+#  justo con SIGINT, que es lo que el kernel entrega ante Ctrl+Alt+Del con
+#  `kernel.ctrl-alt-del = 0`. O sea: el camino existía entero y no estaba
+#  conectado a ningún botón. Esto lo conecta.
+#
+#  ⚠ ASIMETRÍA QUE NO SE PUEDE TAPAR: hay señal para REINICIAR y no la hay para
+#  APAGAR. Por eso `poweroff` va por SysRq y **se saltea el cascade**. El camino
+#  bueno es `BusRequest::PowerOff` por el bus de arje; cuando `arje-ctl` esté en
+#  la imagen, el shim de poweroff debería delegarle en vez de hacer esto.
+#
+#  ⚠ EL BOTÓN DEL PANEL DEL PROVEEDOR NO SE ARREGLA ACÁ, y no por falta de
+#  ganas: en una caja musl `acpid` es el applet de **busybox**, que encuentra su
+#  handler y **no puede ejecutarlo** (`/bin/sh: can't open './PWRF/00000080':
+#  No file descriptors available` — EMFILE; a mano, parado en el mismo
+#  directorio, el mismo `/bin/sh` lo corre perfecto). Es un defecto de su
+#  `spawn()`. Hasta que haya un `acpid` de verdad, del panel sólo sirve «Reset»,
+#  que es el hipervisor. La línea de comandos sí queda arreglada, que es lo que
+#  este script promete.
+#
+#  ⚠ VAN EN /usr/local/bin A PROPÓSITO: el `secure_path` de sudo lo mira ANTES
+#  que /usr/bin y /sbin, así que estos ganan sin pisar el `reboot` de la caja —
+#  que sigue ahí para el día que el init sea otro.
+#
+#  ⚠ ESTO ES UNA COPIA de la lógica de `tawasuyu/scripts/install-arje-reboot.sh`
+#  (que hace lo mismo sobre una máquina VIVA). Está duplicada a propósito: al
+#  armar la imagen el repo tawasuyu no existe, y una imagen que sólo reinicia si
+#  alguien se acuerda de correr un script de OTRO repo es la clase de arreglo
+#  que se pierde en la siguiente instalación. Si tocás los shims, tocá los dos.
+# =============================================================================
+set -eu
+
+RFS=${1:-}
+[ -n "$RFS" ] || { echo "uso: $0 <rootfs>" >&2; exit 2; }
+[ -d "$RFS" ] || { echo "!! no existe el rootfs '$RFS'" >&2; exit 1; }
+
+install -d -m 755 "$RFS/usr/local/bin"
+
+cat > "$RFS/usr/local/bin/reboot" <<'SH'
+#!/bin/sh
+# reboot — el de verdad, para una máquina con arje-zero de PID 1.
+# El de la caja le manda una señal a PID 1 que arje-zero no atrapa, y el kernel
+# la descarta: vuelve el prompt y no pasa nada. SIGINT es la que sí atiende, y
+# de ahí sale su apagar_maquina(): cascade, kill(-1), sync, remount-ro, reboot(2).
+[ "$(id -u)" = 0 ] || { echo "reboot: hace falta root (probá: sudo reboot)" >&2; exit 1; }
+[ "$(ps -p 1 -o comm=)" = "arje-zero" ] || {
+  echo "reboot: el PID 1 NO es arje-zero — este shim no aplica." >&2
+  echo "        usá el reboot de tu init: /sbin/reboot" >&2
+  exit 1
+}
+sync
+echo "reboot: SIGINT a PID 1 (arje-zero) — reinicio ordenado en curso" >&2
+exec kill -INT 1
+SH
+
+cat > "$RFS/usr/local/bin/poweroff" <<'SH'
+#!/bin/sh
+# poweroff — apaga de verdad, PERO SIN el cascade de arje: arje-zero instala
+# SIGINT y sólo SIGINT, así que no hay señal de apagado que mandarle. Va directo
+# al kernel por SysRq: 'u' = remount-ro de emergencia (evita el fsck del próximo
+# arranque), 'o' = apagar. Los servicios NO reciben su SIGTERM. El camino bueno
+# es BusRequest::PowerOff por el bus de arje, cuando arje-ctl esté en la imagen.
+[ "$(id -u)" = 0 ] || { echo "poweroff: hace falta root" >&2; exit 1; }
+[ "$(cat /proc/sys/kernel/sysrq 2>/dev/null)" = 0 ] && {
+  echo "poweroff: kernel.sysrq está en 0 — no hay por dónde. Ponelo en 1." >&2
+  exit 1
+}
+echo "poweroff: sin cascade (arje no expone señal de apagado) — sync + SysRq u,o" >&2
+sync
+echo u > /proc/sysrq-trigger
+sleep 1
+echo o > /proc/sysrq-trigger
+SH
+
+cat > "$RFS/usr/local/bin/shutdown" <<'SH'
+#!/bin/sh
+# shutdown — traduce las banderas de siempre a los dos shims que sí andan.
+# No implementa el retardo (`shutdown -r +5`): esta máquina no tiene un init que
+# lo agende, y fingir que sí es cómo se llega a «creí que se iba a apagar».
+# Sin argumentos NO se apaga nada: el shutdown de siempre exige un TIME, y un
+# shim que apague en el acto ante un `shutdown` tipeado a medias es una trampa.
+[ $# -gt 0 ] || { echo "uso: shutdown [-r|-h] now   (el retardo no está implementado)" >&2; exit 1; }
+modo=poweroff
+for a in "$@"; do
+  case "$a" in
+    -r) modo=reboot ;;
+    -h|-P|-H) modo=poweroff ;;
+    -c) echo "shutdown: acá no hay nada agendado que cancelar." >&2; exit 0 ;;
+    now|-*) ;;
+    +*|[0-9]*:[0-9]*)
+      echo "shutdown: el retardo '$a' NO está implementado — sin init que lo agende." >&2
+      echo "          corré 'shutdown -r now' cuando de verdad quieras." >&2
+      exit 1 ;;
+  esac
+done
+exec "/usr/local/bin/$modo"
+SH
+
+ln -sf poweroff "$RFS/usr/local/bin/halt"
+chmod 755 "$RFS/usr/local/bin/reboot" "$RFS/usr/local/bin/poweroff" "$RFS/usr/local/bin/shutdown"
+
+# Ctrl+Alt+Del tiene que llegarle a arje como SIGINT, no reiniciar a lo bruto, y
+# SysRq tiene que estar encendido o el shim de poweroff no tiene por dónde. Los
+# dos son el default de esta imagen, pero un default no es una garantía.
+install -d -m 755 "$RFS/etc/sysctl.d"
+cat > "$RFS/etc/sysctl.d/60-arje-botones.conf" <<'CONF'
+# Ctrl+Alt+Del => SIGINT a PID 1 (que es de donde arje saca su reinicio ordenado).
+kernel.ctrl-alt-del = 0
+# SysRq encendido: es por donde apaga /usr/local/bin/poweroff, y es el escape
+# que siempre funciona (`sync; echo u > /proc/sysrq-trigger; echo b > ...`).
+kernel.sysrq = 1
+CONF
+
+echo "==> botones : reboot/poweroff/halt/shutdown en /usr/local/bin + sysctl (SIGINT a PID 1)"
diff --git a/scripts/servidor/claude-caja.sh b/scripts/servidor/claude-caja.sh
index d90ae1d6..7e1ebad2 100755
--- a/scripts/servidor/claude-caja.sh
+++ b/scripts/servidor/claude-caja.sh
@@ -73,15 +73,43 @@ _libre() { ! pgrep -f "[q]orpa run $1 " >/dev/null 2>&1; }
 _inst="$_base"; _n=2
 while ! _libre "$_inst"; do
   _inst="$_base-$_n"; _n=$((_n+1))
-  # ⚠ ESTE NÚMERO ES EL TOPE, y hasta el 2026-09-23 era lo ÚNICO que limitaba cuántos Claudes
-  # hay. Se leía como arquitectura y no lo es: el kernel sólo prohíbe DOS overlays sobre el mismo
-  # `upper` (una jaula = un Claude), y de eso ya se encarga el bucle clonando la capa siguiente.
-  # El techo real es la RAM, MEDIDO acá el 23-sep: 8 sesiones = 2,5 G de RSS (147–493 M cada una,
-  # ~310 M de media) sobre 7,7 G con 8 G de swap y 4,1 ya comidos. Los 4 núcleos NO limitan —las
-  # sesiones esperan la API, no CPU, y `taller` ya serializa los `cargo`—. Disco: 169 M por jaula.
-  # Subirlo es cambiar los dos números de esta línea; pasado ~14 empieza a jugarse con el OOM, que
-  # es el modo de falla documentado (INFORME-CAIDA-2026-08-17: la caída fue con ocho adentro).
-  [ "$_n" -gt 11 ] && { echo "claude: hay 10 sesiones abiertas de $_base — cerrá alguna" >&2; exit 1; }
+  # ⚠ ACÁ HABÍA UN TOPE DURO DE 10 Y SE FUE (2026-09-25, a pedido del operador).
+  #
+  # Lo que decía el comentario viejo sigue siendo cierto: el kernel sólo prohíbe DOS overlays
+  # sobre el mismo `upper`, y de eso ya se encarga el bucle clonando la capa siguiente. El techo
+  # real es la RAM. Lo que estaba mal era el `exit 1`: un número elegido a dedo que le decía «no»
+  # con la máquina a medio usar.
+  #
+  # Y contaba TODAS las jaulas del usuario sin decir CUÁLES. Medido el 25-sep, que es la queja que
+  # lo tumbó: el operador tenía 4 tabs de un proyecto abiertos y 6 sesiones de OTRO proyecto que no
+  # estaba mirando, y el mensaje sólo sabía decir «hay 10 — cerrá alguna». Él contó su grupo de
+  # tabs, el script contaba global, y ninguno de los dos estaba equivocado. Contar sin mostrar qué
+  # se cuenta es lo que hacía que el tope se leyera como capricho: no había forma de saber que las
+  # otras seis existían, ni que se llevaban 1,9 G y tenían el swap al 70 %.
+  #
+  # Ahora avisa UNA vez, con la evidencia medida y con el proyecto de cada jaula, y DEJA PASAR.
+  # El modo de falla de pasarse es el OOM (INFORME-CAIDA-2026-08-17, la caída fue con ocho
+  # adentro), y decidirlo con los números a la vista es del operador, no de esta línea.
+  if [ "$_n" -gt 11 ] && [ -z "${_avisado:-}" ]; then
+    _avisado=1
+    {
+      echo "claude: van $((_n - 2)) jaulas de $_base abiertas. No te freno — mirá la máquina y decidí:"
+      awk '/^MemAvailable:/{a=$2} /^SwapTotal:/{st=$2} /^SwapFree:/{sf=$2}
+           END{printf "  RAM libre %d M · swap %d/%d M (%d%%) · ~310 M por sesión ⇒ entran ~%d más\n",
+                      a/1024, (st-sf)/1024, st/1024, (st ? 100*(st-sf)/st : 0), a/1024/310}' /proc/meminfo
+      echo "  las que ya están (jaula · proyecto):"
+      pgrep -af "[q]orpa run $_base" 2>/dev/null | while read -r _pid _resto; do
+        _hijo=$(pgrep -P "$_pid" 2>/dev/null | head -1)
+        printf "    %-22s %s\n" \
+          "$(printf '%s' "$_resto" | awk '{print $4}')" \
+          "$(readlink "/proc/${_hijo:-0}/cwd" 2>/dev/null || echo '?')"
+      done
+    } >&2
+  fi
+  # Corte de seguridad, NO política: si `pgrep` contestara «ocupada» para siempre —un `/proc`
+  # inconsistente, un nombre que casa de más— sin el `exit 1` de antes esto era un bucle infinito.
+  # 64 está tan por encima de cualquier uso real que sólo lo alcanza una falla.
+  [ "$_n" -gt 65 ] && { echo "claude: 64 jaulas de $_base y todas ocupadas — eso es una falla de conteo, no uso. Mirá \`pgrep -af \"qorpa run $_base\"\`." >&2; exit 1; }
 done
 if [ ! -d "$BASEDIR/$_inst" ]; then
   echo "claude: abro una sesión más ($_inst) — clonando la capa, ~1 s" >&2