Cuatro arranques fríos en serie, el perfil partiendo del 5 que dejó la corrida del umbral:
S1 pin 0 antes 5 después 1 el navegador ✓ +97 s
S2 sin pin antes 1 después 2 el navegador ✓ +98 s
S3 sin pin antes 2 después 3 (matada a los 60 s)
S4 sin pin antes 3 después 4 EL DIÁLOGO ✗ en 420 s
Tres cosas:
· la tasa, por fin con perfil sano: dos corridas independientes y pegadas, +97 y +98 s, contra los
+123/+195/+197/+204 de antes. Con el anfitrión descargado el mismo artefacto pinta en la mitad del
tiempo ⇒ el número es de la máquina y la carga tanto como del producto, y se publica con sus
condiciones;
· el umbral se reprodujo solo y en otra secuencia: S4 arrancó con 3, sumó a 4 y abrió el diálogo —
segunda medición independiente del mismo borde, esta vez identificando la ventana por `min_size`
y no por el título (que está traducido, así que un grep por «Troubleshoot Mode» en una imagen en
castellano no engancha y su ausencia se lee como la conclusión contraria);
· ⚠ y una corrida que PINTA no limpia el contador: S2 pintó y dejó 2. Más todavía,
`toolkit.startup.last_success` vale lo mismo en las CUATRO (1789494795, las 17:53) ⇒ el gancho de
cierre no corrió ni una vez, ni en las que pintaron. Pintar no es terminar de arrancar.
Entonces «quién limpia el contador» SIGUE SIN MEDIR, y el propio documento decía que esta serie lo
cerraría: no lo cierra, y queda escrito así. Hace falta una corrida que deje al navegador terminar
de arrancar y salir limpio.
Lo más caro es para el instrumento: con `--until-paint` cada corrida suma uno, pinte o no, así que
una serie se rompe sola a la cuarta — que es lo que pasó ayer sin que nadie lo viera. El arnés ahora
AVISA cuando el contador está en 3 («esta corrida va a abrir el diálogo, no el navegador») y la
receta para una serie queda escrita: pinnear `--crashes 0` en cada corrida, que resetea la base
(S1 lo muestra partiendo de 5).
También cae una candidata mía, con un cero que viene con control (medido por la otra sesión): el pref
no tiene default en este build, así que fijarlo en 0 SÍ se persiste y el «ausente» no era el pin.
Medido en los dos lados, partiendo del perfil en 2 y con tres fases más en un arranque: el que
encuentra 2 guardado PINTA (y queda en 3); el que encuentra 3 lo sube a 4, compara 4 > 3 y abre el
diálogo (y queda en 4); el siguiente queda en 5 y vuelve a abrir el diálogo. O sea que la
comparación va DESPUÉS del incremento del propio arranque: contando desde la última corrida que
terminó bien, son cuatro arranques interrumpidos seguidos y el quinto lanzamiento ya no abre el
navegador.
`toolkit.startup.last_success` vale lo mismo en las tres fases (1789494795, el arranque de la última
corrida sana): el contador sube mientras esa marca no se mueve. Con las dos cifras juntas, un
contador que no cambió deja de ser ambiguo entre «no subió» y «subió y se limpió».
⚠ Y eso refuta una atribución que yo había escrito ayer y que estaba en tres sitios: mostrar el
diálogo NO limpia el contador (k2 lo dejó en 4, k3 en 5). Queda retirada del SDD y del comentario
del arnés. La desaparición del 16 entre las 17:12 y las 17:20 pasa a ser un hecho SIN explicación
medida, con dos candidatas escritas y ninguna dada por buena — y una de ellas es mía y fea: todas
las corridas que pintaron llevaban `--crashes 0` pinneado, que es el valor por DEFECTO, y un pref
igual al default no se persiste. El «ausente» puede ser eso y no una limpieza. Queda dicho cómo se
cierra: dejar el contador en 2 SIN pinnearlo, correr hasta que pinte y leerlo después.
Un discriminador que salió gratis: `ATUQ-EXIT=137` es «la matamos» (128+9) y `ATUQ-EXIT=0` es «se
fue sola» por el diálogo. El código de salida contesta lo que una captura no podía.
Medido por la otra sesión del frente (work/umbral-1.txt), con la predicción escrita antes de mirar.
Cuatro secciones discutiendo por qué la ventana de atuq salía de 117x70, y no era la ventana de atuq.
Lo dice el mismo volcado de protocolo que ya se había leído, dos líneas más abajo de donde paró la
lectura:
558 -> xdg_toplevel#58.set_title("Open atuq in Troubleshoot Mode?")
Es safeMode.xhtml, el diálogo de Modo de resolución de problemas. La cadena, cada eslabón con su
fuente y ninguno deducido:
1. el perfil del usuario traía `toolkit.startup.recent_crashes = 16` (prefs.js del 15-Sep 00:36,
ANTES de las corridas del día; leído con debugfs sobre la partición, sin montar y sin root);
2. el umbral de NUESTRO build es 3 (browser/omni.ja -> defaults/preferences/firefox.js:831);
3. BrowserGlue.sys.mjs:389 abre el diálogo MODAL en `_beforeUIStartup()`, que por su propio
comentario corre «before the first window is opened»: no hay navegador detrás esperando;
4. el `set_max_size(348, 16332)` sale del Fluent del diálogo (`max-width: 400px`);
5. y el proceso se va solo con `ATUQ-EXIT=0` porque el único camino de ese diálogo que termina el
programa es `onCancel()` -> `quit(eForceQuit)`. Quién lo cancela no está medido.
El control, en los dos sentidos, mismo artefacto y mismo perfil: con el contador limpio pinta a
+123 s (704.458 px); fijado en 16, nada en 300 s y vuelve el diálogo. Y una honestidad que cambia la
fuerza del argumento: en la corrida limpia el `--crashes 0` fue un NO-OP —Gecko ya lo había
borrado—, así que la que prueba la causa es la que lo pone de vuelta y rompe de vuelta.
Cae también la sospecha del §6.10.duodecies: con `WidgetScreen:5`, Gecko ve la pantalla completa
(1280x800 de workarea) CINCO SEGUNDOS antes de crear la ventana. No había carrera con wl_output.
Y lo más caro: la serie de primera-pintura.json está contaminada por el propio andamiaje. El arnés
mata la VM con el navegador vivo, cada arranque sin terminar es una caída para Gecko y pasados 3 el
sujeto medido deja de ser el navegador. «3 de 13» y «2 de 10» midieron un perfil que se degradaba
corrida a corrida. Lo que cada corrida sume exactamente NO está medido y así queda escrito.
Queda una decisión de producto que no se mete de paso porque re-sella el artefacto: si atuq debe
traer `toolkit.startup.max_resumed_crashes = -1`.
Mudando `api.sergio.gioser.net` a la caja de produccion, `takana qorpa provision` aborto:
Error: no encuentro harkaq-exec en /root/.cache/harkaq/harkaq-exec
construilo: gcc -O1 -Wall -static -o ... scripts/harkaq/harkaq-exec.c
El mensaje es bueno y la receta que propone es IMPOSIBLE de seguir: en una caja instalada no hay
gcc, ni `scripts/`, ni arbol de desarrollo. El binario solo existia como un `gcc` a mano en la
cache de `$HOME` del hub, o sea que la jaula del ADR 0015 funcionaba unicamente en la maquina
donde alguien la habia compilado.
Y su companero estaba igual, medido en `build-state.json`: `bwrap` sellado desde hace meses con
`"perfiles": []` — CERO perfiles. Lo invocan por PATH tanto `qorpa` como el sandbox de
`takana build` (`takana-build/src/sandbox.rs`), asi que **ninguna imagen de takana podia enjaular
nada, ni construir**. Es [[subcomando-sin-driver]] un piso mas abajo: el CLI que los llama viaja
en todas las imagenes y sus herramientas en ninguna.
Tres piezas:
· `recipes/harkaq-exec.toml` — nueva. Estatico musl, `b3:cd34954f...`, 269 K. El pin va al commit
que toco la FUENTE (`1d9ddcee`, 2026-09-03) y no a HEAD: el `.c` no se mueve desde entonces y el
repo commitea cada media hora por el cron de la cosecha — pinear HEAD re-hashearia la receta cada
media hora sin que su fuente cambiara. La fuente sigue en `scripts/harkaq/` y no en un arbol
propio porque tres scripts la compilan desde ahi y dos copias divergen en silencio.
· `qorpa.rs` — busca el binario tambien en `/usr/bin`, que es de donde sale en cualquier maquina
que no sea el hub. El orden es HARKAQ_BIN (lo que el operador declara) → arbol de desarrollo →
paquete, para que un cambio en la jaula se pruebe sin instalar nada. Y el error ya nombra las
dos salidas, no solo la del hub.
· `targets.toml` — los dos en `perfil.base`.
Medido en la caja de produccion tras aplicarlos: `bwrap 0.11.0` + harkaq-exec responden, y
`takana qorpa provision sergioh-api` instala python 3.14.7 con pacman DENTRO de la jaula
(`[harkaq] jaula puesta: ABI 9`). El 3.14 no es casualidad: el venv de gioser trae extensiones
`cpython-314-...-gnu.so`, asi que la imagen de Arch pineada da la misma serie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las otras tres recetas cgo (sq, usql, gitea) dan DERIVA, no no-determinismo: sus dos
reconstrucciones coinciden entre sí y el artefacto guardado era el de antes del arreglo.
Ya están al día.
Y de paso se les fue una deuda que NO era de reproducibilidad sino de PORTABILIDAD: el
`x_cgo_sigaction` de sq y usql —código C de cgo, sin guarda de CPU en runtime— venía con
AVX2 horneado, de cuando el driver pisaba el `-mcpu=baseline`. Un binario así no falla al
construir ni al sellar: falla al EJECUTAR en una CPU sin AVX2, con SIGILL. Ahora los tres
salen baseline (cero ymm, cero zmm en esa función).
── El límite del libro, anotado en su encabezado ──────────────────────────────────────
`gocryptfs` acabó con CUATRO filas al MISMO hash: dos NO-DETERMINISMO y dos reproduce, y
las cuatro son ciertas. La clave (receta, hash) no puede distinguir antes y después de un
arreglo del FRAMEWORK, porque la fase generada no entra en `hash_inputs` — a propósito,
para no re-sellar 362 recetas cada vez que se toca un driver.
Corolario, que es lo caro: tras tocar un driver, lo sellado NO se rehace solo. Hay que
forzarlo receta por receta, y si nadie lo hace el store se queda con artefactos que ya
nadie construiría así.
`gocryptfs` era el último no-determinismo vigente del corpus. Ya REPRODUCE.
── La medición, que primero hice mal ──────────────────────────────────────────────────
Comparé los dos ficheros de `store/.divergen/` y salían 1.874.136 bytes distintos, con
`.text` entre ellos y funciones de OpenSSL cambiando de AVX2 a AVX-512. Era el par
EQUIVOCADO: `.divergen/<D>` es el artefacto VIEJO archivado, no una reconstrucción. El
par que define el veredicto es `<D>.r1` (1ª reconstrucción) contra el que queda en el
store (2ª).
Comparado bien, el no-determinismo son **7 bytes, todos en `.debug_str`**: los dígitos
de `go-build598798091` contra `go-build270591055`.
(Lo de AVX no era ruido: era DERIVA real contra el artefacto viejo, sellado antes de que
467a87cd pusiera `-mcpu=baseline` en el CC de cgo. Se trata aparte, abajo.)
── La causa ───────────────────────────────────────────────────────────────────────────
`-trimpath` reescribe rutas, pero cuál gana depende de dónde esté el work dir de Go:
dentro del módulo gana la reescritura del módulo y el componente ALEATORIO sobrevive;
fuera, Go reescribe el work dir entero a `/tmp/go-build` y el número desaparece. El
driver ponía `GOTMPDIR=/src/.gotmp`, que es justo dentro del módulo.
Con CGO off da igual —no hay fuentes C generadas, ninguna ruta del work dir entra en el
DWARF— y por eso las 15 recetas Go puras verificadas reproducen. Con cgo sí entra. Por
eso el cambio va acotado a `cgo = true`: las otras 358 no lo necesitan.
── Medido en los dos sentidos, con un módulo cgo de juguete y cachés limpias ─────────
· GOTMPDIR dentro ⇒ `go-build3806217352` en el binario; dos builds DIFIEREN
· GOTMPDIR fuera ⇒ ninguna cadena `go-build`; dos builds IDÉNTICOS
Y sobre gocryptfs de verdad: DERIVA primero (las dos reconstrucciones ya coincidían
entre sí y el guardado era el viejo), REPRODUCE en la pasada siguiente. En el binario
sólo queda `/tmp/go-build`, el marcador constante de trimpath.
── Va a `/cache` y no a `/tmp`, y se COMPRUEBA que exista ─────────────────────────────
El sandbox monta `--tmpfs /tmp` (~½ RAM) y un proyecto Go grande lo desborda: ésa era la
razón de poner esto en `/src`. `/cache` es un bind RW a disco (el de la caché de zig),
así que conserva el disco y queda fuera del módulo.
Y no se da por hecho que esté: con `--tmp-overlay /` la raíz es escribible, así que un
`mkdir -p /cache/gotmp` a ciegas TRIUNFARÍA creando el directorio en la capa tmpfs —en
RAM, el desbordamiento que se quería evitar, y en silencio. Si el bind falta, el build
muere diciéndolo.
`grep NO-DETERMINISMO` daba 3 y sugería tres problemas abiertos. Dos ya no existen:
kirigami y syntax-highlighting se arreglaron el mismo día y sus filas están clavadas
al hash VIEJO, que es justo lo que la clave (receta, hash) hace bien — pero quien
cuenta líneas no lo ve.
Se deja el veredicto medido intacto y se añade a qué hash fue superado. El único
no-determinismo VIGENTE al hash de hoy es `gocryptfs`, que no está en ningún perfil.
Los dos ya reconstruidos contra el syntax-highlighting arreglado.
REPRODUCEN 2 · DERIVA 0 · NO-DETERMINISMO 0.
Sellar de nuevo no demuestra nada por sí solo: lo que cierra el arreglo es que el
árbol reconstruido REPRODUZCA.
El arreglo movió el hash (4c1638a0… → 3db73c19…) y con él sus 4 dependientes
directos y 5 transitivos: kate, ktexteditor, plasma-desktop, plasma-workspace y
powerdevil. Comprobado aparte del log: para las seis existe
`store/<hash-de-hoy>-<nombre>`.
⚠ `kate` falló en el primer intento con «No space left on device» y esta vez NO era
el disco: `/mnt/vvv` tenía 15 G libres. El que se agotaba era un **tmpfs**, y un
tmpfs sin sitio significa RAM sin sitio — la caja tenía 855 MB disponibles, zram ya
con 6,3 G, otro agente compilando Rust y un QEMU encima. Al reintentar con 1,7 G
disponibles construyó en 242 s a la primera.
Que el mismo mensaje signifique dos cosas distintas —disco lleno o RAM llena— es
justo lo que hace caro el diagnóstico: hay que mirar QUÉ sistema de ficheros, no
sólo `df` del que uno tiene en la cabeza.