Queríamos que Claude Code se manejara solo. Que iniciara una tarea, la repitiera de forma autónoma durante decenas de iteraciones, gestionara su propio contexto y siguiera funcionando durante horas sin que nadie lo vigilara. Un concepto simple. Tres semanas de infierno de infraestructura para que funcionara de verdad.
Esta es la cronología completa de cada bug, condición de carrera y falla de arquitectura con que nos topamos al construir los sistemas autónomos de loops y agentes de SynaBun. Cada arreglo destapó un problema peor debajo. No exageramos.
La propuesta: loops autónomos
La idea era sencilla. Le das a Claude una tarea repetitiva (interactuar con comunidades de IA en Twitter, monitorear un deploy, procesar un lote de datos) y él la ejecuta en un loop. Cada iteración recibe un contexto limpio, el hook Stop impulsa el siguiente ciclo y todo se sostiene solo sin que toques el teclado.
Construimos la primera versión en un día. Una herramienta MCP con las acciones start, stop y status. Un archivo de estado en disco. El hook Stop lee el estado, incrementa el contador de iteraciones, inyecta el prompt de la tarea y bloquea la respuesta de Claude para forzar el siguiente ciclo. Un diseño limpio. Funcionó en la primera prueba.
Luego intentamos usarlo en serio.
Semana uno: todo muere después de 3 iteraciones
El loop arrancaba bien. Iteración 1, genial. Iteración 2, bien. Iteración 3: silencio. Claude simplemente dejaba de responder. Sin error, sin caída, sin mensaje de tiempo agotado. Solo... nada.
Causa raíz: fallaba la detección del prompt. El controlador del loop, del lado del servidor, tenía que detectar cuándo Claude estaba listo para recibir entrada, vigilando la aparición del carácter de prompt > en la salida de la terminal. La regex que usábamos para detectar ese prompt tenía huecos. La salida de terminal de Claude Code viene envuelta en secuencias de escape ANSI (códigos de color, posicionamiento del cursor, indicadores de modo DEC) y nuestra regex solo reconocía prompts en ASCII limpio. Cualquier secuencia ANSI entre el salto de línea y el carácter > rompía la detección por completo.
Pero era peor todavía. El búfer de salida era de 2KB. Las respuestas de Claude lo superaban con frecuencia, así que el carácter de prompt quedaba fuera del límite del búfer. El controlador buscaba un prompt que ya se había descartado de la memoria.
Arreglo: Ampliamos el búfer de 2KB a 8KB. Reescribimos la regex de detección del prompt para que eliminara las secuencias ANSI antes de comparar. Agregamos un campo stopReason al estado del loop para poder distinguir entre límites de iteraciones, límites de tiempo y fallas en la detección del prompt. Pero apenas estábamos empezando.
El problema de los ID de sesión huérfanos
Cuando el controlador del loop envía /clear entre iteraciones para darle a Claude un contexto limpio, Claude Code genera un nuevo ID de sesión. Nuestro archivo de estado del loop se identificaba por el ID de sesión original. Después de /clear, el nombre del archivo de estado ya no coincidía con la sesión activa. El hook Stop buscaba el archivo de estado por ID de sesión, no encontraba nada y concluía que no había ningún loop activo.
Técnicamente, el loop seguía vivo en disco. Solo que se había vuelto invisible para todos los hooks de la cadena.
Intento de arreglo 1: Agregar un escaneo de respaldo. Si falla la búsqueda directa por ID de sesión, se recorren todos los archivos de estado de loops en busca de alguno con active: true. Funcionaba en escenarios de un solo usuario, pero era catastróficamente incorrecto en contextos paralelos. Cuando las pruebas se ejecutaban de forma concurrente, el hook Stop de una prueba le robaba el archivo de loop a otra a través del escaneo de respaldo.
Intento de arreglo 2: Quitar por completo el escaneo de respaldo del hook Stop. En su lugar, que el hook prompt-submit se encargue de renombrar el archivo. Cuando llega el marcador [SynaBun Loop] después de /clear, el hook prompt-submit renombra el archivo de estado del ID de sesión anterior al nuevo. Para cuando se dispara el hook Stop, el archivo ya está en la ruta correcta.
Intento de arreglo 3 (sí, hubo un tercero): Agregar una verificación de antigüedad y statSync al escaneo de respaldo de prompt-submit. Solo se aceptan archivos recientes (de menos de 5 minutos). Los archivos obsoletos de loops terminados o caídos se ignoran en lugar de secuestrar la siguiente sesión.
El límite de tiempo que mataba los loops de navegador
Fijamos MAX_MINUTES en 60 y DEFAULT_MINUTES en 30. Razonable para tareas rápidas de automatización. Totalmente insuficiente para loops de automatización de navegador que tienen que navegar por páginas, esperar a que carguen, obtener instantáneas del árbol de accesibilidad, hacer clic en elementos y escribir en formularios. Una sola iteración de interacción en Twitter podía tardar 3-4 minutos. Con los 30 minutos predeterminados, alcanzas unas 8 iteraciones antes de que el límite de tiempo mate el loop.
Arreglo: MAX_MINUTES subió a 480. DEFAULT_MINUTES subió a 60. Además, la ventana del escaneo de respaldo por ID de sesión dejó de ser fija, de 5 minutos, y pasó a usar el valor maxMinutes del propio loop, para que los loops de larga duración no dejen huérfanos sus propios archivos de estado.
La trampa del muro de inicio de sesión
Los loops de navegador se toparon con una página de inicio de sesión de Twitter. El agente, siguiendo sus reglas de autonomía ("si algo falla, prueba un enfoque alternativo, no te detengas a preguntar"), abandonó por completo el navegador de SynaBun y recurrió a WebSearch y WebFetch. Empezó a hacer solicitudes HTTP directas a Twitter en lugar de usar el navegador con el que debía interactuar. Los resultados eran inútiles, pero el loop seguía iterando y quemando créditos de la API en basura.
Causa raíz: Las instrucciones de autonomía eran demasiado amplias. "Prueba una alternativa" es lo correcto ante fallas técnicas: un error 500, un tiempo de espera agotado, un elemento del DOM que no aparece. Pero un muro de inicio de sesión no es una falla técnica. Es un bloqueo que requiere acción humana. Solo el usuario puede iniciar sesión, resolver un CAPTCHA o completar la 2FA.
Arreglo: Dividir la regla de autonomía en dos categorías. Fallas técnicas: reintenta, prueba alternativas, sigue adelante. Bloqueos que requieren acción humana (páginas de inicio de sesión, CAPTCHAs, 2FA, muros de pago): DETENTE de inmediato, informa al usuario y ESPERA a que se resuelva. No recurras a WebSearch. El panel del navegador es interactivo: el usuario puede encargarse, pero solo si el agente hace una pausa lo bastante larga para dejarlo.
Semana dos: deterioro mental
El loop corrió durante 50 iteraciones. En las primeras 15, la calidad fue excelente. Interacciones variadas, buen tono, sin objetivos duplicados. Luego, cerca de la iteración 20, las cosas empezaron a desviarse. Estrategias repetitivas. Las mismas frases de apertura. Para la iteración 30, ya publicaba dos veces en cuentas con las que ya había interactuado. Para la iteración 40, había olvidado por completo las reglas de tono y usaba guiones en cada respuesta: justo el formato que habíamos prohibido de forma explícita.
Causa raíz: amnesia por compactación. Claude Code tiene una ventana de contexto. Cuando se llena, el sistema compacta la conversación: resume los mensajes antiguos para liberar espacio. Después de la compactación, nuestro hook Stop volvía a inyectar el prompt de la tarea. Pero ese prompt era estático. Contenía las instrucciones originales y nada más. Todo el contexto dinámico que Claude había acumulado (con qué cuentas había interactuado, qué estrategias funcionaban, qué patrones había descubierto) había desaparecido. Compactado en un resumen que no incluía nada de eso.
No era un bug en el sentido tradicional. Era un vacío de arquitectura. El sistema de loops estaba diseñado solo para la autonomía dentro de una sesión. Tenía cero resiliencia frente a la compactación. Sin integración con la memoria, sin seguimiento del progreso, sin ningún estado que sobreviviera al reinicio del contexto.
El sistema contra el deterioro mental
Construimos una solución de tres capas:
Capa 1: diario del loop. Una nueva acción update en la herramienta MCP del loop le permite a Claude escribir un breve resumen después de cada iteración. Esos resúmenes se guardan en el archivo de estado como un diario rotativo (las últimas 10 entradas, de 200 caracteres cada una). El hook Stop inyecta las últimas 3 entradas del diario en el prompt de cada iteración, junto con un resumen continuo del progreso. Incluso después de la compactación, Claude recibe una vista condensada del historial reciente.
Capa 2: memoria obligatoria. Cada 5 iteraciones, el hook Stop bloquea el loop hasta que Claude llama a remember para guardar el progreso acumulado en la memoria vectorial de SynaBun. No es opcional. El hook se niega, literalmente, a continuar el loop hasta que se haga la llamada a la memoria. Si Claude ignora la instrucción, el hook reintenta con urgencia creciente. Después, el hook post-remember actualiza el estado del loop para registrar cuándo se guardó por última vez en la memoria.
Capa 3: puente de compactación. El hook pre-compact captura el estado del loop activo antes de que empiece la compactación. El hook session-start busca ese estado capturado cuando la compactación termina e inyecta un bloque ACTIVE LOOP RECOVERY con la tarea completa, el diario, el resumen del progreso y una instrucción para recuperar con recall las memorias de progreso guardadas. A eso se suma un anclaje de estilo en puntos de control clave (iteraciones 1, 10, 20, 30, 40) para que Claude vuelva a las reglas originales de tono y formato.
También aprendimos una lección dolorosa sobre los hooks: los tres hooks de la cadena tienen que conocer todos los modos de sesión. El hook session-start, el hook prompt-submit y el hook stop inyectan instrucciones cada uno por su cuenta. Cuando hicimos que solo uno de ellos reconociera los loops, los otros dos siguieron inyectando contexto contradictorio. Cada nuevo "modo" de operación tiene que conectarse a través de los tres hooks o las cosas se rompen de formas sutiles y desesperantes.
La condición de carrera de /clear
Este fue invisible durante dos semanas. El controlador del loop envía /clear entre iteraciones, espera el prompt y luego envía el mensaje de la siguiente iteración. Pero el controlador consulta el estado en un intervalo de 2 segundos. Para cuando consulta, Claude ya terminó de responder e imprimió el prompt >. El prompt ya está en el búfer.
Paso 1 del controlador: vaciar el búfer. Después, esperar una salida nueva que contenga el prompt.
¿Ves el problema? El prompt ya estaba en el búfer. Vaciarlo primero significaba esperar un prompt nuevo que nunca iba a llegar. El controlador esperaba 75 segundos y luego declaraba un fallo de detección del prompt. Tras 10 fallos consecutivos, mataba el loop.
Solución: Buscar el prompt en el búfer existente ANTES de vaciarlo. Si Claude ya está en el prompt, se omite la espera por completo y se pasa a /clear. El vaciado del búfer posterior al envío de /clear quedó intacto: ese sí necesita esperar la salida posterior al clear.
La fuga de loops entre sesiones
Tres rutas de código distintas, en el controlador del loop y en los escaneos de respaldo de los hooks, recorrían TODOS los archivos de estado de loops del directorio de datos sin comprobar terminalSessionId. Si tenías dos sesiones de terminal abiertas, una ejecutando un loop y otra con trabajo interactivo, el estado del loop de una sesión se filtraba a la otra. El hook Stop de la sesión interactiva tomaba el archivo de estado del loop y empezaba a inyectar prompts de iteración en una conversación que no tenía nada que ver con el loop.
Solución: Añadimos controles de propiedad a las tres rutas de código. El controlador del loop solo se conecta a los loops cuyo terminalSessionId coincide con la terminal actual. El hook prompt-submit conserva terminalSessionId al renombrar. El escaneo de respaldo comprueba la propiedad antes de apropiarse de un archivo de estado.
Semana tres: los agentes se suman a la fiesta
Los loops por fin eran estables. Era hora de añadir agentes: procesos de Claude Code en segundo plano que se lanzan desde el Estudio de automatizaciones de la Neural Interface. Configuras una tarea, eliges un modelo, activas las herramientas de SynaBun y lo lanzas. El agente se ejecuta de forma autónoma en segundo plano.
Nada funcionó.
El bug de sincronización del DOM
Los agentes lanzados desde el Estudio de automatizaciones siempre tenían synabun=false y browser=none. Sin herramientas MCP, sin navegador. Ningún agente servía para lo que estaba pensado.
Causa raíz: En confirmLaunch(), la función llamaba a closeLaunchDialog(), que llama a .remove() sobre el elemento del diálogo, ANTES de leer los valores del DOM. Cinco llamadas a document.getElementById() leían de un árbol DOM destruido. withSynabun devolvía undefined (el elemento ya no existía), que se convertía en false. El selector de submodo devolvía null y recurría al modo "single". Las iteraciones quedaban en 1 por defecto. Los minutos máximos quedaban en 30 por defecto.
Todas y cada una de las opciones de configuración se perdían en silencio.
Solución: Mover las 5 lecturas del DOM antes de closeLaunchDialog(). Capturar los valores mientras el diálogo existe y luego cerrarlo.
El secuestro de la secuencia de arranque de CLAUDE.md
Con el arreglo del DOM aplicado, los agentes por fin se lanzaban con las herramientas de SynaBun. Pero dedicaban sus primeros 4 turnos a ejecutar la secuencia de arranque de CLAUDE.md (saludar al usuario, recuperar las sesiones recientes, recuperar las preferencias de estilo de comunicación) en lugar de hacer la tarea. El agente se había lanzado como una automatización en segundo plano. No hay ningún usuario al que saludar. No hay preferencias de comunicación que cargar. Estaba quemando costosos turnos de API en ceremonias.
Después generaba texto como "Voy a navegar a Twitter..." sin llamar realmente a ninguna herramienta. En modo --print, una respuesta de solo texto (sin bloques tool_use) termina la sesión. El agente decía lo que iba a hacer y moría antes de hacerlo.
Solución: Añadimos un prompt de sistema defaultAgentPrompt que anula el comportamiento de arranque de CLAUDE.md. Instrucciones explícitas: omitir el saludo, omitir las recuperaciones, ir directo a la tarea. Encadenar ToolSearch con llamadas reales a herramientas en la misma respuesta; nunca generar texto sin una llamada a herramienta. Subimos maxTurns de 25 a 200 para dar margen a la automatización del navegador.
La continuidad de sesión provocaba compactación
Los agentes usaban --resume para mantener el contexto entre iteraciones, con un SESSION_CONTINUITY_WINDOW de 5 (reanudar la misma sesión durante 5 iteraciones consecutivas). Eso significaba que el contexto se acumulaba durante 5 iteraciones antes de reiniciarse. En los agentes de automatización del navegador, que hacen 10-15 llamadas a herramientas por iteración, esto llenaba rápido la ventana de contexto. Entraba la compactación. El agente se colgaba.
Solución: Sesión nueva en CADA iteración. Nada de --resume, nunca. Se genera un nuevo randomUUID() para cada iteración. Cada iteración recibe la tarea completa reinyectada, más las entradas del diario de las iteraciones anteriores. Hay un poco más de sobrecarga de ToolSearch por iteración, pero el riesgo de compactación desaparece por completo. El diario aporta continuidad suficiente.
Iteraciones sin llamadas a herramientas
En modo --print, las respuestas de solo texto terminan la sesión con código de salida 0. El controlador de agentes veía el código de salida 0 (éxito) y pasaba a la siguiente iteración. Pero la iteración no había logrado nada: el modelo hablaba de lo que haría sin hacerlo.
Solución: Contar las llamadas a herramientas por iteración. Si una iteración produce 0 llamadas a herramientas con código de salida 0, se reintenta una vez con un prompt contundente: "DEBES llamar a herramientas. No describas lo que vas a hacer. Hazlo."
La guerra de prioridades de la compactación
Incluso con sesiones nuevas en cada iteración, los loops normales (impulsados por el hook Stop) seguían teniendo problemas de compactación. El hook Stop tenía un sistema de prioridades: CHECK 1 era la compactación (máxima prioridad) y CHECK 1.5 era la continuación del loop. Cuando había un loop activo y una compactación pendiente, la comprobación de compactación ganaba siempre. El loop se detenía mientras corría la compactación y a veces no se recuperaba nunca.
Solución: Añadimos un PRE-CHECK que se ejecuta antes que todas las demás comprobaciones. Un escaneo rápido de loops activos. Si hay un loop activo, se omite por completo la comprobación de compactación y se limpian las marcas pending-compact. El loop tiene prioridad sobre la compactación porque usar una sesión nueva por iteración ya evita que el contexto se infle: la compactación no hace falta.
Lo que aprendimos
Construir sistemas de IA autónomos no es un problema de ingeniería de software. Es un problema de gestión de estado sobre un sustrato poco fiable. Claude Code es una caja negra con una interfaz de terminal. No puedes inspeccionar su estado interno. No puedes predecir cuándo se disparará la compactación. No puedes controlar el comportamiento del modelo de forma determinista. Las únicas herramientas que tienes son los hooks (que se disparan en puntos concretos del ciclo de vida), los archivos de estado (que gestionas tú) y la ingeniería de prompts (que funciona hasta que deja de funcionar).
Todas nuestras suposiciones sobre la fiabilidad eran erróneas:
- "El prompt siempre se podrá detectar": falso. Las secuencias de escape ANSI, los tamaños de búfer y los tiempos conspiran para ocultarlo.
- "Los ID de sesión son estables": falso.
/cleargenera uno nuevo. La recuperación tras la compactación, también. - "El contexto sobrevive a la compactación": falso. La compactación tiene pérdidas. El estado dinámico es lo primero que se pierde.
- "El modelo sigue las instrucciones": falso. Las sigue hasta que el contexto se satura; entonces se desvía. Las reglas de estilo son la primera baja.
- "Si el prototipo funciona, el sistema funciona": catastróficamente falso. Cada bug solo se manifestaba después de la iteración 3, o a los 20 minutos, o tras la compactación, o bajo carga concurrente.
La arquitectura final tiene cinco archivos de hooks, un controlador de loops del lado del servidor, un controlador de agentes con lógica de reintentos, un sistema de diario, guardado obligatorio de memorias, un puente para la compactación, controles de propiedad y un escalado a envío forzado para cuando la detección del prompt falla tres veces seguidas. Es compleja porque el problema es complejo. Primero probamos enfoques más simples. Todos murieron después de 3 iteraciones.
El sistema de loops lleva más de una semana ejecutando en producción campañas de interacción en redes sociales. Loops de automatización del navegador de 50 iteraciones que sobreviven a la compactación, manejan con soltura los muros de inicio de sesión, mantienen el contexto durante horas de ejecución y producen una calidad constante de la iteración 1 a la iteración 50.
Funciona. Solo hicieron falta tres semanas de infierno para llegar hasta aquí.
Todos los bugs, arreglos y decisiones de arquitectura documentados arriba están guardados en la memoria persistente de SynaBun: 21+ entradas repartidas en varias categorías. Cada vez que tocamos el sistema de loops, la IA que lo construyó recuerda exactamente qué salió mal la última vez. Para eso existe la memoria persistente. Tu IA debería aprender de sus errores para que no tengas que volver a explicárselos.
Bastantes veces tuvimos que explicárselos nosotros la primera vez.