Why NextWhy Next
Volver a la lista
💻 Desarrollo11 min de lectura

Lo que pasó cuando solté varios agentes de IA en un mismo repo

Pensé que lanzar sesiones de agente en paralelo multiplicaría la productividad. En la realidad, en una sola semana estallaron accidentes en cadena: secuestro de ramas, commits montados sobre la rama de otro, commits huérfanos rescatados con el reflog y hasta la doble implementación de la misma funcionalidad. Una tipología de los accidentes que crean los agentes paralelos que comparten un checkout, y las reglas que levanté tras sufrirlos.

Lo esencial

El verdadero enemigo de las sesiones de agente en paralelo no es la capacidad del modelo, sino el directorio de trabajo compartido. HEAD es la variable global de todos: el cambio de rama de una sesión monta el commit de otra en el sitio equivocado. Los cuatro tipos de accidente vividos en una semana con sus recuperaciones, y las tres líneas de defensa: aislamiento con worktree, reverificar la rama justo antes del commit y reducir el alcance de la puerta de verificación.

En esta página

Trabajando con agentes de IA, la ambición llega sola. Mientras una sesión arregla un bug, otra puede ir refactorizando, y una tercera investigando un issue, ¿no? Si puedo lanzar varios modelos, la productividad crecerá en la misma proporción.

Yo también empecé así. Y en una semana lo aprendí: el verdadero enemigo de los agentes paralelos no es la capacidad del modelo, sino el directorio de trabajo que comparten.

HEAD es una variable global

La causa se resume en una frase. Cuando varias sesiones comparten un único checkout de git, el estado "rama actual" se convierte en la variable global de todos.

Si imaginas a dos personas trabajando a la vez en el mismo ordenador, lo raro se ve enseguida; pero al lanzar los agentes no lo pensé. Con una sesión en cada pestaña del terminal, parecen aisladas entre sí. Pero el sistema de archivos es uno, y HEAD también es uno. En el momento en que una sesión hace git checkout, el suelo se mueve bajo los pies de todas las demás.

Los accidentes que estallaron aquella semana tenían tipos bien nítidos.

Secuestro de rama. Mientras la sesión A trabajaba en su rama de tema, la sesión B cambió de rama para hacer lo suyo. A hizo commit sin enterarse, y el commit quedó montado sobre la rama de B. También hubo caso en la dirección contraria: en el momento en que A iba a hacer commit, la rama había cambiado a develop, y solo el hook que bloquea los commits directos a ramas protegidas evitó el accidente. Sin ese hook, habría entrado tal cual.

Commits huérfanos. La rama de tema de la sesión A la borró la sesión B durante una limpieza. Los commits de A quedaron huérfanos, sin pertenecer a ninguna rama, y hubo que rebuscar en el reflog, encontrar el hash del commit y recuperarlo con cherry-pick. Menos mal que se rescató: si el reflog hubiera expirado o no lo hubiera encontrado, el trabajo entero se habría evaporado.

Contaminación del staging. En el momento en que la sesión A creaba su commit, el borrado de archivos que la sesión B había dejado en staging estaba subido en el área de staging junto a lo demás. De haber hecho commit tal cual, el borrado de B se habría colado en el commit de A. Se filtró porque, repasando el diff, apareció un cambio desconocido; si el agente no hubiera mirado el diff justo antes del commit, nadie se habría enterado.

Doble implementación. El tipo más descorazonador. Dos sesiones, sin saber de la existencia de la otra, implementaron cada una la misma funcionalidad. Las dos la hicieron bien y con diligencia, y una hubo que tirarla entera. El tiempo ahorrado con la paralelización, devuelto tal cual.

Hasta en la puerta de verificación se pisan entre ellas

El problema no eran solo los archivos y las ramas. Nuestro harness tiene una puerta que, antes de que una sesión termine, ejecuta el análisis estático y los tests de todo el repo para que no se pueda cerrar el trabajo en estado roto. En una sesión en solitario es un mecanismo excelente.

Con sesiones en paralelo se convirtió en una trampa en la que unas les ponen la zancadilla a otras. La sesión A solo había tocado documentación, pero su puerta fallaba por el error de compilación de un archivo que la sesión B estaba arreglando en ese momento. A gastaba turnos en demostrar que "este cambio no es mío" y, en el peor día, esperó 30 minutos a que B terminara de ordenar lo suyo. El peor incidente fue cuando una sesión, para pasar la puerta, modificó directamente el código en el que otra sesión estaba trabajando. La puerta se convirtió justamente en el incentivo para meter mano en el trabajo ajeno.

La comprobación en sí no está mal. Lo que se volvió un error fue que su alcance fuera "el repo entero" en el momento en que el repo dejó de ser de una sola persona.

Tres líneas de defensa

Después de sufrir los accidentes tipo por tipo, levanté una defensa de tres capas.

  1. Aislamiento con worktree como valor por defecto. Si con git worktree le das a cada sesión un directorio de trabajo independiente, HEAD deja de ser una variable compartida. El secuestro de ramas, los commits huérfanos y la contaminación del staging desaparecen de raíz. Tres de los cuatro tipos de accidente de arriba se esfuman con esta sola jugada. Pero no es gratis. En un monorepo hay que repetir la instalación de dependencias y la generación de código en cada worktree, y en repos que usan herramientas que se llevan mal con los worktree, como git-crypt, es difícil imponerlo. Nosotros mismos mantenemos un repo con checkout compartido por esta restricción, y por eso hacen falta las otras dos líneas de defensa.
  2. Reverificar la rama justo antes del commit. Cuando la sesión empieza a trabajar, deja registrado "yo trabajo en esta rama", y justo antes del commit lo contrasta con el HEAD actual. Si difieren, detiene el commit y primero averigua la situación. La regla es tan simple que casi da vergüenza, pero todos los accidentes de secuestro de rama nacieron de lo mismo: "el HEAD del momento del commit no era el HEAD que yo conocía". Una persona lo notaría porque el nombre de la rama aparece en el prompt, pero un agente no lo comprueba si no se lo mandas explícitamente.
  3. Acotar la puerta de verificación a mis cambios. El alcance que inspecciona la puerta de cierre de sesión se estrecha: no el repo entero, sino los archivos que esa sesión modificó de verdad. Desaparece tanto el que mi cierre quede bloqueado por el WIP ajeno como el incentivo de tocar el código de otro para pasar la puerta. La salud del repo completo la vuelve a mirar de todos modos la CI sobre el estado ya commiteado. No hacía falta que la puerta de sesión mirara también el conjunto.

A eso le sumé una regla operativa. Antes de lanzar una sesión, repaso las ramas y PR abiertos para ver si hay solapamiento de alcance. La doble implementación no es un problema que git pueda bloquear, sino un problema de despacho de tareas, así que no quedaba otra que atajarlo con hábito y no con herramientas.

Los problemas de concurrencia no eran solo cosa de las bases de datos

Puesto en orden, el cuadro resulta familiar. Recurso compartido, acceso concurrente sin locks, condiciones de carrera y niveles de aislamiento. El mismo problema que llevamos décadas aprendiendo con las bases de datos y el código multihilo, reproducido sobre el directorio de trabajo.

Trabajando con un solo agente, este problema no existe. Cuando persona y agente se turnan, la persona hace implícitamente de coordinadora. El problema empieza en el momento en que los agentes son varios y la persona suelta la coordinación. A partir de ahí, el directorio de trabajo es un recurso compartido que necesita control de concurrencia y, si lo haces girar sin aislamiento, pierdes datos sin falta, igual que les pasaba a las bases de datos.

No estoy diciendo que haya que renunciar a los agentes paralelos. Sigo lanzando varias sesiones cada día. Lo que cambió es una sola cosa: ya no las meto a todas en la misma habitación.

Artículos relacionados

0 comentarios