
Skynet estrena memoria y una mascota que te cuenta lo que pasa
Semana del 19 al 25 de octubre. Skynet, nuestro orquestador de agentes, aprendió a recordar lo que hace entre una ejecución y la siguiente y ganó una mascota que te dice en qué está. Por el camino descubrimos que terminar un trabajo no es lo mismo que entregarlo.
Lo que nos propusimos
Skynet es el orquestador en el que varios agentes de código (Claude Code, Codex y Antigravity) planifican, implementan, revisan y validan cambios por su cuenta. El domingo pasado ya sabía lanzarlos, pasarles datos entre fases y pararse a esperar a una persona. Lo que hacían, sin embargo, se evaporaba al terminar: un log, un coste y poco más.
La semana empezó cerrando lo que quedaba de la Fase 2: los tres proveedores reales funcionando de verdad (Gemini CLI se retiró y su sitio lo ocupó Antigravity) y la última pieza del editor visual, que ya permite crear, versionar y publicar un workflow desde cero sin tocar YAML.
A partir de ahí, el tema fue que lo que producen los agentes se quede y llegue a alguien:
- que los archivos que genera una ejecución (un informe, una imagen) se puedan abrir y descargar;
- que Skynet tenga memoria: que recuerde lo que se hizo en ejecuciones anteriores para que la siguiente no empiece de cero;
- y que la interfaz diga qué está pasando sin que tengas que ir a mirarlo.
El ciclo fue el de siempre: Claude escribe la PR desde Claude Desktop, Hermes la despliega en la máquina de pruebas y ejecuta su guía de aceptación, y Diego decide si se fusiona. Veinticuatro veces.
Los problemas que nos encontramos
Un documento que solo era una ruta
Una ejecución generó un informe en Markdown dentro de su espacio de trabajo. La interfaz enseñaba esto: una ruta. Ni abrirlo ni descargarlo. El trabajo estaba hecho y, a efectos prácticos, no existía.
La solución tentadora era tratar como resultado cualquier fichero que el agente hubiera tocado. Habría funcionado en la demo. También habría convertido cada archivo temporal en un «entregable» y, peor, habría abierto la puerta a descargar cosas fuera del espacio de trabajo: revisando el código apareció que la comprobación de artefactos existente seguía enlaces simbólicos, así que un enlace a un fichero de fuera contaba como «existe».
El evento que nunca se podía guardar
Probando esos artefactos, un agente se quedó colgado. No había fallado: Skynet lo marcaba como «sin respuesta» mientras el runner seguía intentando enviar lo mismo una y otra vez.
El agente había leído un ZIP. La salida binaria traía un byte nulo, el runner lo metió en un evento y PostgreSQL se negó a guardarlo. El lote entero volvía con un error, el runner lo reintentaba, y así para siempre. Un único carácter invisible bloqueaba toda la cola de ese agente.
Una memoria que el agente no consultaba
Para la memoria, el plan inicial era modesto: notas escritas a mano que se añaden al prompt. Diego quería otra cosa, que Skynet recordara lo que se había hecho para poder retomar proyectos y lanzar investigaciones encima de lo anterior. Eso se parece mucho a Engram, la memoria persistente que usamos en Claude Code. Pero Engram guarda en un SQLite local de cada máquina, y Skynet puede tener varios runners. Así que no se usó Engram: se copió su modelo dentro del propio Skynet.
Luego vinieron las sorpresas. Codex rechazaba las herramientas de memoria porque su política es no ejecutar nada que pida aprobación. Antigravity no tiene soporte verificado para servidores MCP, así que solo recibe un bloque inicial. Y en la primera prueba completa con Claude Code, el agente recibió cinco entradas de memoria en el prompt y no buscó ni leyó ninguna más. La memoria estaba ahí. Nadie la miró. Hermes marcó la aceptación como parcial, no como «casi».
El informe que no llegó
La más clara de todas la contó Hermes. Una aceptación de memoria: GitHub entrega el evento, la cola lo recoge, el agente despliega, trabaja varios minutos y termina. Todo en verde. Y el mensaje final nunca apareció en Telegram. Desde fuera, parecía un webhook perdido; por dentro, el trabajo estaba hecho y el resultado guardado.
En la misma línea, un worker que alcanzó el límite de herramientas dejó la cola marcada como ocupada sin que hubiera ningún proceso detrás. Y el domingo, la revisión final de los iconos no se lanzó: GitHub entregó el evento justo cuando la máquina de pruebas estaba sin memoria, recibió un 500 y no volvió a intentarlo.
Las fuentes que no eran nuestras
Para la nueva tipografía, Diego pasó unos ficheros de fuente. Al abrirlos, eran Duolingo Sans, con una licencia que prohíbe usarlas fuera de Duolingo, y el repositorio de Skynet es público. Funcionaban perfectamente; no se podían usar.
Cómo lo resolvimos
Los artefactos se resolvieron con un contrato: una fase declara qué archivos produce, y Skynet captura solo esos. Nada de «lo que haya cambiado». La captura comprueba que la ruta está dentro del espacio de trabajo, rechaza los enlaces que salen de él, y la web los enseña junto a la fase que los generó, documentos Markdown e imágenes incluidos. Al cargar las imágenes desde el propio endpoint de descarga ni siquiera hizo falta tocar la política de seguridad de contenidos.
El byte nulo se arregló en los dos extremos (más abajo, el único fragmento técnico del post).
La memoria quedó con procedencia y permisos: quién escribió cada entrada, versiones, fijado, edición y archivo. Los agentes la consultan y la escriben con herramientas MCP explícitas; a Codex se le aprueban solo las del servidor de memoria, nada más. Cada fase declara si la memoria está apagada, es de solo lectura o puede escribir, y al terminar cada ejecución Skynet deja una entrada de bitácora con estado, rama, cambios y coste. El workflow de investigar mejoras pasó a pedir explícitamente que se busque en la memoria, y en la segunda prueba el agente sí lo hizo.

La memoria de un proyecto. Arriba, lo que escribe una persona y queda fijado; debajo, la bitácora que Skynet deja sola al terminar cada ejecución.

La segunda prueba. Esta vez el agente sí abre la memoria con sus herramientas, encuentra la restricción fijada y la respeta.
Para que las pruebas con agentes reales fueran baratas, Hermes creó
demo-repo: un proyecto Python minúsculo, con un analizador de texto y
tres tests. Una aceptación completa de memoria costó unos 0,41 dólares. Si
se hubiera hecho contra el propio Skynet, habría sido mucho más lenta y
cara para comprobar lo mismo.
Para el último tramo hacia las personas, la respuesta de esta semana fue la mascota. Un companion que vive en todas las páginas y dice si Skynet espera, trabaja, necesita una aprobación, ha fallado o ha terminado, también fuera de la pestaña: en el título, en el favicon y con notificaciones del navegador. La primera versión era un nigiri del boceto de Diego y no encajaba con la estética; la definitiva sale del logo. Y al construirla apareció otro hueco: una conexión nueva a los eventos en vivo recibía el histórico completo desde el principio. Ahora pide solo lo que viene.

El dashboard con el companion y la tipografía nueva. Lo que antes había que ir a buscar en la lista de ejecuciones ahora te lo dice él.
Las fuentes de Duolingo se descartaron y se eligió la alternativa libre más parecida, Barlow, con JetBrains Mono para el código.
Lo del informe que no llegó, en cambio, no está resuelto. Hermes reconstruyó el recorrido con las entregas de GitHub, la cola y las sesiones, y recuperó el informe a mano. Lo que hace falta es un outbox duradero: guardar el resultado, el destino y el estado del envío, y reintentar después de un reinicio. Está en la lista de la semana que viene.
El único fragmento técnico de este post
El byte nulo es un caso de manual de mensaje envenenado, y la forma en que bloqueó la cola se repite en cualquier sistema con eventos.
PostgreSQL no admite el carácter \u0000 dentro de un jsonb, ni siquiera
escapado. Si el productor manda un lote de eventos y uno lo lleva, el lote
entero falla. Si la política del productor es «reintenta hasta que
funcione», no funcionará nunca, y todo lo que venga detrás queda atascado.
El arreglo tiene tres partes, y las tres hacen falta:
- sanear en origen: el runner limpia los caracteres que la base no acepta antes de enviar;
- sanear en destino: el control plane no confía en que el runner lo haya hecho, porque mañana habrá otro runner o otra versión;
- aislar el fallo: si un lote falla, se reenvía evento a evento, y el que no se puede guardar se marca como inválido y se aparta, en lugar de reintentarse sin fin.
La tercera es la que más se olvida. Las dos primeras arreglan este byte; la tercera arregla el siguiente que no hemos imaginado. Los ocho tests que reproducían el bloqueo se escribieron primero y fallaron antes del arreglo.
Mientras tanto, Hermes
Además de aceptar cada PR, Hermes tuvo una semana de operador. La revisión de código con proveedores reales se convirtió en una frontera de seguridad: un agente revisor tiene herramientas, y nada impedía que alguna escribiera donde no debía. De ahí salieron cuatro entregas para endurecer rutas, Git, secretos, entradas y despliegue. El principio: un revisor inspecciona y explica, pero no adquiere permisos por accidente.
Fuera de Skynet, recuperó la integración entre Alexa y Home Assistant (el enlace de cuentas apuntaba a una URL antigua), arrancó el rack doméstico en el orden correcto tras un corte de luz (primero el NAS, luego las Raspberry que dependen de él) y diagnosticó un reinicio de la máquina de pruebas: abrir VS Code en remoto con Java y Gradle agotó sus 8 GiB de RAM y toda la swap comprimida. La decisión fue no usarla como máquina de desarrollo. Después del reinicio, Akademia no volvió sola porque sus contenedores no tenían política de reinicio. Ahora la tienen.
Lo que no salió bien
- El informe que no llegó a Telegram sigue sin una solución duradera.
- Un worker dejó la cola ocupada sin proceso, otra vez.
- La revisión de los iconos se perdió por un 500 del webhook durante el bloqueo de memoria de la máquina de pruebas.
- La primera prueba de memoria fue parcial: el agente no consultó la memoria y un secreto no quedaba marcado.
- Las fuentes no se podían usar por licencia.
- Tests intermitentes por todas partes: bloqueos de PostgreSQL al cancelar dos agentes a la vez, una carrera de git al crear worktrees y Maven Central limitando descargas cuatro veces.
- El límite de cinco horas se agotó el domingo a mitad de los iconos, que siguen en una PR abierta.
Lo que me llevo
Esta semana Skynet aprendió a producir cosas que se quedan: archivos, memoria, una bitácora, una mascota que avisa. Y casi todos los problemas vinieron del mismo sitio, el último tramo. Un documento que existe pero solo es una ruta. Una memoria que está en el prompt y nadie consulta. Un evento que se genera y nunca se guarda. Un informe que se termina y no se envía.
«El agente terminó» es un estado del sistema. «Alguien lo recibió» es otro, y no se deriva del primero. Hay que construirlo aparte, con su contrato (qué se entrega), su persistencia (qué pasa si se cae a mitad) y su evidencia (cómo sé que llegó).
La semana que viene: el outbox duradero para los informes, un cierre del worker que no dependa de que el agente llegue a su última llamada, y los iconos que faltan.
La semana en cifras
main toca 348 ficheros
Las capturas son del despliegue privado de Skynet en la
máquina de pruebas, el domingo 25 a mediodía, con los datos reales de las
aceptaciones sobre demo-repo. La pestaña de artefactos no
aparece porque ninguna de las ejecuciones conservadas declaraba archivos de
salida.