Qué es
El sensor zombie_runs es una métrica de observabilidad añadida al sistema Supercontexto en sesión 25 (2026-04-25). Detecta handlers MCP del Bibliotecario que han abierto un bib_index_run pero han muerto sin cerrarlos (sin llamar a completeRun ni failRun), dejando el run en status='running' de forma indefinida.
Un run se considera zombie si lleva más de 30 minutos en status='running'. Ese umbral supera con holgura cualquier ejecución legítima (el handler más lento, bib_compute_communities con 300+ comunidades, completa en <60s tras el refactor).
Motivación
El incidente de sesión 25 reveló que bib_compute_communities excedía el budget de subrequests de CF Workers (~1 500/req vs límite 1 000) y moría antes de completeRun. El cliente recibía HTTP 200, las estadísticas del grafo parecían correctas (datos de un run previo), y sin un sensor específico el fallo era invisible. Se acumularon 160 runs zombie en producción.
→ Ver [[incident—20260425—bib-compute-communities-zombie]] para el diagnóstico completo.
Implementación
Query D1 (hot-cache.ts)
// functions/_lib/hot-cache.ts — dentro de fetchGraphHealth()
env.DB.prepare(
`SELECT COUNT(*) as n FROM bib_index_runs
WHERE status = 'running' AND started_at < datetime('now', '-30 minutes')`,
)
.first<{ n: number }>()
.catch(() => null)
El resultado se expone como zombie_runs: number en la interfaz GraphHealth y viaja al cliente a través del endpoint /api/supercontext/hot-cache (o el mecanismo de polling del Pulse).
Interfaz TypeScript
// functions/_lib/hot-cache.ts
export interface GraphHealth {
node_count: number;
edge_count: number;
community_count: number;
avg_cohesion_pct: number;
stale_count: number;
last_indexed: string | null;
zombie_runs: number; // ← nuevo en sesión 25
}
// src/components/biblioteca/PulsePanel.tsx
interface GraphHealth {
// ...campos anteriores...
zombie_runs?: number; // optional para compatibilidad hacia atrás
}
Alerta visual en PulsePanel
// src/components/biblioteca/PulsePanel.tsx
{(data.graph_health.zombie_runs ?? 0) > 0 && (
<>
<br />
<span style={{ color: '#ef4444', fontWeight: 600 }}>
⚠ {data.graph_health.zombie_runs} run{data.graph_health.zombie_runs === 1 ? '' : 's'} zombie
(status=running >30 min) — handler murió sin completar
</span>
</>
)}
La alerta aparece en rojo (#ef4444) directamente bajo las estadísticas de “Salud del grafo” en el Pulse Panel. Es visible de inmediato sin necesidad de navegar a ninguna vista secundaria.
Mecanismo complementario: reapZombieRuns
El sensor detecta zombies; el reaper los resuelve automáticamente en el siguiente run de cualquier tipo.
// functions/api/mcp/handlers/biblioteca.ts
async function reapZombieRuns(db: D1Database): Promise<void> {
await db.prepare(
`UPDATE bib_index_runs
SET status = 'failed',
errors = '["zombie reaper: status=running > 5min, asumido muerto"]',
completed_at = datetime('now')
WHERE status = 'running'
AND started_at < datetime('now', '-5 minutes')`
).run();
}
async function createRun(db: D1Database, runType: string, source?: string): Promise<number> {
await reapZombieRuns(db); // ← llamada preventiva antes de cada nuevo run
// ...INSERT nuevo run...
}
El umbral del reaper (5 min) es diferente al del sensor (30 min): el reaper es agresivo porque 5 min ya garantiza que ningún run legítimo está en curso; el sensor usa 30 min para evitar falsos positivos en picos de carga.
Umbrales y calibración
| Umbral | Componente | Razón |
|---|---|---|
| >5 min | reapZombieRuns | Ningún handler legítimo tarda más (el más lento: ~60s post-refactor) |
| >30 min | Sensor zombie_runs | Margen para evitar alertas falsas en picos; permite al reaper actuar primero |
Si se añaden handlers nuevos con tiempos de ejecución >5 min, revisar el umbral de reapZombieRuns para no marcar como failed runs legítimos en curso.
Integración con el flujo de runs
createRun()
└─ reapZombieRuns() # limpia zombies previos
└─ INSERT bib_index_runs # abre nuevo run
│
├─ [handler ejecuta]
│
├─ completeRun() # happy path → status='completed'
└─ failRun() # error path → status='failed'
fetchGraphHealth() [cada poll]
└─ COUNT running > 30min → zombie_runs
└─ PulsePanel: alerta roja si > 0
Runbook de respuesta
Si el sensor Pulse muestra zombie_runs > N:
- Verificar causa:
bib_list_runsfiltrandostatus=running. Verrun_typeystarted_atde los afectados. - Forzar reap manual si el reaper automático no ha actuado (p.ej. no ha habido ningún
createRunreciente):wrangler d1 execute crearack-workspace-db --remote \ --command "UPDATE bib_index_runs SET status='failed', errors='[\"manual reap\"]', completed_at=datetime('now') WHERE status='running' AND started_at < datetime('now', '-5 minutes')" - Investigar causa raíz: si el handler involucrado tiene loops con >100 statements D1 secuenciales, refactorizar con
db.batch(). - Verificar cron: confirmar que el cron externo (Hetzner Staging) no está lanzando el handler con una frecuencia superior a su tiempo de ejecución real.
→ Ver también [[runbook—biblioteca—manual-reindex]] para contexto completo de operaciones manuales.
Véase también
- [[incident—20260425—bib-compute-communities-zombie]]
- [[concept—biblioteca—supercontexto]]
- [[feature—workspace—pulse-tiempo-real]]
- [[feature—supercontexto—reconcile-d1-repo]]
- [[runbook—biblioteca—manual-reindex]]