Discord Async Inbound Worker Plan

Ziel

Discord-Listener-Timeout als benutzerseitige Fehlersituation beseitigen, indem eingehende Discord-Turns asynchron werden:

  1. Gateway-Listener nimmt eingehende Events schnell an und normalisiert sie.
  2. Eine Discord-Run-Queue speichert serialisierte Jobs, geordnet nach derselben Grenze, die wir heute verwenden.
  3. Ein Worker führt den tatsächlichen Agent-Turn außerhalb der Carbon-Listener-Lebensdauer aus.
  4. 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.ts eingereiht.
  • 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:

  • DiscordInboundJob ist 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.ts wendet Timeout und Abort-Grenze an.
  • src/discord/monitor/message-handler.ts hält den queued Run innerhalb dieser Grenze.
  • src/discord/monitor/message-handler.process.ts fü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 DiscordInboundJob normalisieren
  • 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
    • agentId
    • sessionKey
    • accountId
    • channel
  • 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.ts existiert 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 DiscordInboundJob rekonstruieren
  • 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.ts
  • src/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:
    • DiscordInboundJob zu 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:
    • DiscordInboundWorkerQueue geordnet 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.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

Wahrscheinlich nächste Berührungspunkte:

  • src/auto-reply/dispatch.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/monitor/thread-bindings.ts
  • src/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:

  1. Live-Runtime-Abhängigkeiten aus DiscordInboundJob herausnehmen
  2. Diese Abhängigkeiten stattdessen auf der Discord-Worker-Instanz halten
  3. Queued Jobs auf reine Discord-spezifische Daten reduzieren:
    • Route-Identität
    • Zustellungsziel
    • Absender-Info
    • Normalisierter Nachrichten-Snapshot
    • Gating- und Binding-Entscheidungen
  4. Worker-Ausführungskontext aus diesen reinen Daten innerhalb des Workers rekonstruieren

In der Praxis bedeutet das:

  • client
  • threadBindings
  • guildHistories
  • discordRestFetch
  • 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:

  1. Listener kehrt nach Einreihen zurück ohne den vollen Turn zu awaiten
  2. Per-Route-Ordnung wird beibehalten
  3. Verschiedene Kanäle laufen weiterhin nebenläufig
  4. Antworten werden an das ursprüngliche Nachrichtenziel zugestellt
  5. stop bricht den aktiven, worker-besessenen Run ab
  6. Worker-Fehler erzeugt sichtbare Diagnose ohne spätere Jobs zu blockieren
  7. 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 DiscordInboundJob machen

  • 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.ts wird 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:

  1. Discord-Listener-Timeout gesunde, lang laufende Turns nicht mehr abbricht.
  2. Listener-Lebensdauer und Agent-Turn-Lebensdauer separate Konzepte im Code sind.
  3. Die bestehende Per-Session-Ordnung beibehalten wird.
  4. ACP-gebundene Discord-Kanäle über denselben Worker-Pfad funktionieren.
  5. stop den worker-besessenen Run ansteuert statt den alten listener-besessenen Call-Stack.
  6. Timeout- und Zustellungsfehler explizite Worker-Ergebnisse werden, nicht stille Listener-Drops.

Verbleibende Landing-Strategie

In Follow-up-PRs fertigstellen:

  1. DiscordInboundJob zu reinen Daten machen und Live-Runtime-Refs auf den Worker verschieben
  2. Command-State-Ownership für stop, new und reset bereinigen
  3. Worker-Beobachtbarkeit und Operator-Status hinzufügen
  4. 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.