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.
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
3 qillqakuna · +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