Diez agentes lanzaron el mismo typecheck a la vez. La solución fue una carpeta.
Diecisiete agentes de código en el mismo checkout, diez procesos tsc a la vez, load average 37 y 87 MB de RAM libre. Un typecheck de noventa segundos tardó 7 min 36. Aquí está la medición, por qué la máquina no calculaba, y el pequeño bloqueo compartido que lo resolvió. Copiable en cualquier repositorio.
El 7 de septiembre, nuestra máquina de desarrollo dejó de responder como debía. No fue un cuelgue ni un bloqueo. Simplemente todo tardaba diez veces más, incluidas cosas que no tenían nada que ver con el código.
Los sospechosos evidentes eran todos falsos. El portátil no se estaba calentando: ningún throttling registrado, batería a 30,6 C. Ningún proceso desbocado se comía la CPU. No se había desplegado nada. Lo único inhabitual era que diecisiete CLI de agentes estaban vivos en el mismo repositorio, que aquí es un día de trabajo normal.
Esto es lo que pasaba de verdad, medido en vez de adivinado.
La medición
Máquina de 16 GB y 8 núcleos, encendida desde hacía cinco horas y media, diecisiete agentes trabajando:
| Lo que medimos | Valor |
|---|---|
| CLI de agentes vivos | 17 |
Procesos tsc --noEmit concurrentes, vistos en dos minutos | 3, luego 10 |
| Load average | 37 a 41 |
| RAM libre / compresor de memoria | 87 MB / 7,2 GB |
| Un typecheck de la app de escritorio, máquina saturada | 7 min 36 de reloj para 26 s de CPU |
| El mismo typecheck, máquina tranquila | 33 s |
La línea decisiva es la penúltima. Veintiséis segundos de CPU repartidos en siete minutos y medio son un diez por ciento de utilización. El typecheck no calculaba. Esperaba memoria.
Y uno de esos procesos fue matado por el sistema operativo a mitad de camino. Un tsc matado sale con un código distinto de cero y una salida vacía, lo que es indistinguible de un error de tipos real. Así que, además de ser lenta, la máquina producía veredictos en los que nadie podía confiar.
Nadie hizo nada mal
Esta es la parte en la que merece la pena detenerse, porque es la que hace que el fallo sea tan difícil de ver venir.
Cada uno de esos agentes seguía la regla. Cada uno había editado TypeScript. Cada uno tenía la consigna de verificar sus tipos antes de devolver el trabajo. Cada uno lanzó tsc --noEmit. Ninguno veía a los demás. No existe una pizarra común donde un agente escriba: estoy haciendo lo caro, esperad un momento.
Y luego se autoalimenta. El typecheck se ralentiza porque la máquina está saturada. El agente que lo vigila concluye que está atascado. Así que lo mata y lanza otro. Ese reflejo es correcto por sí solo y catastrófico en grupo, y es la misma familia de fallo que documentamos un mes antes, cuando algunos agentes dejaban detrás procesos de búsqueda atascados: Process Guard es la red que encuentra lo que sí ha arrancado, aquí impedimos el arranque.
Las tres respuestas que no tomamos
Lanzar menos agentes. Eso divide el síntoma y conserva el fallo. Dos typechecks simultáneos en una máquina cargada siguen siendo más lentos que uno solo, y reducir la flota es pagar el problema con aquello que hace que el trabajo vaya rápido.
Un único typecheck al final. Tentador, y equivocado por una razón que no tiene nada que ver con el rendimiento. Un error de tipos encontrado diez tickets después es huérfano: el agente que lo escribió está cerrado, su contexto se ha perdido, y una persona tiene que reabrir todo el tema para corregir una línea. No queríamos aplazar la verificación.
La compilación incremental. Probada y descartada. La ganancia es dudosa en modo --noEmit, y los procesos concurrentes corrompen el .tsbuildinfo compartido. Resuelve la mitad del problema empeorando la otra mitad.
Lo que hicimos en su lugar: una sola comprobación, compartida
La regla no es "verificar menos a menudo". Es un typecheck a la vez, por proyecto, para todo el mundo. Un script envoltorio sustituye N verificaciones por una sola, y responde a tres casos:
- Nada ha cambiado desde la última ejecución, así que devuelve su resultado.
- Ya hay una ejecución en curso, así que la espera y toma su resultado.
- Si no, coge el bloqueo y es el único
tscde la máquina.
Desde el punto de vista del agente no ha cambiado nada: escribe yarn typecheck, obtiene sus errores de tipos. Tampoco espera nunca más que antes, porque una ejecución detrás de la que espera es una ejecución que arrancó antes que la suya. La máquina paga una en vez de diez.
Esa es toda la idea. Lo interesante es que los dos mecanismos que necesita son mucho más pequeños de lo que uno imagina.
El bloqueo es una carpeta
Ni un archivo, ni una base de datos, ni un demonio. Una carpeta.
try {
mkdirSync(lockDir); // funciona: el bloqueo es nuestro
} catch (err) {
if (err.code === 'EEXIST') { /* otro lo tiene, esperamos */ }
}
mkdir crea la carpeta o falla con EEXIST, y lo hace de forma atómica en macOS, Windows y Linux, sin dependencia ni llamada nativa. Escribir un archivo y luego comprobar si existe serían dos operaciones, y dos operaciones son precisamente el hueco por el que se cuela un segundo agente.
Dentro de la carpeta dejamos un owner.json con el pid, el nombre de host y la hora de inicio. Ese archivo sirve para el diagnóstico y para detectar un bloqueo muerto. Nunca es él quien excluye.
Un bloqueo muerto se recupera automáticamente en dos casos: el proceso propietario ha desaparecido (comprobado solo si el nombre de host coincide, porque un pid no significa nada de una máquina a otra), o el bloqueo tiene más de quince minutos.
Una trampa que nos costó un bug. Entre el mkdir y la escritura de owner.json existe una ventana en la que el propietario no se puede leer. Declarar el bloqueo muerto en esa ventana es robárselo a quien acaba de cogerlo, exactamente la carrera que el archivo existe para evitar. Así que cuando no hay propietario legible, juzgamos por la edad de la carpeta, no por el archivo ausente.
La huella es una fecha y un recuento
El caso 1 necesita saber si algo ha cambiado desde la última ejecución. La respuesta evidente sería hacer un hash de los archivos fuente. No lo hacemos.
La huella es <mtime más reciente>:<número de archivos> sobre las raíces deducidas del campo include del tsconfig, más el propio tsconfig.
Con 2300 archivos, leer cada byte cuesta más que la verificación que ahorra. La fecha sola no ve un borrado. El recuento solo no ve una edición. Juntos cubren los dos. El falso negativo asumido son dos modificaciones dentro del mismo milisegundo que dejan el recuento idéntico, y el peor caso es un resultado de caché caducado de unos segundos, nunca un error de tipos silencioso, porque la verificación bloqueante sigue siendo la del build.
Una regla escrita no bastó, así que añadimos un hook
La consigna estaba en AGENTS.md desde el primer día: nunca tsc directo, siempre el script compartido. No bastó, y merece la pena decir honestamente por qué.
Verificar los tipos después de una edición es un reflejo profundamente arraigado. Bajo presión, un agente escribe npx tsc --noEmit sin releer las consignas. Y basta con un solo agente que se salte la regla para recrear la jauría que el bloqueo existe para evitar. Una consigna se discute. Un hook no.
Así que enchufamos un hook PreToolUse en la herramienta Bash, que rechaza un tsc directo y nombra el comando correcto en el rechazo:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
El hook lee la llamada a la herramienta como JSON en stdin, sale con 2 y el motivo en stderr para rechazar, y con 0 para dejar pasar. Dos detalles marcan la diferencia entre un hook útil y uno molesto.
Reconoce tsc en posición de comando, no en cualquier parte de la cadena. Buscar las tres letras en todas partes rechazaría también grep -rn tsc AGENTS.md. El patrón exige por tanto tsc al principio de línea o después de ;, &&, ||, | o (, eventualmente precedido de un lanzador de paquetes y una ruta. También deja pasar tsc --version: no hay razón para rechazar una opción informativa.
Rechaza una segunda cosa que no habíamos anticipado. Vimos a un agente esperar detrás del bloqueo, decidir al cabo de un rato que debía de estar muerto, y borrar la carpeta de bloqueo para desatascarse. Eso arranca un segundo proceso pesado junto al vivo, es decir la elusión perfecta de todo lo que el bloqueo protege. Por eso borrar la carpeta de bloqueo o de caché también se rechaza, con la explicación de que un bloqueo muerto se recupera solo.
Ese segundo rechazo nunca lo habríamos escrito por adelantado. Sale de observar lo que los agentes hacen de verdad cuando están bloqueados, que es mejor fuente de barreras que imaginar lo que podrían hacer.
Dónde se para
El hook es específico de Claude Code. Los demás CLI de agentes de la flota solo ven la regla escrita. Es un agujero conocido y lo asumimos: una barrera que cubre la mayoría de la flota vale más que ninguna barrera mientras esperamos un estándar de hooks que lean todos los CLI.
El script compartido, en cambio, es neutro respecto al proveedor, porque es un comando como otro cualquiera. Cualquier CLI capaz de lanzar yarn typecheck se beneficia del bloqueo, se le obligue o no.
Qué hay que quedarse de esto
El typecheck era nuestro caso más ruidoso, no un caso especial. El patrón se aplica a cualquier comando caro, idempotente en una ventana corta, y lanzado por todos los agentes por la misma buena razón: instalar las dependencias, jugar la suite de tests completa, construir para producción, arrancar un servidor de desarrollo en un puerto fijo.
Tres preguntas, en este orden, y tienes todo el diseño:
- ¿Puedo reutilizar un resultado reciente?
- ¿Puedo unirme a la ejecución que ya está en curso?
- Si no, ¿soy yo el que la arranca, solo?
Si varios agentes comparten tu máquina, lo que merece la pena medir no es cuántos están corriendo. Es cuántos de ellos arrancan el mismo comando en el mismo minuto. Ese número es lo que tu máquina siente de verdad, y mientras no lo mires, culparás al calor.
Si quieres la visión de conjunto sobre cómo hacemos correr varios agentes en un mismo repositorio sin que se pisen, está en Ejecutar agentes de código en paralelo, y la red de seguridad para los procesos que sí han arrancado es Process Guard. Tanto el script compartido como el hook viven en el repositorio de AgentsRoom, es decir ahí donde esos diecisiete agentes estaban trabajando aquella tarde.
Descargar AgentsRoom
Ejecuta todos tus agentes de IA, en todos tus proyectos, desde una sola ventana.
App complementaria: supervisa tus agentes en movimiento
Usa Claude, Codex, Antigravity CLI u otro proveedor de IA.
Envía bugs y peticiones directamente a tu backlog público.
Un vistazo a AgentsRoom en acción.
Seguir leyendo
Cómo Escalar Agentes de Codificación AI en un Equipo de Desarrollo
Un desarrollador con un agente de codificación es una historia de productividad. Cinco desarrolladores con veinte agentes es un problema de coordinación. Aquí está lo que se rompe primero cuando un equipo se expande, y la configuración que se mantiene: archivos de contexto comprometidos, clara propiedad de archivos, revisión por radio de explosión, y costos que realmente puedes ver.
Leer el artículo30 eventos de hook se disparan en una sesión de Claude Code. Solo 3 pueden responder.
La lista completa de eventos de hook de Claude Code, cuándo se dispara cada uno, cuáles son los 15 que pueden bloquear y la regla de stdout que se traga en silencio la salida de la mayoría de los hooks. Una referencia de campo construida ejecutando hooks en producción a lo largo de miles de sesiones de agentes.
Leer el artículoMi entrenador de running es un repositorio Git y un agente Claude
Termino la sesión, mi reloj se sincroniza y tres minutos más tarde el análisis está escrito en mi repositorio, la semana ha sido reajustada y mi entrenador ha dejado un comentario bajo la actividad de Strava. Ninguna app desarrollada, ningún servidor escrito, ninguna factura de API por token: una suscripción a Claude, AgentsRoom y archivos Markdown. Aquí está el montaje completo, reproducible.
Leer el artículo