bash es RAÍZ del perfil base —el shell de la distro— y cinco recetas repartidas en cuatro colas
(appstream, colord, gnome-shell, modemmanager, swayimg) instalan completados en el directorio
que este paquete define. Sin él NO los instalan y siguen adelante: nada falla, nadie se entera,
y el usuario se queda sin tabulador en las herramientas del sistema. Hueco de RUNTIME puro, de
los que ninguna métrica de deuda puede ver — por eso hizo falta la frontera para encontrarlo.
Va al CORPUS y como raíz de , no a una cola: lo alcanzan las cuatro imágenes de escritorio
y la CLI, y una receta resuelve sibling-first y después el catálogo padre, nunca una cola
hermana. Y entra como RAÍZ porque nadie lo alcanza por deps de build: los consumidores sólo
preguntan por su .pc.
Verificado como consumidor, las dos mitades:
pkgconf --variable=completionsdir bash-completion → /usr/share/bash-completion/completions
el marco carga 131 completados y complete devuelve regla · 522 ficheros instalados
REPRODUCE bit a bit · tarball en el mirror
Triaje: los huecos vuelven a CERO. base pasa de 26 a 27 raíces, clausura 58/58 sellada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Los 193 pendientes que destapó el barrido de sway y cosmic empiezan a bajar. Cada veredicto va
con la prueba en la receta, no con una corazonada:
vala opcional las 5 que lo piden llevan vapi=false explícito
gi-docgen opcional gtk4 documentation=false, pango/libadwaita gtk_doc=false
shiboken6-generator opcional las 8 recetas KF6 llevan -DBUILD_PYTHON_BINDINGS=OFF
xvfb-run opcional sólo envuelve tests, y los tests no se corren
libxinerama opcional gtk3/gtk4/mutter con el backend X11 apagado; es Wayland-only
openjpeg opcional ⚠ CON COSTE: poppler -DENABLE_LIBOPENJPEG=none y swayimg
jp2=disabled ⇒ un PDF con imágenes JPEG 2000 abre pero esas
imágenes NO SE VEN. Decisión tomada, no olvido — pero queda
escrito para que el día que alguien reporte 'este PDF sale con
huecos' la respuesta esté acá.
bash-completion HUECO bash es RAÍZ del perfil base, o sea el shell de la distro. Las 5
que lo piden instalan sus completados en el directorio que ese
paquete define y sin él no los instalan: nada se rompe y el
usuario se queda sin tabulador. Barato: datos + un script.
De 193 pendientes a 186. Quedan los de 3 consumidores hacia abajo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
La lista de grafos que consulta 'frontera' estaba escrita a mano y sólo tenía corpus, kde y
gnome. Consecuencia: 'yupana frontera escritorio-sway' y '... escritorio-cosmic' fallaban con
'perfil sin nodos en ningún grafo de estado (¿regeneraste con build-state.py?)' — culpando a un
grafo viejo con los cinco frescos de hacía minutos. Dos escritorios enteros sin poder responder
'¿qué me falta?'.
⚠ QUINTA vez que cae el mismo cable en este repo: GNOME (2026-07-27), COSMIC (2026-08-03) y
wlr/sway (2026-08-26) en cosecha-cron.sh, keystones y duplicados (2026-09-08) en el mismo sitio,
y ahora acá. Las cuatro anteriores se arreglaron AGREGANDO una línea, que arregla el caso y deja
la trampa puesta. Esta vez el fichero se DERIVA de la cola que el perfil declara: corpus →
build-state.json, incoming-<x> → build-state-<x>.json. Una cola nueva trae su grafo sola.
Verificado con control: 'base' sigue usando build-state.json y dando los mismos 41 candidatos.
Y lo que estaba tapado aparece — sway 175, cosmic 148, gnome 191, kde 276.
El triaje pasa de 172 a 365 candidatos (193 nuevos). No es deuda nueva: es deuda que no se
podía ver. Los que más recetas piden encabezan la cola — gi-docgen y vala con 9 cada uno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
La frontera queda en CERO huecos y cero pendientes, y el perfil escritorio-kde en 302/302
sellados. Los tres eran huecos de RUNTIME: KDE sellaba 1037/1037 con la autorización muerta,
los controles QML en estilo genérico y ninguna app X11 capaz de arrancar.
polkit-qt-1 0.201.1 — al conectarlo se vio lo que ninguna métrica decía: kauth NO traía plugin
de backend, ni siquiera el directorio. Ahora trae kf6/kauth/backend/kauth_backend_plugin.so con
el símbolo KAuth::Polkit1Backend. Cascada de 26 dependientes reconstruida, 26/26 sin fallos.
Añadirlo cambió la rama del CMake y pidió kwindowsystem y tras él las X11 — cada cosa dicha por
el configure al fallar, no supuesta.
qqc2-breeze-style 6.7.2 (la del stack Plasma, NO la 6.7.4 de nixpkgs: mezclar versiones de un
mismo release es pedir desajuste de ABI en los plugins QML, que se ve en runtime y no al
construir). Enganchado a plasma-workspace como deps.runtime, no build: es un plugin que Qt carga
por la ruta de imports. Verificado que runtime no re-hashea ⇒ cero rebuilds.
xwayland 24.1.13 — resultó ser una CADENA de seis: libfontenc, libXfont2, libxkbfile,
libxshmfence, xkbcomp y el servidor. Ninguna existía. Tres tropiezos con su lección:
· libxkbfile 1.2.0 ya no trae ./configure: es sólo meson. El resto de las libs X11 de la cola
son autotools, así que copiar su plantilla era lo natural y estaba mal.
· «checking for freetype2... no» culpaba a freetype, que estaba perfecta: freetype2.pc declara
Requires: zlib, libpng y pkg-config resuelve transitivamente. El mensaje señala al paquete
equivocado.
· libXfont2 construye una .so y moría en «recompile with -fPIC» contra la libfreetype.a
estática. Van las variantes -shared.
Y xkbcomp es el eslabón que NINGÚN build habría delatado: xwayland lo lanza como subproceso en
runtime (xkb/ddxLoad.c) y su meson lo pide con required:false, así que la receta compila igual
y el servidor arranca sin teclado. Se encontró leyendo la fuente.
⚠ Sin GLX, y no por preferencia: glx/meson.build:43 pide dependency('gl') y ninguna cola publica
gl.pc — las tres recetas de mesa van con glx y glvnd apagados por decisión anterior. Las apps
X11 corren en 2D. ⚠ Y el meson.build RAÍZ engaña: ahí build_glx sólo enciende una tabla hash y
parece gratis; yo mismo di la preocupación por infundada mirando el fichero equivocado.
Verificado: Xwayland arranca y se identifica («The X.Org Foundation Xwayland Version 24.1.13»),
las 6 REPRODUCEN bit a bit, los 5 grafos con deuda 0, y los 6 tarballs en el mirror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Salió de 'yupana frontera' en cuanto nix volvió a correr: kcoreaddons la pide y no había receta.
Es un hueco de RUNTIME, no de build — KDE sella 1037/1037 sin ella, y por eso NINGUNA métrica de
deuda la veía. Sin la base MIME el escritorio no resuelve asociaciones, iconos por tipo ni
'abrir con', y GIO cae a adivinar por extensión.
Va al CORPUS, no a incoming-kde: la necesitan las cuatro imágenes de escritorio, y una receta
resuelve sibling-first y después el catálogo padre, nunca una cola hermana.
Dos cosas que se comprobaron ANTES de construir, no después de que fallaran:
· El tarball de GitLab no trae el submódulo 'xdgmime' — .gitmodules lo declara y el directorio
llega vacío. Leyendo meson.build:26-47 se ve que sólo lo usa la suite de tests y con
required:false, así que con build-tests=false lo único que pasa es un warning.
· 'i18n.merge_file' corre SIEMPRE, no lo gobierna build-translations: no traduce UI, arma el
fichero de datos desde el .in. De ahí el msgfmt, que gettext-tiny sí provee.
'--prefer-static' desde el principio, que es la lección de json-glib de hace un rato: el binario
sale ESTÁTICO con 0 NEEDED y corre en el host.
Y el artefacto trae la mime.cache ya compilada (update-mimedb=true, que upstream trae en false):
sin ella el paquete son datos crudos y alguien tendría que acordarse de correr
update-mime-database al armar cada imagen.
Verificado:
prueba del consumidor update-mime-database genera 157.560 bytes de cache y 26 media-types
binario estático, 0 NEEDED, corre fuera del sandbox
repro REPRODUCE bit a bit (la caché se genera en el build: era candidata)
mirror el tarball subido al Storage Box (ADR 0013)
frontera huecos 4 → 3
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'yupana frontera' llevaba desde el 2026-08-10 sin poder correr acá porque el hub no tenía nix.
Instalado desde el repo de Artix (extra/nix 2.35.2). Dos cosas que costaron medirse:
· El paquete quedó ROTO al instalarse: le faltaban libboost_{context,iostreams,url}.so.1.92.0
porque el sistema tiene boost 1.91 y 280 paquetes sin actualizar. NO se hizo 'pacman -Syu':
280 paquetes en un server protegido, sin backup, que hospeda la granja, gitea y el runner de
CI no es una decisión que tome un arreglo de tooling. El ensayo en seco mostró que la
reparación acotada eran TRES paquetes exactos (boost-libs, source-highlight, gdb) sin
cascada, y eso sí se hizo. Control después: gdb 17.2 sigue arrancando.
· El store por defecto iba a ~/.nixstore, o sea a '/', que tiene 21 G libres frente a los 126
del volumen. Llenar '/' no rompe una tanda, rompe la máquina. NIX_ROOT pasa a apuntar a
work/nixstore, que además ya es el sitio de lo pesado-y-regenerable. La caché git de nixpkgs
(~69 M) NO la gobierna esa variable —nix la pone en XDG_CACHE_HOME— así que en gioser está
mudada al volumen con un enlace: ~/.cache/nix -> /mnt/vvv/nix-cache.
Con eso, la frontera vuelve a dar respuesta, y es pequeña como corresponde a un corpus cerrado:
172 candidatos → 150 opcionales, 11 provistos, 5 nix-ismos, 4 HUECOS reales (polkit-qt-1,
qqc2-breeze-style, shared-mime-info, xwayland) y 2 pendientes de clasificar (mesa-libclc,
python3-3.14.7-env).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'yupana frontera <perfil>' —el verbo que responde qué pide upstream y no tenemos— moría con
veinte líneas de traceback terminadas en FileNotFoundError: 'nix'. Eso tiene la cara de un bug
de código y manda a depurar el script, cuando lo único que falta es una herramienta que en esta
máquina nunca se instaló: el hub gioser no trae nix, y los docs/state/seed-*.json del repo se
generaron en otra máquina el 2026-08-10. El coste no es el susto, es el desvío.
Ahora es una precondición explícita que dice qué falta y qué hacer, y sale 2.
⚠ Dos errores míos que los controles cazaron en el acto, y por eso van escritos:
· Eximí a --calibrar de la comprobación 'porque no usa nix'. Lo usa: el control lo mostró
imprimiendo 'nix eval lote 1 (3 nombres)…' y muriendo igual. La exención era una suposición
con forma de hecho.
· Con la precondición delante del uso, 'seed-graph.py' a secas pasó a quejarse de nix en vez
de explicarse. Un fallo de entorno va DESPUÉS del uso.
Verificado en los tres caminos: sin args → uso; --frontera → mensaje de entorno; --calibrar →
mensaje de entorno.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Las 6 que quedaban (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) no tenían copia en
el corpus, así que borrar una habría roto su cola: la resolución es sibling-first y después el
catálogo PADRE, nunca una cola hermana. Se promueve una y se barren las 16.
Las 16 eran byte a byte IDÉNTICAS entre sí, con el mismo ArtifactHash — medido con 'hammer hash',
no deducido del nombre. Cada una la comparten entre 2 y 4 imágenes, que es exactamente el
criterio que dejó escrito xkeyboard-config al promoverse: lo que comparten varias imágenes tiene
que vivir en el corpus o no lo alcanzan.
A diferencia de aquella, acá las copias SÍ se barren en el mismo movimiento: sin copia en el
corpus no había dónde caer, y dejarlas sería mantener hasta 4 ficheros que son el mismo hash.
Verificado con la huella de las 1167 recetas antes y después:
ficheros de receta 1167 → 1157 (16 borradas, 6 promovidas)
hashes supervivientes ninguno se movió ⇒ CERO rebuilds
los cinco grafos deuda 0, huérfanas [], sellados == recetas
static-audit MIENTEN 0 de 690
duplicados SOMBRAS REDUNDANTES: 0 ← de 14
⚠ Y una lectura que casi publico mal: mi primera comparación de hashes dijo 'CAMBIÓ ✗' en las
seis. Era el grep de la comparación, que no casaba y dejaba el valor 'antes' vacío — los hashes
nuevos eran idénticos a los medidos minutos antes. Un instrumento roto que grita es tan malo
como uno mudo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Las 14 sombras marcadas 'redundante' dan el MISMO ArtifactHash en todas sus colas — no son
variantes, son el mismo artefacto declarado dos, tres, cuatro y hasta cinco veces. Medido con
'hammer hash' receta por receta, no deducido del nombre.
Se barren sólo las que YA tenían copia en el corpus, porque la resolución es sibling-first: una
dep desde incoming-kde busca incoming-kde/x y, si no está, CAE al corpus/x. Son 8 nombres, 14
ficheros: bzip2-shared, glib-shared, lcms2, mpc, pcre2-shared, sqlite-shared, xkeyboard-config
y xz-shared.
13 de los 14 eran byte a byte idénticos al del corpus. El decimocuarto, xkeyboard-config,
difiere en 14 líneas de COMENTARIO (mismo hash), y ese comentario es el permiso explícito para
esto: quien la promovió el 2026-09-05 escribió que dejaba las copias a propósito y que
'barrerlas es otra unidad de trabajo, no un efecto colateral de ésta'. Ésta es esa unidad.
Verificado con la huella de las 1181 recetas antes y después: NINGÚN hash superviviente se
movió ⇒ cero rebuilds. Y tras regenerar los cinco grafos: deuda 0, huérfanas [], y todas las
clausuras de perfil siguen completas.
⚠ Nota sobre una lectura que casi me confunde: los conteos de 'recipes' NO bajan (kde sigue en
1034) y eso es lo correcto — cuentan nodos alcanzables, no ficheros. Antes incoming-kde/x tapaba
a corpus/x; ahora el del corpus ocupa su lugar. Un nodo por otro.
Quedan 6 nombres (fuse3, hwdata, icu4c, libdisplay-info, libelogind, lua) sin copia en el
corpus: ésos exigen promover una primero, y van aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Cuarta vez que cae el mismo cable. Los comentarios del propio fichero ya lo cuentan para GNOME
(2026-07-27), COSMIC (2026-08-03) y wlr/sway (2026-08-26): un frente que el latido no regenera
envejece en silencio, y un grafo viejo miente con la misma cara que uno fresco.
keystones.json y duplicados.json estaban parados en 2026-09-05. Con un agravante que los otros
tres no tenían: keystones es el fichero que responde '¿qué construyo después?', así que rancio
no se limita a envejecer — DESINFORMA. Medido: decía '1 nudo en deuda, camino crítico
corpus/firefox → corpus/atuq' con los dos sellados hacía horas. Regenerado dice 0 nudos en
deuda, que es la verdad.
Van también al 'git add' Y al commit acotado por pathspec: regenerar sin publicar deja el
fichero fresco en el hub y viejo para todos los demás.
Probado con un ciclo real: 'keystones.json ✓ duplicados.json ✓ estado commiteado+pusheado'.
⚠ De paso, dos cosas que este rato dejó claras sobre mis propias comprobaciones: 'pgrep' y
'grep [c]osecha-cron' se autodetectan cuando MI comando menciona el fichero, así que la guarda
'¿está corriendo?' dio dos falsos positivos. La comprobación fiable mira /proc/*/cmdline y pide
que argv[0] sea un shell y argv[1] el script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'-Ddefault_library=static' gobierna cómo se construye la librería PROPIA, y eso funcionaba: el
artefacto trae libjson-glib-1.0.a. Pero no dice nada de con qué se enlazan los EJECUTABLES, así
que json-glib-format y json-glib-validate salían dinámicos contra libffi.so.8, libz.so.1 y
libc.so — y ese libc.so es la musl de zig, 255 bytes de linker script en el host. O sea binarios
que NO CORREN fuera del sandbox, en una receta que declara link=static. Mismo remedio que
appstream, que ya llevaba --prefer-static.
Medido antes y después:
json-glib-format dinámico, 3 NEEDED → ESTÁTICO, 0 NEEDED
json-glib-validate dinámico, 3 NEEDED → ESTÁTICO, 0 NEEDED
libjson-glib-1.0.a igual (los consumidores no cambian de forma)
Y la prueba del consumidor, que es la que vale: json-glib-format sobre un JSON real corre EN EL
HOST y devuelve {"a":1}. (El ruido de libgvfsdbus.so en stderr es glib intentando dlopen
módulos GIO desde un binario estático: inocuo, la salida sale bien.)
Cascada reconstruida en orden de onda, con el cierre transitivo sacado de yupana y no de un grep
—que no veía a zathura ni a zathura-pdf-poppler por tener el array de deps multilínea, y la regla
del repo es justamente ésa—: zathura, incoming-cosmic/xdg-desktop-portal, zathura-pdf-poppler.
Verificado después, no antes:
static-audit MIENTEN 0 de 690 ✅ toda receta que declara link=static lo cumple
los 5 grafos deuda 0
repro REPRODUCEN 3 · DERIVA 0 · NO-DETERMINISMO 0
⚠ incoming-gnome/json-glib NO se toca: declara link="dynamic" a propósito porque el shell de
GNOME dlopea .so reales para introspección. No miente — es honesta, y su radio de 10 no se paga.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El vigía decía 5 fuentes muertas y sólo UNA estaba en riesgo de verdad: busybox y freetype
tenían el tarball en la caché local Y en el mirror, y firefox-pgo-profile apunta a .invalid a
propósito. La que no tenía copia en ningún lado era libtool (incoming-kde), con ftpmirror.gnu.org
devolviendo 502 — o sea una receta que ya no se podía reconstruir y nadie lo sabía.
Arreglado: el tarball se bajó de ftp.gnu.org, se verificó contra el sha256 PINEADO (coincide
byte a byte), y está en la caché local y en el mirror. La URL de la receta pasa al espejo que
responde, y se midió que eso no re-hashea nada — ANTES y DESPUÉS dan el mismo
b3:5c09bff5..., que es el ADR 0013 comprobado en vez de citado.
Y el vigía ahora cruza cada fuente muerta con la caché local y el mirror, porque mezclar
'upstream caído con el blob guardado' (noticia) con 'caído y sin copia' (emergencia) hace que
el número suba, nadie lo mire, y el día que uno importe esté enterrado entre cuatro que no. A
un guardián lo mata el ruido. Ahora informa EN RIESGO aparte:
== vivas 1175 / 1179 muertas arriba 4 EN RIESGO 0
⚠ Y si el mirror no se puede consultar —el worker no tiene la clave del Storage Box— dice
'indeterminado', NUNCA 'EN RIESGO': un instrumento que no pudo correr no puede parecer un
hallazgo.
Probado con rotura a propósito y con los dos controles, que es lo que separa un guardián que
sirve de uno que nunca saltó:
sha inexistente + upstream muerto → EN RIESGO ✓ salta
blob en caché + upstream muerto → cache-local ✓ no salta
mirror inalcanzable → indeterminado ✓ no alarma en falso
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Con '.fleet' ausente —el hub recién clonado, o sea el único caso que importa— el 'cat' de un
fichero inexistente devuelve 1, y como el script corre con 'set -o pipefail' la tubería hereda
ese estado y el '&& mv' no dispara: el fichero se generaba correctamente y se quedaba sin
mover. Se añaden los '|| true' que faltaban.
⚠ Y por qué mi prueba no lo vio, que es lo que vale anotar: extraje el bloque real del script
para probarlo, pero le puse 'set -u' en vez del 'set -uo pipefail' que el script trae. Copié
el código y no el ENTORNO en que corre, y sin pipefail el caso pasaba. El extractor ahora lee
la línea 'set -' del propio script en vez de que yo la escriba de memoria.
Verificado con un ciclo REAL tras borrar .fleet a mano:
── cosecha-cron arranca
==> dev.gioser.net (154.197.1.13)
siembra ✓
manifiesto ✓ (worker: 764 · total: 4743)
.fleet quedó: dev.gioser.net 154.197.1.13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
'.fleet' está gitignored con razón —los workers efímeros entran y salen, y el reaper lo
reescribe en cada ciclo— pero eso tenía un coste que nadie había pagado: un hub recién
clonado nacía con la flota VACÍA y la granja quedaba desconectada EN SILENCIO. La cosecha
decía 'flota vacía' cada 30 min, los artefactos del worker no volvían, estado-granja.sh
reportaba 'no hay worker vivo' con el worker compilando, y nada fallaba. Así se descubrió
esto hoy, por casualidad.
Se separa lo que debe sobrevivir a un clon (scripts/farm/flota-permanente, versionado) de lo
que es estado de ejecución (.fleet). cosecha-cron siembra .fleet desde el fichero fijo al
empezar cada ciclo, uniendo por nombre. Probado con la siembra extraída del script real:
hub recién clonado (.fleet ausente) → queda dev.gioser.net ← el caso que rompía
con un hworker-3 efímero ya dentro → conviven los dos, sin duplicar
ejecutada dos veces más → idempotente, sigue 1 línea por worker
comentarios del fichero versionado → 0 se cuelan
Y de paso el commit del propio cron pasa a ir acotado por pathspec. Hacía 'git add <rutas>' +
'git commit' a secas, que es justo lo que la regla 2 de CLAUDE.md declara insuficiente: el
índice es compartido y el commit se lleva el índice entero. Pesa más acá que en ningún sitio
porque corre desatendido cada 30 min mientras hay agentes trabajando.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El dato ya estaba en memoria y aun así se perdió dos veces, así que ahora vive en los sitios
que se leen sin buscarlos: regla 1 bis de CLAUDE.md (que carga todo agente en cada sesión) y
la skill 'granja' del repo. Se compila en dev.gioser.net PARA NO GASTAR HETZNER; no se
levantan cajas hcloud salvo petición explícita.
Y se arregla la fragilidad que salió al mirar: el reaper de cosecha-cron expulsa de .fleet
todo lo que no esté en hcloud, y el LXC se salvaba SÓLO porque su nombre contiene la subcadena
'gioser' y caía en una lista negra que existe para proteger al hub de Hetzner — no tiene nada
que ver con él. Medido con el bloque real del reaper contra un .fleet de juguete:
nombre CON 'gioser' → 'en LISTA NEGRA ⇒ intocable' sobrevive
el MISMO host como 'pruebasia-lxc' → 'ya no existe en hcloud' .fleet VACÍO
O sea que la granja se mantenía conectada por una casualidad de nombre, y con otro nombre
volvía a 'flota vacía' en el primer ciclo sin que nada fallara. Ahora el reaper pregunta por
SSH si el host responde, que es preguntarle a la máquina en vez de al nombre. Verificado en
las dos direcciones, con control:
host vivo, nombre sin 'gioser' → 'no es de hcloud pero RESPONDE ⇒ se queda (€0)'
host que no responde → 'no responde ⇒ lo saco de .fleet' (intención original)
Queda anotado también que .fleet está gitignored: un hub recién clonado nace con la flota
vacía y la granja queda desconectada en silencio — la cosecha dice 'flota vacía', los
artefactos no vuelven, y estado-granja.sh reporta 'no hay worker vivo' con el worker
compilando. Es como se descubrió esto hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Segunda vez el mismo día: un firefox colgado de la verificación A/B se quedó con el lock de
build de la granja. La causa es del arnés, no de firefox: 'kill $BW' mata el bwrap EXTERIOR,
que no es el init del namespace de PID, así que el proceso de dentro sigue vivo — y si el
arnés corría bajo flock, hereda el fd del lock y deja a la granja muda hasta que alguien mire.
Medido con control negativo y positivo:
matar sólo el bwrap → queda 1 firefox vivo (reproduce la fuga)
setsid + matar grupo → quedan 0 (arreglado)
Se documenta además que el 'flock' del uso lleva '-o', que no es opcional.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El cp nunca corrió: zsh aborta el comando entero cuando un glob no casa, y ese directorio no
tenía ningún .txt, así que tampoco se copió el .log — y el rm -rf siguiente se ejecutó igual.
Borrar sin comprobar que la copia existía.
Se deja un PERDIDO.md en vez de un directorio vacío, que es lo que la regla 3 de CLAUDE.md
advierte: un directorio vacío no es evidencia, es un nombre. La firma medida antes de la
pérdida (7/1/6, cero screenshot) sí está registrada en la cabecera del cazador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
arnés viejo (sólo CONTENT) 98 segfaults · 1 colgada · 49/50 screenshots
arnés nuevo (los cinco) 0 segfaults · 0 colgadas · 50/50 screenshots
Queda dicho en la cabecera qué prueba esto y qué no. El MECANISMO sí: 98 → 0, exactamente 2
por corrida, en el 100% de las corridas; un efecto determinista se refuta con pocas muestras.
El CUELGUE no: 1 contra 0 en 50 pares no es significativo — con la tasa medida (3 en 130 ≈
2,3%) ver cero en 50 tiene ~31% de probabilidad aunque nada hubiera cambiado, y harían falta
~200 corridas para un negativo convincente.
Lo que sí sostiene: las tres colgadas observadas tienen la MISMA huella exacta (7 fallos de
lanzamiento de pestaña, 1 de rdd, 6 messageManager is null, cero screenshot) y las tres
cayeron donde los ayudantes revientan. Ninguna apareció sin el mecanismo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan: con 'exec 9>' +
'flock 9' cualquier descendiente que sobreviva al script retiene el lock para siempre, y el
siguiente que lo pida espera sin que nada falle. Medido hoy: dos firefox colgados de una caza
sobrevivieron al kill de su bwrap y dejaron a la granja hora y media sin poder compilar.
Dos formas de defecto ⇒ dos arreglos, y no son intercambiables:
· cosecha-cron, campana-deuda, harvest-harkaq — el lock cubre el script ENTERO, así que se
re-ejecutan bajo 'flock -o' (cierra el fd antes de ejecutar). Ir poniendo '9>&-' comando a
comando ahí es jugar a los topos. '-E 77' separa 'estaba ocupado' de 'el trabajo falló',
que con el 9> no se distinguían.
· farm-worker-loop, latido — el lock vive en un subshell / es a propósito el mecanismo de
vida, así que basta '9>&-' en los hijos. En latido NO se puede usar -o: ahí el fd retenido
ES como --status sabe que el latido vive sin pidfile. Su modo de fallo era el peor de
todos: un nieto fugado dejaba el latido MUERTO PERO APARENTANDO ESTAR VIVO.
Medido antes de elegir, no deducido del manual:
exec 9> + flock 9 → el nieto RETIENE (control negativo: reproduce el fallo)
exec {L}> (fd auto) → el nieto RETIENE (bash NO lo marca close-on-exec)
flock -o / 9>&- hijo → lock LIBRE
Y probados los cinco sobre el fichero real, no sobre una maqueta: copias truncadas justo tras
el bloque del lock, invocadas con RUTA RELATIVA DESDE OTRO DIRECTORIO (que es lo que rompe un
$0 sin resolver — de hecho la primera versión de harvest-harkaq calculaba YO después del cd y
habría fallado ahí). Los tres entran, ninguno deja el lock tomado por el nieto, y la exclusión
mutua sigue funcionando con su mensaje y su exit 0 — que es lo que un arreglo de locks puede
romper en silencio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El lock lo sostiene la descripción de fichero abierta y los hijos la heredan. Medido hoy: dos
firefox colgados de una caza sobrevivieron al kill del bwrap que los envolvía y dejaron a la
granja sin poder compilar durante hora y media, sin que nada fallara — el siguiente flock
simplemente espera.
Comprobado en los dos sentidos con control positivo y negativo:
flock lock sh -c 'sleep 25 & exit 0' ⇒ el nieto retiene el lock
flock -o lock sh -c 'sleep 25 & exit 0' ⇒ lock libre
Se añade también cómo diagnosticarlo (fuser -v sobre el fichero de lock, que nombra al proceso
fugado) y se deja anotado que los scripts de scripts/farm/ usan el estilo 'exec 9>' + 'flock 9',
vulnerable igual, como deuda conocida sin barrer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Cada arnés headless tenía su propio subconjunto de MOZ_DISABLE_*_SANDBOX, elegido en su
momento sin medir: banco 1 de 5, perfilar 4, codecs 3, ruteo 3, nativo 2, inicio 2. Con
cualquier subconjunto incompleto el sandbox de Firefox sigue intentando montar su
user-namespace dentro del de bwrap, falla con 'uid_map: EPERM' y deja un ayudante
'Sandbox Forked' muerto de SIGSEGV por cada intento — en el 100% de las corridas, después
de escribir el PNG y por eso invisible.
Medido: 2 cadáveres por corrida con sólo CONTENT apagado, 0 con los cinco. Verificado sobre
el arnés ya editado: segv=0 EPERM=0 screenshot=sí.
Acá no se pierde seguridad: bwrap ya es la jaula, el sandbox de Gecko es redundante y lo
único que hace dentro es fallar.
⚠ Cambia las condiciones de medición del banco de PGO: las cifras de docs/26 se tomaron con
el arnés viejo. Los cocientes del PGO comparan dos firefox bajo el mismo arnés y deberían
aguantar; la línea base absoluta de arranque no tiene por qué. Anotado en la cabecera.
No se tocan scripts/wlr/dunst-headless.sh (otro agente trabajando en él) ni los cazadores,
que conservan el entorno viejo a propósito para poder reproducir la condición original.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
No era la presión de memoria. La caza bajo presión (4 niveles × 20 corridas, con el nivel de
presión MEDIDO por MemAvailable y PSI) reprodujo el cuelgue por primera vez en 282 corridas
—2 de 80, 2,5%, compatible con el 3,5% original— pero SIN dosis-respuesta: los dos cuelgues
cayeron en el nivel con PSI 0,00 y los dos niveles apretados dieron cero.
Tres hipótesis muertas, cada una con su medición:
· OOM se lleva al hijo → oom_kill de /proc/vmstat no se movió (delta=0) y los hijos vivían
· presión de memoria → sin dosis-respuesta
· fork server de Gecko → A/B del pref: el proceso forkserver desaparece (35 muestras → 0,
o sea que el pref hizo efecto) y siguen los mismos 2 segfaults
La causa apareció comparando el log de una colgada con el de una BUENA, que es lo que faltaba:
en las 6 buenas TAMBIÉN revientan 2 hijos con SIGSEGV, después de escribir el PNG y por eso
invisibles. Muestreando /proc adentro, se llaman 'Sandbox Forked': los ayudantes que el sandbox
propio de Firefox forkea para montar su user-namespace, que no puede montar porque ya estamos
dentro del de bwrap ('writing /proc/self/uid_map: EPERM').
Las tres cantidades bajan juntas hasta cero, que es forma de cadena causal:
sandbox completo segv=7 SIN screenshot EPERM=9 SandboxForked=725
content.level=0 segv=2 con screenshot EPERM=2 SandboxForked=68
los 3 prefs .level a 0 segv=1 con screenshot EPERM=1 SandboxForked=33
las 5 MOZ_DISABLE_* segv=0 con screenshot EPERM=0 SandboxForked=0
Y el sandbox completo cuelga DETERMINISTA (rc=124 a los 180 s). Comparte familia con el
intermitente pero no está probado que sean el mismo: al determinista le faltan los 'Failed to
launch'. Queda dicho como lo que es.
Arreglo para cualquier firefox headless en bwrap (donde el sandbox de Gecko no aporta nada,
porque bwrap ya es la jaula):
MOZ_DISABLE_{CONTENT,GMP,RDD,SOCKET_PROCESS,UTILITY}_SANDBOX=1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
El banco medía el ciclo COMPLETO del proceso (arrancar → renderizar → capturar → salir)
y nunca se midió cuánto de eso era arranque. Con una página vacía como control, y las 4
cargas × 2 variantes en UNA sola sesión intercalada (restar entre corridas de días
distintos no vale: la misma variante deriva ~1%):
maquetación trabajo 7281 → 4744 ms -34,8% (publicado: -10,9%)
carga ajena trabajo 4550 → 4634 ms +1,8% = cero, y es buena señal
SunSpider trabajo -34 → -6 ms bajo el ruido
Dos correcciones al documento:
1. El -10,9% es correcto para el ciclo completo pero se leía como 'firefox 11% más
rápido'. Sobre el trabajo de la página el PGO rinde -34,8%: tres veces más. El error
subestimaba el propio resultado.
2. La explicación que di del SunSpider era aire. Su trabajo mide -34 ms, NEGATIVO: la
página con el benchmark tardó menos que la vacía. No había nada que medir, y yo le
colgué encima una teoría sobre el JIT. La teoría puede ser cierta; esta medición
nunca la probó.
Las tablas viejas se dejan sin retocar, con un aviso arriba: el error de método enseña
más que el número corregido. Todo el rigor estadístico estaba puesto sobre una cantidad
que no era la que yo creía medir, y ninguna repetición lo habría revelado — sólo un
control, que costó siete corridas de una página vacía.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
Al montar otro banco, la carga estaba en 3,34 (contra 0,13 de las cuatro cazas)
porque otro agente arrancó un cargo, y kswapd0 consumía CPU con CERO memoria
libre: recuperación de páginas bajo presión. Cada corrida mapea un libxul de
227 MB.
Es exactamente la condición que las cazas NO tenían y que los dos bancos con
cuelgues SÍ. No es prueba —el estado de aquellas ventanas no se reconstruye—
pero es la primera pista concreta, y cambia dónde buscar: cazar BAJO PRESIÓN de
memoria, no con la máquina ociosa.
Al reconstruir atuq sobre el firefox con jarlog, murió:
zipfile.BadZipFile: Bad magic number for central directory (rebrand.py)
El jarlog convierte el omni.ja al formato «jar optimizado» de Mozilla, que mueve
el directorio central al principio:
sin jarlog: PK\003\004 ZIP estándar — zipfile lo abre, 5306 entradas
con jarlog: \376\204#\0 zipfile lo rechaza
Y atuq reempaqueta el omni.ja con zipfile para su branding. O sea que el jarlog
rompe el navegador propio de la distro.
La cuenta es asimétrica: el COSTE está medido y el BENEFICIO no —el banco corre
con caché caliente, donde reordenar el omni.ja no ahorra ninguna lectura, y dio
-0,1%, por debajo del suelo de ruido del propio banco—. Cambiar algo que
funciona por una ganancia que no se pudo medir, rompiendo algo que sí
funcionaba, es mal negocio.
Revertido a los hashes YA construidos y medidos (perfil 3647c6be, firefox
3d199174, atuq fab2fbfb): cero reconstrucciones. La documentación va fuera de
los campos hasheados, verificado antes y después.
Se retoma cuando (1) se mida el arranque EN FRÍO, que es su terreno, y (2)
rebrand.py sepa leer el jar optimizado o des-optimizarlo antes. El blob
92497cdd del mirror ya trae el jarlog: retomarlo es cambiar una línea.
Y LA LECCIÓN, que es la que más vale: el jarlog parecía GRATIS. «Es lo que hace
upstream y no cuesta nada» lo escribí yo en este mismo documento hace unas
horas. El coste no apareció midiendo el jarlog sino CONSTRUYENDO LO QUE DEPENDÍA
DE ÉL. Una función que se declara gratuita sin haber reconstruido a sus
consumidores no es gratuita: es no medida.
Dos defectos con un solo arreglo. El artefacto instalaba en usr/lib/firefox y
usr/bin/firefox —LAS MISMAS RUTAS que la receta firefox— y además tenía el mismo
fallo del lanzador que se le arregló a firefox ayer: verificado sobre el sellado,
CERO entradas de RUNPATH, así que moría con «Couldn't load XPCOM».
Se renombra el árbol a `waterfox` y se añade el lanzador con LD_LIBRARY_PATH.
Renombrar es seguro porque Firefox es REUBICABLE: localiza omni.ja relativo a su
propio binario, que es lo que hace que sus tarballs anden desde cualquier
directorio. NO se toca MOZ_APP_NAME: eso exigiría tocar confvars.sh del árbol y
arrastra el branding, que es la decisión que esta receta deja abierta y que no
se toma de paso.
Guardián nuevo: ni una ruta `firefox` en el artefacto. Si mach install cambia de
layout y algo vuelve a aterrizar ahí, la colisión regresa en silencio — dos
artefactos publicando el mismo fichero, y en la imagen gana uno sin que nada lo
diga.
Verificado CORRIENDO: arranca headless y renderiza (captura de 518.735 bytes,
idéntica en tamaño a la de firefox sobre la misma página).
Y correrlo destapó lo que ninguna inspección estática mostraba, anotado como
tercera cosa a decidir: WATERFOX SALE A LA RED AL ARRANCAR, SOLO, a bajar las
listas de su propio bloqueador desde easylist.to. Acá falla porque el sandbox no
tiene red; en una imagen real sería una conexión no solicitada en el primer
arranque. Esta distro apagó la telemetría de firefox exactamente por eso.
Dos corridas del banco de PGO se colgaron en 120.034 y 120.030 ms — exactamente
el timeout del arnés, o sea bloqueos y no lentitud. Dos de 56 (~3,5%).
NO SE REPRODUJO. 202 corridas en cuatro condiciones, cero cuelgues:
un binario, un sandbox compartido ......... 30 · 0
4 binarios alternando, un sandbox ......... 60 · 0 (descarta churn de caché)
4 binarios, UN SANDBOX NUEVO POR CORRIDA .. 56 · 0 (descarta los namespaces)
ídem con LAS PÁGINAS EXACTAS del banco .... 56 · 0 (descarta la página)
Si la tasa fuera 3,5%, ver cero en 202 tendría probabilidad ~0,06%. La tasa real
bajo estas condiciones no es ésa, y lo que falta está fuera de ellas.
UN ERROR DE MÉTODO QUE COSTÓ 146 CORRIDAS, anotado en el script para no
repetirlo: las tres primeras cazas usaron flex.html y tablas.html porque las
tenía a mano, cuando las colgadas habían sido en fuera-del-corpus.html y
del-corpus.html. Estuve probando una condición que NO era la observada,
creyendo que sí. Antes de concluir «no se reproduce», comprobar que el
instrumento reproduce lo que dice reproducir.
Lo que sí se aprendió, y acota: es TODO O NADA. En 202 corridas ninguna pasó
siquiera de 45 s — o terminan en ~20 s o se bloquean hasta el timeout. No es
degradación, es bloqueo. Sobrevive la hipótesis que no se puede probar
retroactivamente: contención transitoria de otra cosa en la máquina —que es
compartida con otros agentes— durante esas dos ventanas.
El script queda para que la PRÓXIMA vez se capture en el acto (wchan, syscall,
state, memoria y carga del host, y el log del navegador de esa corrida) en vez
de empezar de cero.
sin PGO v1 v2 v3 (= v2 + jarlog)
DOM/maquetación 23.008 20.851 20.527 20.502 (-10,9%)
SunSpider 16.006 15.859 15.869 15.838 (-1,0%)
jarlog solo: -0,1% y -0,2%
Indistinguible de cero, y el argumento no es que los rangos se solapen —que se
solapan— sino que la MISMA variante varía ~1% entre corridas de días distintos
(v1 dio 20.851 hoy y 21.050 ayer). El suelo de ruido del banco es diez veces el
efecto buscado.
No es un fallo del jarlog: el banco corre con la caché de página CALIENTE, y ahí
reordenar omni.ja no ahorra ninguna lectura. Su terreno es el arranque en frío,
que este instrumento no reproduce.
Lo que SÍ está probado es que la función existe, y del lado del artefacto:
libxul IDÉNTICO byte a byte entre v2 y v3, omni.ja distinto en 26 bytes. El
jarlog tocó exactamente lo que debía y nada más. Se conserva porque es lo que
hace upstream y no cuesta nada; lo que no se afirma es una ganancia no medida.
Y no hizo falta la extensión Quitter, que este mismo documento daba por
bloqueante: profileserver.py sólo traduce JARLOG_FILE a MOZ_JAR_LOG_FILE.
Anotado además: los dos atípicos del banco son 120.034 y 120.030 ms, o sea
exactamente el timeout del arnés. No son ruido de carga sino corridas COLGADAS
al arrancar en headless, 2 de 56 (~3,5%).
El jarlog registra en qué orden se LEEN los ficheros dentro de omni.ja al
arrancar; con él, el empaquetador los reordena y el arranque hace lecturas
secuenciales en vez de saltar por el archivo.
NO hizo falta la extensión Quitter de Mozilla, que era lo que yo daba por
bloqueante: basta MOZ_JAR_LOG_FILE en el entorno del navegador. Su
profileserver.py sólo traduce JARLOG_FILE a esa variable — leerlo costó un grep
y ahorró rehacer el arnés entero.
Y se genera en una corrida APARTE, que está medido y no supuesto. Mozilla lo
emite en la misma sesión del profileserver; nuestro arnés arranca un navegador
POR PÁGINA, así que había que saber si el fichero se acumula o se pisa. Se pisa:
tras rejilla.html y tras tablas.html dio exactamente los mismos 27.652 bytes y
469 líneas. Su contenido lo domina el ARRANQUE, no la página, así que una
corrida dedicada vale igual que una de 46 — rehacer el perfilado sólo para
obtenerlo habría sido gasto sin diferencia. Se conserva el profdata ya medido
(el del -11,4%), que es lo que hace comparables los números.
Guardián propio: un jarlog vacío o que no nombre los archivos reales NO rompe el
build de firefox, sólo deja el omni.ja sin ordenar — o sea que se pierde justo
lo que se vino a buscar, en silencio. Se exige que mencione los DOS archivos que
el navegador abre (omni.ja y browser/omni.ja).
atuq b3:fab2fbfb, 340 M, libxul de 227.043.776 bytes: el mismo del motor con el
perfil de 46 páginas. Verificado que ARRANCA desde una hidratación limpia de
escritorio-sway, 0 errores de relocación.
El corpus queda en 860/862 sellados —los dos que faltan son `ajeno`, que es
frontera y no deuda— y el vigía de sonames en CERO huecos en los cinco perfiles.
sin PGO v1 (36 pág) v2 (46 pág)
DOM/maquetación 23.285 ms 21.050 (-9,6%) 20.640 (-11,4%)
SunSpider 3d-raytrace 16.003 ms 15.846 (-1,0%) 15.857 (-0,9%)
La mejora en maquetación NO se afirma por las medianas sino por la separación:
SEIS de las siete muestras de v1 son más lentas que TODAS las de v2; sólo una
cae dentro del rango de v2.
En SunSpider v1 y v2 son INDISTINGUIBLES —los valores se entrelazan por
completo—, que es exactamente lo esperable: ese camino lo ejecuta el JIT y el
PGO no lo toca. Un corpus mejor no puede mejorar lo que el PGO no alcanza. El
-0,9% frente al -1,0% no dice que v2 sea peor ahí: dice que no se distingue.
Una de las siete corridas de v2 dio 120.029 ms contra ~20.600 de las otras seis.
La mediana lo ignora por diseño; una media lo habría convertido en «v2 es
catastróficamente peor». Fue la razón de elegir mediana antes de ver un número.
Y lo que el banco no puede afirmar, escrito: las 10 páginas nuevas ejercitan los
mismos subsistemas que la página de medición, así que el parentesco con lo que
ahora se entrena es mayor que antes. El -11,4% es real para ESTA carga;
generalizarlo a «cualquier página» sería el mismo error que cometía el corpus de
Mozilla, en la otra dirección.
`scripts/test-atuq-inicio.py` le pregunta al motor por sus overrides en vez de
mirar el XPI en el disco: moz-extension://…/inicio.html en las dos, contra
about:home / about:newtab cuando se le saca el XPI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
La v0.3 las puso por extensión de sistema y quedó como afirmación. Que el XPI
esté en el artefacto no dice nada: el override lo puede rechazar el gestor de
extensiones, lo puede pisar una política, o el navegador puede arrancar con el
chrome viejo cacheado. Ninguna de las tres falla ruidosamente — se ve una pestaña
nueva perfectamente normal, que es de otro.
Se mide preguntándole AL MOTOR, no mirando el disco: una sonda con permiso
`browserSettings` lee `homepageOverride` y `newTabPageOverride`.
positivo moz-extension://…/inicio.html (las dos)
control about:home · about:newtab
El control negativo borra el XPI de `inicio` dentro del overlay temporal —el
rootfs real no se toca— y exige el resultado contrario.
Y una escotilla `ATUQ_DIR` en las dos sondas nuevas, con su aviso a gritos: el
corpus es compartido y hoy mismo el perfil PGO v2 de otro frente re-hasheó
`firefox` y con él `atuq`, así que el artefacto VIGENTE no existe en ningún store
hasta que alguien pague un build de horas. Sin escotilla no se puede correr una
sola prueba de atuq en esa ventana; con ella se corre contra un artefacto viejo A
SABIENDAS, y por eso el resultado no se puede citar como «atuq de hoy pasa».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
Estaba en un scratchpad y se perdía; ahora vive en el repo con el porqué de cada
guarda, porque las tres se pagaron durante la primera corrida:
1. --unshare-pid en el bwrap. Sin él los hijos SOBREVIVEN al sandbox: un
http.server quedó vivo 19 HORAS, retuvo el puerto y envenenó las corridas
siguientes con «Address in use».
2. Puerto aleatorio por corrida. Elimina la colisión de raíz en vez de
detectarla: si otro proceso tiene un puerto, este arranque usa otro.
3. Marcador único por corrida, servido desde una copia ESCRIBIBLE del corpus.
La versión anterior horneaba el marcador como constante del script, y eso
rompía justo lo que el chequeo existe para probar: el servidor huérfano
servía el MISMO fichero de marca y el chequeo lo daba por bueno. Verificaba
«alguien sirve este contenido», no «este servidor es el mío». Con el marcador
por corrida, un servidor ajeno devuelve otra cosa y se aborta.
Y queda escrito por qué los sandboxes de Firefox van apagados en el
entrenamiento: con ellos los hijos mueren con signal 11, el proceso que renderiza
no nace, el servidor registra 0 GET y el único .profraw es el del padre
arrancando — un perfil de nada, con todo en verde. Es lo que hace el
profileserver.py de Mozilla. Sólo aplica al entrenamiento.
La espera al servidor va con reintento y no con un sleep fijo: 2 s concluían
«no responde» sobre uno que sí iba a responder.
El corpus se amplió por una MEDICIÓN, no por corazonada. Con las 36 de Mozilla
la ganancia era -9,4% en DOM/maquetación y -1,0% en 3d-raytrace, y la razón es
que un benchmark JIT-bound es casi ciego al PGO: el bucle caliente lo ejecuta
código que el JIT emite en runtime, y el PGO optimiza el intérprete, el GC y el
propio JIT, no lo que el JIT produce. El corpus de Mozilla está dominado en
número de páginas por SunSpider, o sea que entrenaba justo donde menos rinde.
Con las 10 de scripts/pgo-corpus/ sumadas, el efecto se ve EN EL PROPIO PERFIL:
ejecuciones registradas 5.885.254.799 -> 38.498.366.335 (6,5x)
máximo por función 416.219.136 -> 1.552.416.768 (3,7x)
Funciones y bloques totales no cambian (501.521 / 3.734.991) porque son la
estructura estática del binario instrumentado, no lo que se ejecutó.
Blob nuevo publicado y verificado bajándolo del mirror por el mismo camino que
usa hammer: 17.525.708 bytes, sha256 95472411.
Antes de escribir el crate del host en tawasuyu había que saber si el mecanismo
funciona en nuestro build. Funciona, y el cuadro deja las cuatro cosas que hacen
falta para escribirlo: dónde va el manifiesto (/usr/lib/mozilla, no el appdir),
qué instala de verdad la extensión (el escaneo de distribution/extensions, NO el
install_url de la política), que el host tiene que ser un proceso largo, y que el
único error que nombra la causa es el del puerto de connectNative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
Todo el §6 del SDD 26 —sct, descargas al CAS, archivo con RAG, torrent— pasa por
un solo mecanismo: un proceso Rust hablando native messaging con la extensión.
Antes de escribir ese crate en tawasuyu conviene saber si el camino existe en
NUESTRO build, que es propio, rebrandeado y con MOZ_REQUIRE_SIGNING vacío.
Existe. Una extensión de diez líneas y un host de tres:
positivo MENSAJE {"ok":true}
control DESCONECTADO No such native application puente_atuq
El control no sólo falla: NOMBRA la causa, que es lo que confirma que la ruta del
manifiesto es la que se probó.
TRES COSAS MEDIDAS QUE NO ERAN OBVIAS:
1. El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el
appdir. Gecko lo busca por `XRESysNativeManifests`, que en Linux sale de un
`/usr/lib/mozilla` compilado, y el rebranding a atuq no lo mueve.
2. Un host que escribe y SALE pierde el mensaje. La primera sonda hacía printf y
terminaba: el puerto llegaba a onDisconnect «sin error y sin mensaje», o sea
el peor informe posible — parece que el camino no existe. Con un sleep detrás
del printf, el mensaje aparece. El host de verdad es un proceso largo, así que
en producción no se nota; en una prueba, sí.
3. `ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea
de log. Probado con normal_installed y con force_installed, y con el XPI dentro
y fuera del appdir: ninguna instala. Lo que instala las extensiones de atuq es
el ESCANEO de `distribution/extensions/`. La política sirve para fijarlas y
configurarlas; leerla como «esto es lo que las instala» es un error fácil,
porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.
`sendNativeMessage` no sirve para diagnosticar: devuelve «An unexpected error
occurred» para todo. El error del puerto de `connectNative` es el único que
nombra la causa.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
Medida la ganancia del PGO ayer: -9,4% en una página de DOM/maquetación y -1,0%
en 3d-raytrace de SunSpider. La razón de la diferencia es que un benchmark
JIT-bound es casi CIEGO al PGO: el bucle caliente no lo ejecuta el C++ de
SpiderMonkey sino el código máquina que el JIT emite en runtime, y el PGO
optimiza el intérprete, el GC y el propio JIT — no lo que el JIT produce.
El corpus de Mozilla está dominado en número de páginas por SunSpider, o sea que
entrenaba mucho justo donde el PGO menos rinde. Estas diez ejercitan el camino
que sí es C++ de punta a punta:
flex flexbox anidado, wrap, alineaciones
rejilla CSS Grid: pistas, áreas nombradas, auto-fit
tablas tablas grandes con table-layout FIJO y AUTO (dos algoritmos)
texto columnas, shaping, reflujo por cambio de ancho
selectores DOM profundo contra 600 reglas, muchas que NO casan
pintado degradados, sombras, opacidad, mix-blend-mode
transformar transforms y contextos de apilamiento
svg paths, degradados, clip
desbordes overflow anidado, position:sticky, scroll programático
reflujo layout thrashing: leer y escribir geometría alternadamente
Reglas que cumplen todas, y no son de estilo:
· DETERMINISTAS (LCG propio, ni Math.random ni Date ni red) — dos corridas hacen
lo mismo, así que dos perfiles difieren por el timing de los contadores y no
por haber visitado caminos distintos.
· AUTOCONTENIDAS — el perfilado corre sin red.
· NUESTRAS — nada de páginas ajenas capturadas, que traerían licencia y
fragilidad.
· ACOTADAS — 16 a 32 s cada una; el perfilado arranca un navegador POR PÁGINA.
Validadas las diez contra el firefox sellado: todas renderizan y producen
captura. `flex` hubo que acortarla de 40 a 12 cajas raíz: con 40 la página medía
68.241 px de alto y la captura moría con «Failed to allocate a surface due to
invalid size». El layout ocurría igual, pero sin captura no se ejercita el
pintado, que es justo la mitad que se venía a entrenar.
Esta prueba dio «NO concluyente — la pestaña sin contenedor no salió directa»
dos veces seguidas, y la causa no tenía nada que ver con atuq: **otro frente de
esta misma máquina tenía levantado un `python3 -m http.server 8099`** desde hacía
dos horas. La oreja del destino no podía atarse, no llegaba nada, y el informe
publicaba un fallo inexistente. En un repo que comparten varios agentes, un
puerto fijo es estado compartido sin dueño.
Y había un segundo bug que es el que lo hizo dañino: el `bind` vivía DENTRO del
hilo de la oreja, donde `fatal()` no puede matar el proceso — `SystemExit` en un
hilo secundario sólo termina ese hilo. Así que la prueba imprimía «el puerto está
ocupado, la medición no valdría nada» y **seguía adelante hasta publicar un
veredicto**. Un guardián que avisa de que no puede medir y mide igual es peor que
uno que no mide.
Ahora las dos orejas se atan en el hilo principal, con el puerto 0: lo elige el
kernel. Si no hay puertos, no hay prueba.
Con eso, y contra el atuq de hoy (d36ae188, el del arreglo del LD_LIBRARY_PATH):
positivo destino 40843 · proxy 38503 → GET /directo al destino, SOCKS5 al proxy
control destino 40917 · proxy 37113 → las dos directas, cero al proxy
O sea que el ruteo por contenedor del §6.8 sigue en pie y no lo rompió nada de
hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs