D1 bind-param overflow silencioso
Cuándo
Descubierto el 2026-04-21 durante la Sesión 3 del sprint Supercontexto (Fase 1 Backbone). Duración del período afectado indeterminada pero el grafo llevaba varios días estancado: el cron ejecutaba desde v1.0.58 (2026-04-17) reportando éxito sin haber insertado ningún nodo real.
Síntomas
El script scripts/bib_ast.py recibía HTTP 200 del handler bib_index_ast y terminaba con exit 0. El log del cron mostraba [OK] en todas las apps. Sin embargo, el grafo permanecía congelado: ningún nodo nuevo, ninguna arista actualizada. Apps con mayor volumen (core 292 qualified_names, terminal 596, monitoring 545) no tenían representación real en D1. El Widget Salud reflejaba datos obsoletos sin ninguna alarma.
Causa raíz
D1 (SQLite en Workers) impone límite de ~100 bind parameters por statement. El handler bib_index_ast construía cláusulas WHERE qualified_name IN (?, ?, …) con un placeholder por cada elemento del payload. Para apps con >100 qualified_names, el driver devolvía D1_ERROR: too many SQL variables, pero el handler capturaba la excepción, la incluía en errors[] del body JSON y retornaba HTTP 200. El cliente (bib_ast.py) no inspeccionaba el body: interpretaba el 200 como éxito total. Error enterrado en la respuesta.
Anti-patrón: HTTP 200 ≠ éxito. Status code correcto no garantiza que la operación de negocio se completó, especialmente cuando el handler serializa errores internos como payload en lugar de propagarlos al status HTTP.
Fix aplicado
af73a3e (workspace functions/api/mcp/handlers/biblioteca.ts): dos constantes que separan presupuestos de bind:
const CHUNK_IN = 90; // placeholders máximos en una cláusula IN
const CHUNK_BATCH = 500; // statements por db.batch()
Cláusulas IN para pre-fetch se trocean en ventanas de 90; cada una con su presupuesto de binds. Upserts se agrupan en lotes de 500 statements vía db.batch(), que asigna presupuesto independiente a cada statement. Reduce subrequests de O(n×3) a O(n/500) y elimina el error.
2794b9f (CreaRack-Pro scripts/bib_ast.py): el script ahora desenvuelve body JSON, detecta errors[] y falla con exit ≠ 0 si nodes_created + nodes_updated == 0 con payload no vacío. Cualquier 200 que oculte error de D1 se convierte en fallo observable.
Lecciones → feedback memory
- HTTP 200 ≠ éxito (Regla 15 CLAUDE.md): verificar siempre contenido del body, especialmente en APIs que serializan errores internos. 2xx sin inspección del payload es confianza falsa.
- CI/cron log ≠ operación completada: exit 0 del script era técnicamente correcto pero semánticamente incorrecto. Contrato de un cron de indexación debe ser: exit 0 solo si al menos un nodo creado o actualizado con payload no vacío.
- Apps grandes rompen primero en silencio: límites de plataforma (bind params, subrequest count) se alcanzan primero en módulos voluminosos. Tests de integración deben incluir fixtures con volumen realista.
Preventivos futuros
- Alertas sobre
nodes_created + nodes_updated == 0: si cron termina con payload enviado pero cero nodos, disparar alerta Pulse inmediatamente. - Smoke test de volumen en CI: ejecutar
bib_index_astcon payload de >100 qualified_names sintéticos en test D1 y verificar inserciones. - Handler debe retornar 4xx/500 ante errores fatales de D1: nunca absorber excepciones de infraestructura en
errors[]con 200. Contrato HTTP debe reflejar resultado real. - Monitorizar tamaño del grafo (node count por app) como métrica en Widget Salud: meseta sostenida de varios días = señal temprana de cron fallando silenciosamente.
Véase también
- [[decision—20260215—d1-biblioteca]]
- [[concept—biblioteca—supercontexto]]