Zum Inhalt springen

OpenClaw Discord-Worker: Timeout-Fehler dauerhaft beheben

Du kennst sicher das Problem: Dein Bot bekommt eine Anfrage über Discord, fängt an zu arbeiten, aber bevor er fertig ist, kappt der Gateway die Verbindung. Das passiert oft, wenn der Agent etwas länger zum Nachdenken braucht oder komplexe Aufgaben erledigt. Der User sieht dann nur eine Fehlermeldung, obwohl die Antwort im Hintergrund vielleicht schon fast fertig war.

Wir lösen dieses Problem jetzt grundlegend, indem wir die Verarbeitung asynchron machen. So trennen wir die schnelle Annahme des Events von der eigentlichen Ausführung des Agenten. Das sorgt für eine stabilere User Experience und gibt uns mehr Kontrolle über die Timeouts.

Wir wollen den Discord Listener Timeout als Fehlerquelle für User eliminieren, indem wir eingehende Discord-Turns asynchron verarbeiten:

  1. Der Gateway-Listener nimmt Events schnell an und normalisiert sie.
  2. Eine Discord Run-Queue speichert serialisierte Jobs, die über denselben Ordering-Key wie heute gesteuert werden.
  3. Ein Worker führt den eigentlichen Agent-Turn außerhalb der Lebensdauer des Carbon-Listeners aus.
  4. Antworten werden nach Abschluss des Runs an den ursprünglichen Channel oder Thread gesendet.

Das ist die langfristige Lösung für das Problem, dass Discord-Runs in der Queue bei channels.discord.eventQueue.listenerTimeout abbrechen, während der Agent-Run eigentlich noch Fortschritte macht.

Dieser Plan ist bereits teilweise umgesetzt.

Was bereits erledigt ist:

  • Discord Listener Timeout und Discord Run Timeout sind jetzt separate Einstellungen.
  • Angenommene Discord-Turns werden in src/discord/monitor/inbound-worker.ts eingereiht.
  • Der Worker übernimmt jetzt den langlaufenden Turn anstelle des Carbon-Listeners.
  • Das bestehende Ordering pro Route bleibt durch den Queue-Key erhalten.
  • Es gibt Testabdeckung für Timeout-Regressionen im Discord-Worker-Pfad.

Was das konkret bedeutet:

  • Der Timeout-Bug in der Produktion ist behoben.
  • Ein langlaufender Turn stirbt nicht mehr, nur weil das Zeitbudget des Discord-Listeners abläuft.
  • Die Worker-Architektur ist allerdings noch nicht ganz fertig.

Was noch fehlt:

  • DiscordInboundJob ist erst teilweise normalisiert und enthält noch Live-Referenzen zur Runtime.
  • Die Befehls-Semantik (stop, new, reset, zukünftige Session-Controls) ist noch nicht vollständig Worker-nativ.
  • Worker-Observability und der Operator-Status sind noch minimal.
  • Es gibt noch keine Persistenz bei Neustarts (Restart Durability).

Das aktuelle Verhalten koppelt den gesamten Agent-Turn an die Lebensdauer des Listeners:

  • src/discord/monitor/listeners.ts setzt die Timeout- und Abort-Grenzen.
  • src/discord/monitor/message-handler.ts hält den Turn in der Queue innerhalb dieser Grenzen.
  • src/discord/monitor/message-handler.process.ts erledigt Media-Loading, Routing, Dispatch, Typing, Draft-Streaming und die finale Antwort direkt inline.

Diese Architektur hat zwei große Nachteile:

  • Lange, aber gesunde Turns können vom Listener-Watchdog abgebrochen werden.
  • User sehen keine Antwort, selbst wenn die Runtime am Ende eine produziert hätte.

Den Timeout einfach zu erhöhen hilft zwar kurzfristig, ändert aber nichts am fehlerhaften Grundprinzip.

  • Keine Neugestaltung von Nicht-Discord-Kanälen in diesem Durchgang.
  • Den Worker in der ersten Implementierung nicht zu einem generischen Framework für alle Kanäle aufblähen.
  • Noch keine Abstraktion für einen geteilten Inbound-Worker erstellen; wir teilen Low-Level-Primitiven nur dort, wo Duplikate offensichtlich sind.
  • Keine Persistenz für Crash-Recovery im ersten Schritt, außer es ist für einen sicheren Betrieb zwingend erforderlich.
  • Keine Änderungen an der Route-Selection, Binding-Semantik oder ACP-Policy in diesem Plan.

Der aktuelle Discord-Verarbeitungspfad hängt noch von einigen Live-Objekten der Runtime ab, die nicht im langfristigen Job-Payload bleiben sollten:

  • Carbon Client
  • Rohe Discord-Event-Shapes
  • In-Memory Guild-History-Map
  • Thread-Binding-Manager Callbacks
  • Live-Typing und Draft-Stream-Status

Wir haben die Ausführung zwar schon in eine Worker-Queue verschoben, aber die Normalisierung ist noch unvollständig. Momentan läuft der Worker eher nach dem Prinzip “später im selben Prozess mit denselben Live-Objekten” statt einer echten, rein datenbasierten Job-Grenze.

DiscordMessageListener bleibt der Eingangspunkt, aber seine Aufgabe ändert sich:

  • Preflight- und Policy-Checks ausführen.
  • Den Input in einen serialisierbaren DiscordInboundJob normalisieren.
  • Den Job in eine asynchrone Queue (pro Session oder Channel) einreihen.
  • Sofort an Carbon zurückkehren, sobald das Enqueue erfolgreich war.

Der Listener sollte nicht mehr für die gesamte Lebensdauer des LLM-Turns verantwortlich sein.

Wir führen einen serialisierbaren Job-Deskriptor ein, der nur die Daten enthält, die für den späteren Run nötig sind.

Minimale Struktur:

  • Route Identity
    • agentId
    • sessionKey
    • accountId
    • channel
  • Delivery Identity
    • Destination Channel ID
    • Reply Target Message ID
    • Thread ID (falls vorhanden)
  • Sender Identity
    • Sender ID, Label, Username, Tag
  • Channel Context
    • Guild ID
    • Channel Name oder Slug
    • Thread Metadaten
    • Aufgelöster System-Prompt Override
  • Normalisierter Message Body
    • Basis-Text
    • Effektiver Nachrichtentext
    • Attachment-Deskriptoren oder aufgelöste Media-Referenzen
  • Gating-Entscheidungen
    • Ergebnis der Mention-Anforderung
    • Ergebnis der Befehls-Autorisierung
    • Gebundene Session- oder Agent-Metadaten (falls zutreffend)

Der Job-Payload darf keine Live-Carbon-Objekte oder mutable Closures enthalten.

Aktueller Status:

  • Teilweise erledigt.
  • src/discord/monitor/inbound-job.ts existiert und definiert die Übergabe an den Worker.
  • Der Payload enthält noch Live-Discord-Kontext und muss weiter reduziert werden.

Ein Discord-spezifischer Worker-Runner wird verantwortlich für:

  • Rekonstruktion des Turn-Kontexts aus dem DiscordInboundJob.
  • Laden von Medien und zusätzlichen Channel-Metadaten.
  • Dispatching des Agent-Turns.
  • Auslieferung der finalen Antwort-Payloads.
  • Aktualisierung von Status und Diagnose.

Empfohlener Ort:

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

Das Ordering muss für eine bestimmte Route identisch zum heutigen Verhalten bleiben.

Empfohlener Key:

  • Nutze dieselbe Logik wie in resolveDiscordRunQueueKey(...).

Das bewahrt das bestehende Verhalten:

  • Eine gebundene Agent-Konversation überschneidet sich nicht mit sich selbst.
  • Verschiedene Discord-Channels können trotzdem unabhängig voneinander fortschreiten.

Nach der Umstellung gibt es zwei separate Timeout-Klassen:

  • Listener Timeout
    • Deckt nur Normalisierung und Enqueue ab.
    • Sollte kurz sein.
  • Run Timeout
    • Optional, wird vom Worker verwaltet, explizit und für User sichtbar.
    • Sollte nicht versehentlich von den Carbon-Listener-Einstellungen geerbt werden.

Das löst die aktuelle Kopplung zwischen “Discord Gateway Listener blieb am Leben” und “Agent-Run ist gesund”.

  • Status: Teilweise umgesetzt.
  • Erledigt:
    • buildDiscordInboundJob(...) extrahiert.
    • Worker-Handoff-Tests hinzugefügt.
  • Offen:
    • DiscordInboundJob zu reinen Daten machen.
    • Live-Abhängigkeiten in Worker-Services verschieben statt im Job-Payload.
    • Aufhören, den Prozess-Kontext durch das Zurückmischen von Listener-Referenzen zu bauen.
  • Status: Umgesetzt.
  • Erledigt:
    • DiscordInboundWorkerQueue mit Run-Queue-Key hinzugefügt.
    • Listener reiht Jobs ein, statt direkt auf processDiscordMessage(...) zu warten.
  • Worker führt Jobs im selben Prozess (nur im Speicher) aus.

Das ist der erste funktionale Cutover.

  • Status: Noch nicht gestartet.
  • Ownership für Delivery, Typing und Draft-Streaming hinter Worker-Adapter verschieben.
  • Direkte Nutzung des Preflight-Kontexts durch Rekonstruktion im Worker ersetzen.
  • processDiscordMessage(...) temporär als Fassade behalten und dann aufteilen.
  • Status: Noch nicht gestartet. Sicherstellen, dass native Discord-Befehle korrekt funktionieren, wenn Arbeit in der Queue liegt:

  • stop

  • new

  • reset

  • Zukünftige Befehle zur Session-Steuerung.

Die Worker-Queue muss genug Status preisgeben, damit Befehle den aktiven oder wartenden Turn ansprechen können.

  • Status: Noch nicht gestartet.
  • Queue-Tiefe und Anzahl aktiver Worker im Monitor-Status anzeigen.
  • Enqueue-Zeit, Startzeit, Endzeit sowie Timeout- oder Abbruchgründe loggen.
  • Worker-Timeouts oder Delivery-Fehler klar in den Logs ausweisen.
  • Status: Noch nicht gestartet. Erst wenn die In-Memory-Version stabil läuft:

  • Entscheiden, ob Discord-Jobs einen Gateway-Restart überleben müssen.

  • Falls ja: Job-Deskriptoren und Delivery-Checkpoints persistieren.

  • Falls nein: Die In-Memory-Grenze explizit dokumentieren.

Das sollte ein separates Follow-up sein.

Aktuelle Primärdateien:

  • 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

Aktuelle Worker-Dateien:

  • 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

Wahrscheinliche nächste Kontaktpunkte:

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

Wir müssen die Worker-Grenze jetzt “echt” machen.

Das steht als Nächstes an:

  1. Live-Abhängigkeiten aus DiscordInboundJob entfernen.
  2. Diese Abhängigkeiten stattdessen in der Discord-Worker-Instanz halten.
  3. Jobs auf rein Discord-spezifische Daten reduzieren:
    • Route Identity
    • Delivery Target
    • Sender Info
    • Normalisierter Message-Snapshot
    • Gating- und Binding-Entscheidungen
  4. Den Execution-Kontext im Worker aus diesen Daten rekonstruieren.

In der Praxis bedeutet das:

  • client
  • threadBindings
  • guildHistories
  • discordRestFetch
  • Andere mutable Runtime-Handles

Diese sollten nicht mehr in jedem Job stecken, sondern im Worker selbst oder hinter entsprechenden Adaptern. Danach kümmern wir uns um die Bereinigung der Befehle stop, new und reset.

Die bestehende Timeout-Abdeckung bleibt erhalten:

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

Neue Tests für:

  1. Listener kehrt nach dem Enqueue zurück, ohne auf den Turn zu warten.
  2. Ordering pro Route bleibt erhalten.
  3. Verschiedene Channels laufen weiterhin parallel.
  4. Antworten landen am korrekten Zielort.
  5. stop bricht den aktiven Worker-Run ab.
  6. Worker-Fehler erzeugen Diagnose-Daten, ohne folgende Jobs zu blockieren.
  7. ACP-gebundene Discord-Channels routen auch unter dem Worker korrekt.
  • Risiko: Befehls-Semantik weicht vom synchronen Verhalten ab. Maßnahme: Implementierung der Befehls-Logik direkt im selben Cutover.

  • Risiko: Antwort-Zustellung verliert Thread- oder Reply-Kontext. Maßnahme: Delivery Identity als First-Class-Citizen in DiscordInboundJob behandeln.

  • Risiko: Doppelte Nachrichten bei Retries oder Queue-Restarts. Maßnahme: Erster Durchgang nur im Speicher; Idempotenz vor einer eventuellen Persistenz einbauen.

  • Risiko: message-handler.process.ts wird während der Migration unübersichtlich. Maßnahme: Aufteilung in Helfer für Normalisierung, Ausführung und Zustellung.

Der Plan ist abgeschlossen, wenn:

  1. Discord Listener Timeouts keine gesunden, langlaufenden Turns mehr abbrechen.
  2. Listener-Lifetime und Agent-Turn-Lifetime im Code getrennt sind.
  3. Das bestehende Ordering pro Session erhalten bleibt.
  4. ACP-gebundene Discord-Channels über denselben Worker-Pfad laufen.
  5. stop den Worker-Run anspricht statt den alten Call-Stack des Listeners.
  6. Timeouts und Delivery-Fehler explizite Worker-Ergebnisse sind und nicht mehr lautlos im Listener verpuffen.

Wir erledigen das in folgenden PRs:

  1. DiscordInboundJob zu reinen Daten machen und Live-Referenzen in den Worker schieben.
  2. Ownership für stop, new und reset bereinigen.
  3. Worker-Observability und Status-Anzeigen hinzufügen.
  4. Über Persistenz entscheiden oder die In-Memory-Grenze dokumentieren.

Das bleibt ein überschaubares Follow-up, solange wir uns auf Discord konzentrieren und verfrühte Abstraktionen vermeiden.

  • Schau dir die inbound-worker.ts an, um zu sehen, wie die Queue aktuell Jobs verarbeitet.
  • Prüfe die Test-Suites für die Queue, um sicherzustellen, dass deine Änderungen das Ordering nicht zerschießen.

AI Setup Assistant

OpenClaw

OpenClaw Expert

Noch festgefahren?

Wenn diese Seite nicht hilft, frage OpenClaw Expert nach Schritt-fuer-Schritt-Loesungen.