Ir al contenido

Optimiza OpenClaw: Implementa el worker asíncrono en Discord

¿Te ha pasado que tu bot de Discord se queda en silencio justo cuando el agente está a punto de dar la respuesta perfecta? Es frustrante ver cómo un proceso saludable muere solo porque el listener alcanzó su límite de tiempo.

Manejar eventos en tiempo real requiere rapidez, pero los procesos de IA suelen tomar su tiempo. Si intentas hacer todo dentro del ciclo de vida del Gateway, terminarás con usuarios molestos y ejecuciones interrumpidas. Aquí te explico cómo estamos moviendo todo a un modelo asíncrono para que eso no vuelva a pasar.

Queremos eliminar los fallos por timeout en el listener de Discord que afectan a los usuarios. Para lograrlo, haremos que los turnos entrantes de Discord sean asíncronos:

  1. El listener del Gateway acepta y normaliza los eventos entrantes rápidamente.
  2. Una cola de ejecución de Discord almacena los jobs serializados, usando la misma lógica de ordenamiento que tenemos hoy.
  3. Un worker ejecuta el turno real del agente fuera del tiempo de vida del listener de Carbon.
  4. Las respuestas se envían al canal o hilo de origen una vez que la ejecución termina.

Esta es la solución a largo plazo para evitar que los turnos de Discord en cola fallen por timeout en channels.discord.eventQueue.listenerTimeout mientras el agente todavía está trabajando.

Este plan está parcialmente implementado.

Lo que ya está listo:

  • El timeout del listener de Discord y el timeout de ejecución de Discord ahora son configuraciones separadas.
  • Los turnos entrantes aceptados se añaden a la cola en src/discord/monitor/inbound-worker.ts.
  • El worker ahora es el dueño del turno de larga duración en lugar del listener de Carbon.
  • Se mantiene el orden por ruta mediante la clave de la cola.
  • Existe cobertura de pruebas para evitar regresiones de timeout en la ruta del worker de Discord.

En palabras sencillas:

  • El bug de timeout en producción está corregido.
  • El turno de larga duración ya no muere solo porque el presupuesto de tiempo del listener de Discord expira.
  • La arquitectura del worker aún no está terminada.

Lo que falta:

  • DiscordInboundJob todavía está normalizado solo en parte y aún mantiene referencias live del runtime.
  • La semántica de comandos (stop, new, reset, futuros controles de sesión) aún no es totalmente nativa del worker.
  • La observabilidad del worker y el estado del operador son mínimos.
  • Todavía no hay durabilidad ante reinicios.

El comportamiento actual vincula todo el turno del agente al tiempo de vida del listener:

  • src/discord/monitor/listeners.ts aplica el timeout y el límite de aborto.
  • src/discord/monitor/message-handler.ts mantiene la ejecución en cola dentro de ese límite.
  • src/discord/monitor/message-handler.process.ts realiza la carga de medios, enrutamiento, despacho, typing, streaming de borradores y entrega final de la respuesta de forma síncrona.

Esa arquitectura tiene dos problemas:

  • El watchdog del listener puede abortar turnos largos que están funcionando bien.
  • Los usuarios pueden no recibir respuesta incluso cuando el runtime habría generado una.

Subir el timeout ayuda, pero no cambia el modo de fallo.

  • No rediseñes canales que no sean de Discord en esta pasada.
  • No amplíes esto a un framework de worker genérico para todos los canales en la primera implementación.
  • No extraigas una abstracción de worker entrante compartida entre canales todavía; solo comparte primitivas de bajo nivel cuando la duplicación sea obvia.
  • No añadidas recuperación ante caídas con persistencia en la primera fase, a menos que sea necesario para un despliegue seguro.
  • No cambies la selección de rutas, la semántica de bindings o la política de ACP en este plan.

La ruta de procesamiento de Discord actual todavía depende de algunos objetos live del runtime que no deberían estar dentro del payload del job a largo plazo:

  • Carbon Client
  • Formas de eventos de Discord crudas
  • Mapa de historial de guilds en memoria
  • Callbacks del gestor de thread binding
  • Estado de typing y streaming de borradores

Ya movimos la ejecución a una cola de worker, pero el límite de normalización está incompleto. Ahora mismo, el worker se ejecuta “más tarde en el mismo proceso con algunos de los mismos objetos live”, en lugar de ser un límite de job basado solo en datos.

DiscordMessageListener sigue siendo el punto de entrada, pero su trabajo ahora es:

  • Ejecutar verificaciones de preflight y políticas.
  • Normalizar la entrada aceptada en un DiscordInboundJob serializable.
  • Encolar el job en una cola asíncrona por sesión o por canal.
  • Retornar inmediatamente a Carbon una vez que el encolado tiene éxito.

El listener ya no debe ser el dueño del ciclo de vida completo del turno del LLM.

Introduce un descriptor de job serializable que contenga solo los datos necesarios para ejecutar el turno más tarde.

Forma mínima:

  • Identidad de la ruta
    • agentId
    • sessionKey
    • accountId
    • channel
  • Identidad de entrega
    • ID del canal de destino
    • ID del mensaje al que se responde
    • ID del hilo si existe
  • Identidad del remitente
    • ID, etiqueta, nombre de usuario y tag del remitente
  • Contexto del canal
    • ID de la guild
    • Nombre o slug del canal
    • Metadatos del hilo
    • Override del system prompt resuelto
  • Cuerpo del mensaje normalizado
    • Texto base
    • Texto efectivo del mensaje
    • Descriptores de archivos adjuntos o referencias de medios resueltas
  • Decisiones de gating
    • Resultado del requisito de mención
    • Resultado de autorización de comandos
    • Metadatos de la sesión o agente vinculados si aplica

El payload del job no debe contener objetos live de Carbon ni closures mutables.

Estado actual de implementación:

  • Parcialmente hecho.
  • src/discord/monitor/inbound-job.ts existe y define el traspaso al worker.
  • El payload todavía contiene contexto live del runtime de Discord y debe reducirse más.

Añade un ejecutor de worker específico para Discord responsable de:

  • Reconstruir el contexto del turno desde DiscordInboundJob.
  • Cargar medios y cualquier metadato adicional del canal necesario para la ejecución.
  • Despachar el turno del agente.
  • Entregar los payloads de respuesta final.
  • Actualizar el estado y los diagnósticos.

Ubicación recomendada:

  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.ts

El orden debe ser equivalente al actual para un límite de ruta determinado.

Clave recomendada:

  • Usa la misma lógica de clave de cola que resolveDiscordRunQueueKey(...).

Esto preserva el comportamiento existente:

  • Una conversación de agente vinculada no se entrelaza consigo misma.
  • Diferentes canales de Discord pueden progresar de forma independiente.

Después de la transición, habrá dos clases de timeout separadas:

  • Timeout del listener
    • Solo cubre la normalización y el encolado.
    • Debe ser corto.
  • Timeout de ejecución
    • Opcional, propiedad del worker, explícito y visible para el usuario.
    • No debe heredarse accidentalmente de la configuración del listener de Carbon.

Esto elimina el acoplamiento accidental entre “el listener del gateway de Discord se mantuvo vivo” y “la ejecución del agente es saludable”.

  • Estado: Parcialmente implementado.
  • Hecho:
    • Se extrajo buildDiscordInboundJob(...).
    • Se añadieron pruebas de traspaso al worker.
  • Pendiente:
    • Hacer que DiscordInboundJob sea solo datos planos.
    • Mover las dependencias live del runtime a servicios propiedad del worker en lugar de incluirlas en el payload de cada job.
    • Dejar de reconstruir el contexto del proceso inyectando referencias live del listener en el job.
  • Estado: Implementado.
  • Hecho:
    • Se añadió DiscordInboundWorkerQueue con clave por ruta de ejecución.
    • El listener encola jobs en lugar de esperar directamente a processDiscordMessage(...).
    • El worker ejecuta los jobs en el mismo proceso, solo en memoria.

Esta es la primera transición funcional.

  • Estado: No iniciado.
  • Mueve la propiedad de la entrega, el typing y el streaming de borradores detrás de adaptadores orientados al worker.
  • Reemplaza el uso directo del contexto live de preflight con la reconstrucción del contexto del worker.
  • Mantén processDiscordMessage(...) temporalmente como una fachada si es necesario, luego divídelo.
  • Estado: No iniciado. Asegúrate de que los comandos nativos de Discord sigan funcionando correctamente cuando el trabajo está en cola:

  • stop

  • new

  • reset

  • Cualquier comando futuro de control de sesión.

La cola del worker debe exponer suficiente estado de ejecución para que los comandos puedan dirigirse al turno activo o en cola.

Fase 5: Observabilidad y experiencia del operador

Sección titulada «Fase 5: Observabilidad y experiencia del operador»
  • Estado: No iniciado.
  • Emite la profundidad de la cola y el recuento de workers activos en el estado del monitor.
  • Registra el tiempo de encolado, tiempo de inicio, tiempo de finalización y el motivo de timeout o cancelación.
  • Muestra claramente en los logs los fallos de entrega o timeouts propiedad del worker.
  • Estado: No iniciado. Solo después de que la versión en memoria sea estable:

  • Decide si los jobs de Discord en cola deben sobrevivir a un reinicio del gateway.

  • Si es así, persiste los descriptores de job y los checkpoints de entrega.

  • Si no, documenta el límite explícito en memoria.

Esto debería ser un seguimiento separado a menos que la recuperación tras reinicio sea obligatoria para el lanzamiento.

Archivos principales actuales:

  • src/discord/monitor/listeners.ts
  • src/discord/monitor/message-handler.ts
  • src/discord/monitor/message-handler.preflight.ts
  • src/discord/monitor/message-handler.process.ts
  • src/discord/monitor/status.ts

Archivos actuales del worker:

  • src/discord/monitor/inbound-job.ts
  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.test.ts
  • src/discord/monitor/message-handler.queue.test.ts

Próximos puntos de contacto probables:

  • src/auto-reply/dispatch.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/monitor/thread-bindings.ts
  • src/discord/monitor/native-command.ts

El siguiente paso es hacer que el límite del worker sea real en lugar de parcial.

Haz esto a continuación:

  1. Mueve las dependencias live del runtime fuera de DiscordInboundJob.
  2. Mantén esas dependencias en la instancia del worker de Discord en su lugar.
  3. Reduce los jobs en cola a datos planos específicos de Discord:
    • Identidad de la ruta.
    • Destino de entrega.
    • Información del remitente.
    • Snapshot del mensaje normalizado.
    • Decisiones de gating y binding.
  4. Reconstruye el contexto de ejecución del worker a partir de esos datos planos dentro del worker.

En la práctica, esto significa que:

  • client
  • threadBindings
  • guildHistories
  • discordRestFetch
  • Otros manejadores mutables solo de runtime.

Deben dejar de vivir en cada job en cola y, en su lugar, vivir en el propio worker o detrás de adaptadores propiedad del worker.

Después de que eso se implemente, el siguiente seguimiento debe ser la limpieza del estado de comandos para stop, new y reset.

Mantén la cobertura actual de reproducción de timeouts en:

  • src/discord/monitor/message-handler.queue.test.ts

Añade nuevas pruebas para:

  1. El listener retorna después del encolado sin esperar al turno completo.
  2. Se preserva el orden por ruta.
  3. Diferentes canales siguen ejecutándose de forma concurrente.
  4. Las respuestas se entregan al destino original del mensaje.
  5. stop cancela la ejecución activa propiedad del worker.
  6. El fallo del worker produce diagnósticos visibles sin bloquear jobs posteriores.
  7. Los canales de Discord vinculados a ACP siguen enrutándose correctamente bajo la ejecución del worker.
  • Riesgo: La semántica de los comandos se desvía del comportamiento síncrono actual. Mitigación: Implementa la conexión del estado de los comandos en la misma transición, no después.

  • Riesgo: La entrega de respuestas pierde el contexto del hilo o del mensaje al que responde. Mitigación: Haz que la identidad de entrega sea de primera clase en DiscordInboundJob.

  • Riesgo: Envíos duplicados durante reintentos o reinicios de la cola. Mitigación: Mantén la primera pasada solo en memoria, o añade idempotencia de entrega explícita antes de la persistencia.

  • Riesgo: message-handler.process.ts se vuelve difícil de entender durante la migración. Mitigación: Divídelo en helpers de normalización, ejecución y entrega antes o durante la transición al worker.

El plan está completo cuando:

  1. El timeout del listener de Discord ya no aborta turnos de larga duración saludables.
  2. El tiempo de vida del listener y el tiempo de vida del turno del agente son conceptos separados en el código.
  3. Se preserva el orden por sesión existente.
  4. Los canales de Discord vinculados a ACP funcionan a través de la misma ruta del worker.
  5. stop se dirige a la ejecución propiedad del worker en lugar de a la antigua pila de llamadas del listener.
  6. Los fallos de timeout y entrega se convierten en resultados explícitos del worker, no en caídas silenciosas del listener.

Termina esto en PRs de seguimiento:

  1. Haz que DiscordInboundJob sea solo datos planos y mueve las referencias live del runtime al worker.
  2. Limpia la propiedad del estado de comandos para stop, new y reset.
  3. Añade observabilidad del worker y estado del operador.
  4. Decide si la durabilidad es necesaria o documenta explícitamente el límite en memoria.

Esto sigue siendo un seguimiento acotado si se mantiene solo para Discord y si seguimos evitando una abstracción prematura de worker para todos los canales.

¿Necesitas ayuda para configurar el worker en tu entorno? Prueba el AI Setup Assistant.

OpenClaw

OpenClaw Expert

Sigues atascado?

Si esta pagina no resolvio tu caso, pregunta a OpenClaw Expert para pasos concretos.