El log decía IMAP login failed: Invalid credentials. La contraseña era correcta. Lo comprobé copiándola a mano, iniciando sesión desde otro script, mirando la configuración carácter por carácter. Todo bien. El agente seguía sin poder entrar al correo.
Me llevó un rato encontrarlo, y cuando lo encontré me dio vergüenza: el problema eran dos comillas. Dos caracteres que ningún error mencionaba, en un archivo que yo mismo había escrito, y que llevaban días saboteando el login.
Esta semana se me juntaron tres bugs de esa familia: los que mienten en el log. Los cuento con fechas y arreglos reales porque los tres viven en mi auto_healing_patterns.json, el archivo donde mi sistema anota cada patrón de fallo para no tropezar dos veces con la misma piedra. Ya va por 29.
Bug 1: las comillas invisibles (hoy mismo)
Mis scripts gsc-watcher-v3.py y seo_chain.py leen el archivo .env con un parser propio de tres líneas, porque no quería meter python-dotenv como dependencia en ese punto. El parser hace esto:
k, v = line.split('=', 1)
env[k.strip()] = v.strip()
Funciona. Hasta que el .env tiene una línea así:
GOOGLE_APP_PASSWORD="xxxx xxxx xxxx xxxx"
Con comillas, como manda la costumbre. v.strip() quita espacios, no comillas. El valor que llega a IMAP es "xxxx xxxx xxxx xxxx" con las comillas dentro. Google rechaza el login y el error te dice que las credenciales son inválidas, lo cual es mentira y verdad a la vez: la contraseña es correcta, pero llega envuelta en dos caracteres extra.
La pista definitiva: con python-dotenv la misma línea funcionaba, porque esa librería sí pela las comillas. Ahí cayó el diagnóstico.
El arreglo, aplicado esta mañana en los dos scripts:
env[k.strip()] = v.strip().strip('"').strip("'")
Una línea. Si vas a parsear .env a mano, ponla desde el primer día. O usa python-dotenv y olvídote.
Bug 2: el grep que falla cuando acierta (2 de agosto)
El domingo a las 17:20 mi loop homepage-optimizer agotó sus reintentos y se marcó como EXHAUSTED. El check era de los que verifican ausencia: "que haya 0 duplicados en la homepage". La comprobación usaba grep -c con un máximo esperado de 0.
El detalle que me mordió: grep -c devuelve exit code 1 cuando encuentra 0 coincidencias. Y 0 coincidencias era exactamente lo que yo quería. Mi runner leía exit code 1 y lo traducía como fallo. El loop fallaba por estar sano.
Lo arreglé el lunes a las 3:14 en loop-runner-v2.py: si el check declara expect_min o expect_max y la salida es un número, el número manda. El exit code de grep se ignora en ese caso. La regla general quedó anotada en el archivo de patrones: en checks de ausencia, exit 1 de grep es el estado deseado.
Este es mi bug favorito de los tres, porque el sistema estaba haciendo exactamente lo correcto y lo castigaba por ello.
Bug 3: el parámetro que la API ignora (4 de agosto)
A las 4:25 de la madrugada, extract.py del Nightly Chain abortó con "3 batches vacíos consecutivos". Se quedaron 707 documentos sin procesar. El script llamaba a la API de DeepSeek directa con enable_thinking: false para que el modelo fuera al grano.
El problema: la API ignora ese parámetro. El modelo siguió razonando, gastó los 4096 tokens de max_tokens enteros en razonamiento, y entregó 0 tokens de contenido. El json.loads recibía vacío y el script, con razón, se rendía.
La sintaxis correcta es "thinking": {"type": "disabled"}, más subir max_tokens a 8192 por si acaso. Verificado tras el parche: reasoning tokens de 4096 a 0, el contenido vuelve a llegar, y de paso le agregué al parser un strip de las fences markdown ```json que a veces envuelven la respuesta.
Lo que me molesta de este bug no es el tiempo perdido: es que la documentación del parámetro existe, pero la leí después de la avería, no antes. Clásico.
Lo que los tres tienen en común
En los tres casos el mensaje de error describía un problema que no existía. Credenciales inválidas que eran válidas. Un fallo que era un acierto. Una respuesta vacía que en realidad era una respuesta que no pedí.
La regla que me quedó, y que ahora aplico antes de tocar nada:
- Reproduce el bug en tres líneas antes de arreglarlo. Un script mínimo que llame al parser, al grep o a la API con los mismos datos. Si no puedes reproducirlo en tres líneas, todavía no lo entendiste.
- Duda del log, no de tus datos. Cuando el error contradice algo que verificaste a mano, el que miente es el error.
- Anota el patrón, no solo el fix. Cada uno de estos tres bugs vive en
auto_healing_patterns.jsoncon síntoma, causa y arreglo. Cuando el sistema se encuentra con algo parecido, ya tiene el mapa. El archivo pasó de 0 a 29 patrones en cinco semanas, y la mayoría entraron por errores que parecían otra cosa.
Un sistema de agentes no se cae por los errores grandes. Se cae por los pequeños que se disfrazan de otra cosa.
Los errores grandes, los evidentes, se arreglan solos porque los ves. Los que duelen son estos: invisibles, silenciosos, y con un mensaje de error que te manda a buscar en la dirección equivocada. La única defensa que me ha funcionado es desconfiar del log y escribir cada patrón cuando lo encuentro, para que la próxima vez la búsqueda dure cinco minutos y no una mañana.