Discord Async Inbound Worker Plan
Ziel
Discord-Listener-Timeout als benutzerseitige Fehlersituation beseitigen, indem eingehende Discord-Turns asynchron werden:
- Gateway-Listener nimmt eingehende Events schnell an und normalisiert sie.
- Eine Discord-Run-Queue speichert serialisierte Jobs, geordnet nach derselben Grenze, die wir heute verwenden.
- Ein Worker führt den tatsächlichen Agent-Turn außerhalb der Carbon-Listener-Lebensdauer aus.
- Antworten werden nach Abschluss des Runs an den ursprünglichen Kanal oder Thread zurückgeliefert.
Dies ist der langfristige Fix für Discord-Runs in der Queue, die bei channels.discord.eventQueue.listenerTimeout auslaufen, während der Agent-Run selbst noch Fortschritte macht.
Aktueller Status
Dieser Plan ist teilweise implementiert.
Bereits erledigt:
- Discord-Listener-Timeout und Discord-Run-Timeout sind jetzt separate Einstellungen.
- Angenommene eingehende Discord-Turns werden in
src/discord/monitor/inbound-worker.tseingereiht. - Der Worker besitzt jetzt den lang laufenden Turn anstelle des Carbon-Listeners.
- Die bestehende Per-Route-Ordnung wird durch den Queue-Key beibehalten.
- Timeout-Regressions-Coverage existiert für den Discord-Worker-Pfad.
Was das in einfacher Sprache bedeutet:
- der Produktions-Timeout-Bug ist behoben
- der lang laufende Turn stirbt nicht mehr nur weil das Discord-Listener-Budget abläuft
- die Worker-Architektur ist noch nicht fertig
Was noch fehlt:
DiscordInboundJobist noch nur teilweise normalisiert und enthält noch Live-Runtime-Referenzen- Befehlssemantiken (
stop,new,reset, zukünftige Session-Controls) sind noch nicht vollständig worker-nativ - Worker-Beobachtbarkeit und Operator-Status sind noch minimal
- es gibt noch keine Restart-Dauerhaftigkeit
Warum das existiert
Das aktuelle Verhalten bindet den vollständigen Agent-Turn an die Listener-Lebensdauer:
src/discord/monitor/listeners.tswendet Timeout und Abort-Grenze an.src/discord/monitor/message-handler.tshält den queued Run innerhalb dieser Grenze.src/discord/monitor/message-handler.process.tsführt Medien-Laden, Routing, Dispatch, Typing, Draft-Streaming und finale Reply-Zustellung inline aus.
Diese Architektur hat zwei problematische Eigenschaften:
- Lange, aber gesunde Turns können durch den Listener-Watchdog abgebrochen werden
- Benutzer sehen keine Antwort, obwohl die nachgelagerte Runtime eine erzeugt hätte
Das Timeout zu erhöhen hilft, ändert aber nicht den Fehlermodus.
Nicht-Ziele
- Nicht-Discord-Kanäle in diesem Durchgang nicht umgestalten.
- Dies nicht zu einem generischen All-Channel-Worker-Framework in der ersten Implementierung ausbauen.
- Noch keine gemeinsame kanalübergreifende Inbound-Worker-Abstraktion extrahieren; nur Low-Level-Primitive teilen wenn Duplikation offensichtlich ist.
- Keine dauerhafte Crash-Recovery im ersten Durchgang hinzufügen, es sei denn zum sicheren Landen nötig.
- Route-Auswahl, Binding-Semantik oder ACP-Policy in diesem Plan nicht ändern.
Aktuelle Einschränkungen
Der aktuelle Discord-Verarbeitungspfad hängt noch von einigen Live-Runtime-Objekten ab, die nicht in der langfristigen Job-Payload bleiben sollten:
- Carbon
Client - Rohe Discord-Event-Formen
- In-Memory-Guild-History-Map
- Thread-Binding-Manager-Callbacks
- Live-Typing- und Draft-Stream-Zustand
Wir haben die Ausführung bereits auf eine Worker-Queue verschoben, aber die Normalisierungsgrenze ist noch unvollständig. Aktuell ist der Worker „führe später im selben Prozess mit einigen der gleichen Live-Objekte aus”, keine vollständig datenbasierte Job-Grenze.
Zielarchitektur
1. Listener-Phase
DiscordMessageListener bleibt der Eingangspunkt, aber seine Aufgabe wird:
- Preflight- und Policy-Prüfungen durchführen
- Akzeptierte Eingabe in einen serialisierbaren
DiscordInboundJobnormalisieren - Den Job in eine Per-Session- oder Per-Channel-Async-Queue einreihen
- Sofort zu Carbon zurückkehren sobald das Einreihen erfolgreich ist
Der Listener soll die durchgängige LLM-Turn-Lebensdauer nicht mehr besitzen.
2. Normalisiertes Job-Payload
Einen serialisierbaren Job-Deskriptor einführen, der nur die zum späteren Ausführen des Turns nötigen Daten enthält.
Minimale Form:
- Route-Identität
agentIdsessionKeyaccountIdchannel
- Zustellungs-Identität
- Ziel-Kanal-ID
- Reply-Ziel-Nachrichten-ID
- Thread-ID falls vorhanden
- Absender-Identität
- Absender-ID, Label, Username, Tag
- Kanal-Kontext
- Guild-ID
- Kanal-Name oder Slug
- Thread-Metadaten
- Aufgelöster System-Prompt-Override
- Normalisierter Nachrichteninhalt
- Basistext
- Effektiver Nachrichtentext
- Anhangsdeskriptoren oder aufgelöste Medien-Referenzen
- Gating-Entscheidungen
- Mention-Requirement-Ergebnis
- Befehlsautorisierungs-Ergebnis
- Gebundene Session- oder Agent-Metadaten falls zutreffend
Das Job-Payload darf keine Live-Carbon-Objekte oder veränderliche Closures enthalten.
Aktueller Implementierungsstatus:
- teilweise erledigt
src/discord/monitor/inbound-job.tsexistiert und definiert die Worker-Übergabe- das Payload enthält noch Live-Discord-Runtime-Kontext und sollte weiter reduziert werden
3. Worker-Phase
Einen Discord-spezifischen Worker-Runner hinzufügen, verantwortlich für:
- Turn-Kontext aus
DiscordInboundJobrekonstruieren - Medien und zusätzliche Kanal-Metadaten für den Run laden
- Agent-Turn dispatchen
- Finale Reply-Payloads zustellen
- Status und Diagnose aktualisieren
Empfohlener Speicherort:
src/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.ts
4. Ordnungsmodell
Die Ordnung muss für eine gegebene Route-Grenze dem heutigen Verhalten entsprechen.
Empfohlener Schlüssel:
- Dieselbe Queue-Key-Logik wie
resolveDiscordRunQueueKey(...)verwenden
Dies bewahrt bestehendes Verhalten:
- Eine gebundene Agent-Konversation interleaved nicht mit sich selbst
- Verschiedene Discord-Kanäle können weiterhin unabhängig fortschreiten
5. Timeout-Modell
Nach der Umstellung gibt es zwei separate Timeout-Klassen:
- Listener-Timeout
- deckt nur Normalisierung und Einreihung ab
- sollte kurz sein
- Run-Timeout
- optional, worker-besessen, explizit und benutzersichtbar
- sollte nicht versehentlich von Carbon-Listener-Einstellungen geerbt werden
Dies entfernt die aktuelle versehentliche Kopplung zwischen „Discord-Gateway-Listener blieb am Leben” und „Agent-Run ist gesund.”
Empfohlene Implementierungsphasen
Phase 1: Normalisierungsgrenze
- Status: teilweise implementiert
- Erledigt:
buildDiscordInboundJob(...)extrahiert- Worker-Übergabetests hinzugefügt
- Verbleibend:
DiscordInboundJobzu reinen Daten machen- Live-Runtime-Abhängigkeiten in worker-besessene Services verschieben statt in Per-Job-Payload
- Stoppen, Prozesskontext durch Zusammenfügen von Live-Listener-Refs zurück in den Job zu rekonstruieren
Phase 2: In-Memory-Worker-Queue
- Status: implementiert
- Erledigt:
DiscordInboundWorkerQueuegeordnet nach aufgelöstem Run-Queue-Key hinzugefügt- Listener reiht Jobs ein statt direkt
processDiscordMessage(...)zu awaiten - Worker führt Jobs in-process, nur im Speicher aus
Dies ist die erste funktionale Umstellung.
Phase 3: Prozess-Aufspaltung
- Status: nicht begonnen
- Zustellungs-, Typing- und Draft-Streaming-Ownership hinter worker-seitige Adapter verschieben.
- Direkte Nutzung von Live-Preflight-Kontext durch Worker-Kontext-Rekonstruktion ersetzen.
processDiscordMessage(...)temporär als Fassade beibehalten wenn nötig, dann aufsplitten.
Phase 4: Befehlssemantiken
-
Status: nicht begonnen Sicherstellen, dass native Discord-Befehle korrekt funktionieren wenn Arbeit queued ist:
-
stop -
new -
reset -
alle zukünftigen Session-Control-Befehle
Die Worker-Queue muss genug Run-Zustand bereitstellen, damit Befehle den aktiven oder queued Turn ansteuern können.
Phase 5: Beobachtbarkeit und Operator-UX
- Status: nicht begonnen
- Queue-Tiefe und aktive Worker-Zähler in den Monitor-Status emittieren
- Einreihungszeit, Startzeit, Endzeit und Timeout- oder Abbruchgrund aufzeichnen
- Worker-besessene Timeout- oder Zustellungsfehler klar in Logs sichtbar machen
Phase 6: Optionaler Dauerhaftigkeits-Follow-up
-
Status: nicht begonnen Erst wenn die In-Memory-Version stabil ist:
-
entscheiden ob queued Discord-Jobs Gateway-Neustarts überleben sollen
-
wenn ja, Job-Deskriptoren und Zustellungs-Checkpoints persistieren
-
wenn nein, die explizite In-Memory-Grenze dokumentieren
Dies sollte ein separater Follow-up sein, es sei denn Restart-Recovery wird zum Landen benötigt.
Datei-Impact
Aktuelle primäre Dateien:
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
Aktuelle Worker-Dateien:
src/discord/monitor/inbound-job.tssrc/discord/monitor/inbound-worker.tssrc/discord/monitor/inbound-job.test.tssrc/discord/monitor/message-handler.queue.test.ts
Wahrscheinlich nächste Berührungspunkte:
src/auto-reply/dispatch.tssrc/discord/monitor/reply-delivery.tssrc/discord/monitor/thread-bindings.tssrc/discord/monitor/native-command.ts
Nächster Schritt jetzt
Der nächste Schritt ist, die Worker-Grenze real statt partiell zu machen.
Als Nächstes tun:
- Live-Runtime-Abhängigkeiten aus
DiscordInboundJobherausnehmen - Diese Abhängigkeiten stattdessen auf der Discord-Worker-Instanz halten
- Queued Jobs auf reine Discord-spezifische Daten reduzieren:
- Route-Identität
- Zustellungsziel
- Absender-Info
- Normalisierter Nachrichten-Snapshot
- Gating- und Binding-Entscheidungen
- Worker-Ausführungskontext aus diesen reinen Daten innerhalb des Workers rekonstruieren
In der Praxis bedeutet das:
clientthreadBindingsguildHistoriesdiscordRestFetch- andere veränderliche runtime-only Handles
sollten nicht mehr auf jedem queued Job leben, sondern auf dem Worker selbst oder hinter worker-besessenen Adaptern.
Nachdem das gelandet ist, sollte der nächste Follow-up das Command-State-Cleanup für stop, new und reset sein.
Testplan
Die bestehende Timeout-Repro-Coverage beibehalten in:
src/discord/monitor/message-handler.queue.test.ts
Neue Tests hinzufügen für:
- Listener kehrt nach Einreihen zurück ohne den vollen Turn zu awaiten
- Per-Route-Ordnung wird beibehalten
- Verschiedene Kanäle laufen weiterhin nebenläufig
- Antworten werden an das ursprüngliche Nachrichtenziel zugestellt
stopbricht den aktiven, worker-besessenen Run ab- Worker-Fehler erzeugt sichtbare Diagnose ohne spätere Jobs zu blockieren
- ACP-gebundene Discord-Kanäle routen korrekt unter Worker-Ausführung
Risiken und Gegenmaßnahmen
-
Risiko: Befehlssemantiken driften vom aktuellen synchronen Verhalten ab Gegenmaßnahme: Command-State-Plumbing im selben Umstellungs-Durchgang landen, nicht später
-
Risiko: Reply-Zustellung verliert Thread- oder Reply-To-Kontext Gegenmaßnahme: Zustellungsidentität als erstklassig in
DiscordInboundJobmachen -
Risiko: Doppelte Sends bei Retries oder Queue-Neustarts Gegenmaßnahme: Ersten Durchgang rein in-memory halten oder explizite Zustellungs-Idempotenz vor Persistenz hinzufügen
-
Risiko:
message-handler.process.tswird während der Migration schwerer nachvollziehbar Gegenmaßnahme: In Normalisierungs-, Ausführungs- und Zustellungs-Helper aufsplitten vor oder während der Worker-Umstellung
Akzeptanzkriterien
Der Plan ist abgeschlossen wenn:
- Discord-Listener-Timeout gesunde, lang laufende Turns nicht mehr abbricht.
- Listener-Lebensdauer und Agent-Turn-Lebensdauer separate Konzepte im Code sind.
- Die bestehende Per-Session-Ordnung beibehalten wird.
- ACP-gebundene Discord-Kanäle über denselben Worker-Pfad funktionieren.
stopden worker-besessenen Run ansteuert statt den alten listener-besessenen Call-Stack.- Timeout- und Zustellungsfehler explizite Worker-Ergebnisse werden, nicht stille Listener-Drops.
Verbleibende Landing-Strategie
In Follow-up-PRs fertigstellen:
DiscordInboundJobzu reinen Daten machen und Live-Runtime-Refs auf den Worker verschieben- Command-State-Ownership für
stop,newundresetbereinigen - Worker-Beobachtbarkeit und Operator-Status hinzufügen
- Entscheiden ob Dauerhaftigkeit nötig ist oder die In-Memory-Grenze explizit dokumentieren
Dies bleibt ein begrenzter Follow-up, wenn er Discord-only bleibt und wir weiterhin eine voreilige kanalübergreifende Worker-Abstraktion vermeiden.