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.
Objetivo
Sección titulada «Objetivo»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:
- El listener del Gateway acepta y normaliza los eventos entrantes rápidamente.
- Una cola de ejecución de Discord almacena los jobs serializados, usando la misma lógica de ordenamiento que tenemos hoy.
- Un worker ejecuta el turno real del agente fuera del tiempo de vida del listener de Carbon.
- 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.
Estado actual
Sección titulada «Estado actual»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:
DiscordInboundJobtodaví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.
Por qué existe esto
Sección titulada «Por qué existe esto»El comportamiento actual vincula todo el turno del agente al tiempo de vida del listener:
src/discord/monitor/listeners.tsaplica el timeout y el límite de aborto.src/discord/monitor/message-handler.tsmantiene la ejecución en cola dentro de ese límite.src/discord/monitor/message-handler.process.tsrealiza 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.
Lo que no buscamos
Sección titulada «Lo que no buscamos»- 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.
Restricciones actuales
Sección titulada «Restricciones actuales»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.
Arquitectura objetivo
Sección titulada «Arquitectura objetivo»1. Etapa del Listener
Sección titulada «1. Etapa del Listener»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
DiscordInboundJobserializable. - 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.
2. Payload del Job normalizado
Sección titulada «2. Payload del Job normalizado»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
agentIdsessionKeyaccountIdchannel
- 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.tsexiste y define el traspaso al worker.- El payload todavía contiene contexto live del runtime de Discord y debe reducirse más.
3. Etapa del Worker
Sección titulada «3. Etapa del Worker»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.tssrc/discord/monitor/inbound-job.ts
4. Modelo de ordenamiento
Sección titulada «4. Modelo de ordenamiento»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.
5. Modelo de timeout
Sección titulada «5. Modelo de timeout»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”.
Fases de implementación recomendadas
Sección titulada «Fases de implementación recomendadas»Fase 1: Límite de normalización
Sección titulada «Fase 1: Límite de normalización»- Estado: Parcialmente implementado.
- Hecho:
- Se extrajo
buildDiscordInboundJob(...). - Se añadieron pruebas de traspaso al worker.
- Se extrajo
- Pendiente:
- Hacer que
DiscordInboundJobsea 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.
- Hacer que
Fase 2: Cola de worker en memoria
Sección titulada «Fase 2: Cola de worker en memoria»- Estado: Implementado.
- Hecho:
- Se añadió
DiscordInboundWorkerQueuecon 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.
- Se añadió
Esta es la primera transición funcional.
Fase 3: División del proceso
Sección titulada «Fase 3: División del proceso»- 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.
Fase 4: Semántica de comandos
Sección titulada «Fase 4: Semántica de comandos»-
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.
Fase 6: Seguimiento opcional de durabilidad
Sección titulada «Fase 6: Seguimiento opcional de durabilidad»-
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.
Impacto en los archivos
Sección titulada «Impacto en los archivos»Archivos principales actuales:
src/discord/monitor/listeners.tssrc/discord/monitor/message-handler.tssrc/discord/monitor/message-handler.preflight.tssrc/discord/monitor/message-handler.process.tssrc/discord/monitor/status.ts
Archivos actuales del worker:
src/discord/monitor/inbound-job.tssrc/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.test.tssrc/discord/monitor/message-handler.queue.test.ts
Próximos puntos de contacto probables:
src/auto-reply/dispatch.tssrc/discord/monitor/reply-delivery.tssrc/discord/monitor/thread-bindings.tssrc/discord/monitor/native-command.ts
Siguiente paso ahora
Sección titulada «Siguiente paso ahora»El siguiente paso es hacer que el límite del worker sea real en lugar de parcial.
Haz esto a continuación:
- Mueve las dependencias live del runtime fuera de
DiscordInboundJob. - Mantén esas dependencias en la instancia del worker de Discord en su lugar.
- 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.
- Reconstruye el contexto de ejecución del worker a partir de esos datos planos dentro del worker.
En la práctica, esto significa que:
clientthreadBindingsguildHistoriesdiscordRestFetch- 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.
Plan de pruebas
Sección titulada «Plan de pruebas»Mantén la cobertura actual de reproducción de timeouts en:
src/discord/monitor/message-handler.queue.test.ts
Añade nuevas pruebas para:
- El listener retorna después del encolado sin esperar al turno completo.
- Se preserva el orden por ruta.
- Diferentes canales siguen ejecutándose de forma concurrente.
- Las respuestas se entregan al destino original del mensaje.
stopcancela la ejecución activa propiedad del worker.- El fallo del worker produce diagnósticos visibles sin bloquear jobs posteriores.
- Los canales de Discord vinculados a ACP siguen enrutándose correctamente bajo la ejecución del worker.
Riesgos y mitigaciones
Sección titulada «Riesgos y mitigaciones»-
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.tsse 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.
Criterios de aceptación
Sección titulada «Criterios de aceptación»El plan está completo cuando:
- El timeout del listener de Discord ya no aborta turnos de larga duración saludables.
- El tiempo de vida del listener y el tiempo de vida del turno del agente son conceptos separados en el código.
- Se preserva el orden por sesión existente.
- Los canales de Discord vinculados a ACP funcionan a través de la misma ruta del worker.
stopse dirige a la ejecución propiedad del worker en lugar de a la antigua pila de llamadas del listener.- Los fallos de timeout y entrega se convierten en resultados explícitos del worker, no en caídas silenciosas del listener.
Estrategia de lanzamiento restante
Sección titulada «Estrategia de lanzamiento restante»Termina esto en PRs de seguimiento:
- Haz que
DiscordInboundJobsea solo datos planos y mueve las referencias live del runtime al worker. - Limpia la propiedad del estado de comandos para
stop,newyreset. - Añade observabilidad del worker y estado del operador.
- 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.
Siguiente paso
Sección titulada «Siguiente paso»OpenClaw Expert
Sigues atascado?
Si esta pagina no resolvio tu caso, pregunta a OpenClaw Expert para pasos concretos.