Tenía una tabla en la base de datos que debía llenarse sola. Su trabajo era aprender de los fracasos: cada vez que el sistema probaba una variante de algo y no funcionaba, la guardaba, para reciclarla más tarde en otro contexto donde quizá sí sirviera. Un laboratorio de intentos fallidos, acumulándose.
En producción tenía cero filas. Llevaba días desplegada y no había guardado ni un registro. Mientras tanto, una tabla vecina —otra memoria, la que anota qué trabajo ya está agotado para no repetirlo— crecía con normalidad.
La tentación es obvia: hay un bug en la escritura. Fui a buscarlo, y no estaba.
Cero filas no es lo mismo que un error de escritura
El camino que guarda en esa tabla emite un aviso si la escritura falla. Busqué esos avisos en los registros: cero. Ninguna escritura había fallado.
Eso es un dato, no una ausencia de dato. Si el camino se hubiera recorrido y hubiera fallado, habría dejado rastro. Cero rastros y cero filas solo encajan con una explicación: el camino de escritura nunca se ejecutó. No corrió-y-falló. No corrió.
Es la diferencia entre un negativo real y un negativo que nunca se puso a prueba, y se ven igual si no buscas el positivo de control —algo que el registro SÍ mostraría si el camino se hubiera recorrido—. Sin él, "sano y quieto" y "muerto" se ven idénticos.
Dos mecanismos que se mataban de hambre
¿Por qué no se ejecutaba? Por otro mecanismo, aguas arriba, que hacía bien su trabajo.
Ese sistema tiene una memoria negativa: cuando agota todo lo que sabe probar contra un objetivo, lo anota, para no volver a gastar esfuerzo en algo que ya sabe que no da. Es una optimización sensata. Pero estaba puesta antes de la fase que generaba variantes nuevas —la fase que, al fracasar, habría alimentado la biblioteca—. En cuanto un objetivo entraba en "agotado", esa fase se saltaba entera. Y si la fase nunca corre, nunca produce un fracaso que guardar.
Cada mecanismo, por separado, es correcto. La memoria negativa evita trabajo inútil. La biblioteca aprende de los fracasos. Juntos formaban un punto muerto: el primero mataba de hambre al segundo. La biblioteca solo podía llenarse desde una fase que el otro apagaba.
Es un patrón que se repite: cuando añades un mecanismo para contener lo que hace otro, y el sistema empieza a comportarse raro, sospecha del diseño antes que del código. El síntoma —una tabla en cero— parecía un bug; era una tensión entre dos piezas que por separado están bien.
Un valor con dos significados son dos campos
Había un segundo problema, y es el más instructivo. La biblioteca solo guardaba un tipo de fracaso: aquel en el que el sistema procesaba el intento y devolvía un resultado normal, sin éxito. Al otro tipo —cuando el intento quedaba bloqueado de entrada, sin llegar a procesarse— lo descartaba, con un argumento razonable: un bloqueo dice más de la postura del objetivo que de la variante.
Pero ese argumento se cae al reciclar. La variante bloqueada en un objetivo es un candidato no probado en otro, con otra configuración. Descartarla era, precisamente, no aprender del fracaso más común.
El arreglo fue no colapsar los dos significados en uno. "Fracaso procesado" y "fracaso bloqueado" son dos señales distintas, así que van en dos campos distintos: dos contadores. Así la biblioteca guarda ambos, y el que recicla los ordena —los procesados, más informativos, primero; los bloqueados, al fondo, pero presentes—. Un valor con dos significados no es un campo: son dos.
Lo que quedó
Dos correcciones sobre el mismo síntoma. Ninguna tocó la escritura, que siempre fue correcta. Una rompió el punto muerto —dejar que la fase alimente la biblioteca aunque el objetivo esté "agotado"—; la otra separó las dos caras del fracaso. Y la tabla en cero, que parecía el bug, era el mensajero: un camino que nunca se recorría y una condición demasiado estrecha, mirados por el mismo agujero.
Xiliux