⚡Nostr: Por qué mi agente de IA dejó de responder ?
Por qué mi agente de IA dejó de responder por Nostr (y no era nada de lo que parecía)
Tengo un agente de IA corriendo 24/7 en una Raspberry Pi4 self-hosted, que habla conmigo por DMs de Nostr. Después de unos cortes de luz dejó de responder. La causa no era la red, ni los relays, ni el modelo, ni la librería de Nostr. Era física: una CPU débil perdiendo una carrera contra un reloj. Esta es la cacería, y por qué el método importa más que la corazonada.
El montaje
“Moto” es un agente (OpenClaw + Gemini) que vive en una Raspberry Pi 4 sobre Umbrel. Le escribo por dos canales: Telegram y Nostr DM (NIP-04). El de Nostr es el que me importa: porque es soberano, sin intermediarios. Mi cliente móvil (Primal o Amethyst) cifra el DM y un relay lo lleva. El detalle que define todo el problema: es una Pi 4, y donde está hay cortes de luz (VE). Apagones sucios, varios por día a veces. Cada apagón es un reinicio en frío del contenedor.
El síntoma
Después de una tanda de apagones, Moto dejó de responder por Nostr. Le escribía “hola” y nada. Pero por Telegram respondía perfecto, en segundos. Mismo agente, mismo modelo, misma máquina. Esa asimetría (un canal muerto, el otro impecable) es la primera pista, y también la primera trampa. Invita a culpar a “algo de Nostr” y empezar a toquetear su config. Me aguanté las ganas.
La cacería: descartar antes de tocar
Regla que aprendí a la mala: un mensaje que “no llega” tiene muchas causas posibles, y hay que distinguirlas con evidencia, no con corazonadas. Fui tachando sospechosos, cada uno con su prueba:
- ¿La red de la Pi a los relays? Abrí un WebSocket crudo desde dentro del contenedor a cada relay: respondían en 0.7s. Descartado.
- ¿Los relays caídos o lentos? Los consulté desde otra máquina: contestaban al instante.
- ¿Mi cliente no publicaba el DM? Consulté los relays en vivo: mis mensajes sí aterrizaban ahí. El emisor funcionaba.
- ¿El reloj de la Pi desincronizado? (Clásico post-apagón, rompe TLS y firma.) Estaba en hora.
- ¿La librería de Nostr (nostr-tools)? Reproduje su suscripción exacta, mismo filtro y mismos relays, desde la misma Pi, en un script aislado: recibió todo y quedó viva. La librería anda.
- ¿El modelo o el agente? Telegram respondía. El cerebro estaba sano.
- ¿Una sesión corrupta? Sí había una. Un apagón dejó un transcript a medio escribir y la sesión quedó trabada “procesando”, tapando la cola. La limpié. Ayudó, pero no era todo.
Cinco sospechosos obvios, cinco coartadas sólidas. El problema seguía ahí.
El humo
La clave apareció comparando dos formas de levantar el canal:
- En arranque en frío (cuando la Pi bootea), la suscripción a Nostr moría a los ~4 segundos con un
connection timed out, y no reintentaba más. - En recarga en caliente (recargar solo el canal con el agente ya arriba), la misma suscripción conectaba y vivía sin problemas durante minutos.
Misma config, mismos relays, misma red. La única diferencia: cuándo se suscribía.
La causa raíz: una carrera perdida
En el arranque en frío, el canal de Nostr intenta suscribirse mientras el resto del sistema todavía está cargando: calentando el modelo, resolviendo credenciales, inicializando plugins. En una CPU potente eso pasa desapercibido. En una Pi 4, ese trabajo bloquea el hilo de ejecución unos 2 segundos (lo confirmé en las métricas: el event loop se congelaba ~1.9s). ¿Y qué hace una librería de red cuando su hilo se congela? El handshake del WebSocket no se procesa a tiempo, supera el límite interno de la librería (~4.4s) y la conexión se declara “connection timed out”. Peor todavía: la suscripción no se vuelve a intentar sola. El plugin reporta el error y se queda de brazos cruzados. Resultado: Nostr nace muerto en cada arranque, y se queda así. Por eso mi script aislado funcionaba (el hilo estaba libre) y por eso la recarga en caliente funcionaba (el sistema ya había terminado de cargar). No era software roto. Era hardware débil perdiendo una carrera contra un timeout durante el arranque.
La solución
Dos partes:
- Recargar el canal en caliente después de cada arranque. Con el sistema ya estable, la suscripción se monta sin pelear por la CPU y queda sana. Lo automaticé con una tarea programada que se dispara unos 150 segundos después de cada boot. Tras un apagón, Nostr se auto-cura sin que yo toque nada.
- No usar
restartdel contenedor para esto. Eso reinicia todo y vuelve a la carrera del arranque (además de tumbar otros servicios). La recarga en caliente toca solo el canal. El fix de fondo sería que el plugin reintente la suscripción cuando se cae, sobre todo en hardware lento. Eso va como reporte upstream.
Las lecciones (que sirven más allá de Nostr)
- Hardware débil convierte condiciones de carrera latentes en bugs reproducibles. Código que “siempre funcionó” puede romperse en una Pi solo porque el event loop se bloquea unos segundos en el momento equivocado. Los timeouts que nunca disparan en un servidor.
- Distingue emisor de receptor antes de culpar al receptor. Perdí un rato probando desde un cliente que estaba lento publicando, creyendo que Moto no recibía. El problema estaba del otro lado.
- Reproduce el mecanismo en aislamiento. Correr la librería sola, con el filtro exacto, fue lo que partió el problema en dos: “no es la librería, es cómo el sistema la usa bajo presión”.
- La evidencia empírica gana sobre la documentación y la memoria. Más de una pista (qué NIP usa un cliente, qué timestamp tenía un mensaje) solo se resolvió consultando los relays directo.
- Descartar es avanzar. Cada “no es esto” con prueba sólida vale tanto como el hallazgo final.
Por qué importa (para self-hosters)
Correr tu propia infra (un nodo, un agente, lo que sea) en hardware barato y con luz inestable es la realidad de muchas prsonas fuera de los datacenters. Los bugs que vas a encontrar no son los del tutorial: son de recursos, timing y arranque. Vale la pena aprender a diagnosticarlos con método, porque la corazonada casi siempre te manda a toquetear la config equivocada.
Write a comment