sandbox: el merge de deps cae en el store (mismo fs), no en la raíz — desbloquea kio

merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.

Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).

`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).

Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:06:10 -04:00
co-authored by Claude Opus 4.8
parent b9da01631d
commit 6a0dc40a05
2 changed files with 9 additions and 4 deletions
+8 -3
View File
@@ -209,13 +209,18 @@ impl Sandbox {
for d in &self.deps {
d.hash(&mut h);
}
// Junto al store para compartir filesystem (hardlinks no cruzan fs). El store suele estar en
// `<base>/store`; ubicamos el merge en `<base>/.dmerge/<id>`. Derivamos <base> del primer dep.
// DENTRO del store (el padre INMEDIATO de las deps), no en `<store>/../`: los hardlinks NO
// cruzan filesystems, y el store puede ser un mount aparte (en el worker es un VOLUMEN
// bind-montado: `/opt/hammer/store` en /dev/sdb, `/opt/hammer` en /dev/sda1). Con
// `.parent().parent()` el merge caía en la raíz ⇒ `cp -al` cross-device fallaba ⇒ fallback al
// apilado directo ⇒ overflow del lowerdir en kio (5315B > 4096B) y build imposible. El padre
// inmediato de un dep ES el store, siempre mismo fs que las deps. `.dmerge` (dot-prefix) no
// matchea `<hash>-<name>` ⇒ invisible para el store lookup y build-state, como `.times`.
let base = self
.deps
.first()
.and_then(|d| std::fs::canonicalize(d).ok())
.and_then(|d| d.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()))
.and_then(|d| d.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."));
let merged = base.join(".dmerge").join(format!("{:x}", h.finish()));
let done = merged.join(".done");
+1 -1
View File
@@ -68,7 +68,7 @@ else
continue
fi
# 2. RECOGE: store sellado worker→laptop (CAS, merge seguro).
if rsync -az -e "$SSH" "root@$ip:$REMOTE/store/" "$ROOT/store/" 2>&1 | tail -2; then
if rsync -az --exclude /.dmerge -e "$SSH" "root@$ip:$REMOTE/store/" "$ROOT/store/" 2>&1 | tail -2; then
echo " cosecha ✓"
else
echo " ⚠ cosecha falló — sigo"