Messages and delivery
Refaktorierung des Nachrichtenlebenszyklus
Warum dieses Refactoring erfolgte
Der Kanal-Stack entstand aus mehreren lokalen Korrekturen: separate Hilfsfunktionen für eingehende Nachrichten je
Reifegrad (runtime.channel.inbound.run für einfache Adapter,
runtime.channel.inbound.runPreparedReply für umfangreiche Adapter), veraltete Hilfsfunktionen für die Antwortweiterleitung
(dispatchInboundReplyWithBase, recordInboundSessionAndDispatchReply),
kanalspezifisches Vorschau-Streaming und nachträglich in
bestehende Antwort-Payload-Pfade integrierte Dauerhaftigkeit der endgültigen Zustellung. Diese Struktur führte zu zu vielen öffentlichen Konzepten und
zu vielen Stellen, an denen die Zustellungssemantik auseinanderdriften konnte.
Die Zuverlässigkeitslücke, die den Neuentwurf erforderlich machte:
Telegram-Polling-Aktualisierung bestätigt -> endgültiger Assistententext ist vorhanden -> Prozess startet neu, bevor sendMessage erfolgreich ausgeführt wird -> endgültige Antwort geht verlorenZielinvariante: Sobald der Kern entscheidet, dass eine sichtbare ausgehende Nachricht vorhanden sein soll, muss die Sendeabsicht dauerhaft gespeichert werden, bevor der Plattformaufruf versucht wird, und der Plattformbeleg muss nach erfolgreicher Ausführung festgeschrieben werden. Dadurch wird standardmäßig eine Wiederherstellung mit mindestens einmaliger Zustellung ermöglicht. Ein Genau-einmal-Verhalten besteht nur dort, wo ein Adapter native Idempotenz nachweist oder einen nach dem Senden unbekannten Versuch vor der Wiederholung mit dem Plattformstatus abgleicht.
Was ausgeliefert wurde
Die interne Domäne befindet sich in src/channels/message/*:
| Datei | Zuständigkeit |
|---|---|
types.ts |
Typverträge für Adapter, Sendekontext, Beleg und dauerhafte Absicht |
send.ts |
withDurableMessageSendContext / sendDurableMessageBatch — der dauerhafte Sendekontext |
receive.ts |
createMessageReceiveContext — Zustandsautomat für die Bestätigungsrichtlinie eingehender Nachrichten |
live.ts |
Status der Live-Vorschau und Logik zum Abschließen an Ort und Stelle oder zum Ausweichen |
state.ts |
classifyDurableSendRecoveryState — Klassifizierung der Wiederherstellung nach einer Unterbrechung |
receipt.ts |
Normalisiert Ergebnisse des Plattformversands zu MessageReceipt |
capabilities.ts |
Leitet die erforderlichen Fähigkeiten für die dauerhafte endgültige Zustellung aus einem Payload ab |
contracts.ts |
Überprüfung des Vertragsnachweises für deklarierte Adapterfähigkeiten |
adapter.ts |
defineChannelMessageAdapter |
outbound-bridge.ts |
createChannelMessageAdapterFromOutbound — umschließt veraltete Funktionen sendText/sendMedia/sendPayload/sendPoll |
ingress-queue.ts |
createChannelIngressQueue — dauerhafte Warteschlange für eingehende Ereignisse |
durable-receive.ts |
createDurableInboundReceiveJournal — Journal für Annehmen/Ausstehend/Abschließen/Freigeben zur Deduplizierung eingehender Nachrichten |
inbound-reply-dispatch.ts |
dispatchChannelInboundReply und Wrapper mit veralteten Namen |
reply-pipeline.ts |
createChannelReplyPipeline, Hilfsfunktionen für Antwortpräfixe und Eingabe-Rückrufe |
Öffentliche Oberfläche: openclaw/plugin-sdk/channel-outbound (Hilfsfunktionen für Versand/Beleg/Dauerhaftigkeit/Live-Vorschau/Antwort-Pipeline)
und openclaw/plugin-sdk/channel-inbound (Kontext für eingehende Nachrichten, runChannelInboundEvent,
dispatchChannelInboundReply). Adapterbeispiele, aktuelle
Typnamen und Migrationshinweise finden Sie auf diesen Seiten — sie sind die maßgebliche Quelle für die API-
Struktur, nicht die nachfolgenden Entwürfe.
Sendekontext
withDurableMessageSendContext stellt Kanalcode die Schritte render, previewUpdate,
send, edit, delete, commit und fail für eine ausgehende
Nachricht bereit. sendDurableMessageBatch ist der Wrapper für den Regelfall: rendern, senden
und anschließend bei sent/suppressed festschreiben oder bei einem Fehler als fehlgeschlagen markieren.
sendDurableMessageBatch gibt genau ein diskriminiertes Ergebnis zurück:
| Status | Bedeutung |
|---|---|
sent |
Mindestens eine sichtbare Plattformnachricht wurde zugestellt |
suppressed |
Keine Plattformnachricht soll als fehlend gelten (durch Hook abgebrochen, Testlauf usw.) |
partial_failed |
Mindestens eine Nachricht wurde zugestellt, bevor ein späteres Payload oder ein Nebeneffekt fehlschlug |
failed |
Es wurde kein Plattformbeleg erzeugt |
Die Dauerhaftigkeit ist entweder required, best_effort oder disabled
(MessageDurabilityPolicy in src/channels/message/types.ts). required
schlägt sicher fehl, wenn die dauerhafte Absicht nicht geschrieben werden kann; best_effort weicht
auf einen direkten Versand aus, wenn keine Persistenz verfügbar ist; disabled behält das
Verhalten des direkten Versands vor dem Refactoring bei. Hilfsfunktionen für veraltete Kompatibilität verwenden standardmäßig
disabled und leiten nicht allein deshalb required ab, weil ein Kanal über einen generischen
Adapter für ausgehende Nachrichten verfügt.
Die weiterhin gefährliche Grenze liegt nach dem erfolgreichen Plattformaufruf und vor dem
Festschreiben des Belegs. Wenn der Prozess an dieser Stelle beendet wird, kann der Kern nicht wissen, ob die
Plattformnachricht vorhanden ist, sofern der Adapter nicht reconcileUnknownSend deklariert.
Dieser Hook klassifiziert einen unterbrochenen Versand als sent, not_sent oder
unresolved; nur not_sent gestattet eine Wiederholung. Kanäle ohne Abgleich
fallen auf den Status unknown_after_send zurück (src/channels/message/state.ts,
src/infra/outbound/delivery-queue-recovery.ts) und können sich nur dann für eine Wiederholung mit
mindestens einmaliger Zustellung entscheiden, wenn doppelte sichtbare Nachrichten für diesen Kanal einen akzeptablen, dokumentierten
Kompromiss darstellen.
Empfangskontext
createMessageReceiveContext verfolgt den Bestätigungs-/Ablehnungsstatus für jedes eingehende Ereignis mit einem
idempotenten ack() und einem expliziten nack(error). Die Bestätigungsrichtlinie
(ChannelMessageReceiveAckPolicy) ist eine der folgenden:
| Richtlinie | Bestätigt, wenn |
|---|---|
after_receive_record |
Der Kern genügend Metadaten der eingehenden Nachricht für Deduplizierung/Weiterleitung einer erneuten Zustellung persistiert hat |
after_agent_dispatch |
Der Agentenlauf weitergeleitet wurde |
after_durable_send |
Der dauerhafte ausgehende Versand für diesen Durchlauf festgeschrieben wurde |
manual |
Der Aufrufer den Bestätigungszeitpunkt explizit steuert (Standard für Adapter, die keine Richtlinie deklarieren) |
Telegram-Polling verwendet dies, um eine sicher abgeschlossene Aktualisierungsmarke zu persistieren
(safeCompletedUpdateId in extensions/telegram/src/bot-update-tracker.ts):
grammY erfasst weiterhin jede Aktualisierung beim Eintritt in die Middleware-Kette, aber
OpenClaw verschiebt die persistierte Neustartmarke nur über Aktualisierungen hinaus, deren
Weiterleitung abgeschlossen wurde, sodass fehlgeschlagene oder noch ausstehende Aktualisierungen nach einem Neustart erneut verarbeitet werden.
Der vorgelagerte getUpdates-Offset von Telegram bleibt weiterhin in der Zuständigkeit von grammY; eine vollständig
dauerhafte Polling-Quelle, die die erneute Zustellung auf Plattformebene über diese
Marke hinaus steuert, wurde nicht implementiert (siehe Offene Fragen).
Live-Vorschau
src/channels/message/live.ts modelliert Vorschau/Bearbeitung/Abschluss als einen Lebenszyklus:
createLiveMessageState, markLiveMessagePreviewUpdated,
markLiveMessageFinalized, markLiveMessageCancelled und
deliverFinalizableLivePreviewAdapter (eine endgültige Bearbeitung aus einem Entwurf erstellen, sie anwenden
und auf einen normalen Versand ausweichen, wenn die Bearbeitung nicht möglich ist oder fehlschlägt).
LiveMessageState.phase ist idle | previewing | finalizing | finalized | cancelled; canFinalizeInPlace steuert, ob eine Vorschau durch Bearbeitung anstelle eines neuen
Versands zur endgültigen Nachricht werden kann.
Dauerhafte Belege
MessageReceipt (src/channels/message/types.ts) normalisiert eine oder mehrere
Plattformnachrichten-IDs eines einzelnen logischen Versands in platformMessageIds sowie
parts pro Teil (Art, Index, Thread-ID, Antwort-auf-ID). Eine primäre ID wird
für Threads und spätere Bearbeitungen beibehalten. Dadurch können mehrteilige Zustellungen (Text
plus Medien, aufgeteilter Text, Karten-Ausweichlösung) nach einem Neustart wiederholt und dedupliziert werden.
Reduzierung des öffentlichen SDK
Das Refactoring übernahm oder verwarf: als öffentliche
API bereitgestellte Hilfsfunktionen reply-runtime, reply-dispatch-runtime,
reply-reference, reply-chunking, reply-payload,
inbound-reply-dispatch, channel-reply-pipeline und die meisten öffentlichen Verwendungen
von outbound-runtime. src/plugin-sdk/channel-message.ts ist jetzt ein
@deprecated-Reexport-Barrel, das auf channel-outbound /
channel-inbound verweist; Laufzeit-Aliasse für channel.turn wurden entfernt und die alte
Dokumentationsseite /plugins/sdk-channel-turn leitet zur
API für eingehende Kanäle weiter. Neuer Plugin-Code sollte
direkt auf channel-outbound und channel-inbound abzielen.
Abweichungen der Implementierung vom ursprünglichen Design
Der nachfolgende Designentwurf wurde nie wortgetreu ausgeliefert. Er wird aus Gründen der historischen Genauigkeit aufbewahrt; behandeln Sie diese Typnamen nicht als aktuelle API.
- Kein
MessageOrigin/shouldDropOpenClawEcho. Der ursprüngliche Plan sah ein Herkunfts-Tagsource: "openclaw"für Gateway-Fehlermeldungen sowie ein gemeinsames Prädikat vor, das markierte, von Bots verfasste Echos in gemeinsam genutzten Räumen vor derallowBots-Autorisierung verwirft. Dieser Typ und dieses Prädikat sind in der Codebasis nicht vorhanden.allowBotsselbst ist ein echter kanalspezifischer Konfigurationsschlüssel (Slack, Discord, Google Chat und weitere), aber der zu seinem Schutz vorgesehene Mechanismus zur Herkunftsmarkierung wurde nie implementiert. Die Unterdrückung von Echos bei Gateway-Fehlern in Räumen mit aktivierten Bots bleibt eine offene Lücke und ist keine ausgelieferte Garantie. - Kein einheitlicher Namespace
core.messages.receive/send/live/state. Die ausgelieferten Funktionen befinden sich direkt insrc/channels/message/*(withDurableMessageSendContext,createMessageReceiveContext,createLiveMessageState,classifyDurableSendRecoveryState) und nicht hinter einercore.messages.*-Fassade. - Kein generischer normalisierter Nachrichtentyp
ChannelMessage/MessageTarget/MessageRelation. Der Kern übergibt weiterhin konkrete Antwort-Payloads (ReplyPayload) und kanalspezifische Kontexte über die Sendeadapter, anstatt eine einzige plattformneutrale Nachrichtenstruktur mit einer Relationkind: "reply" | "followup" | "broadcast" | "system"zu verwenden. - Die Namen der Bestätigungsrichtlinien unterscheiden sich vom Entwurf. Ausgeliefert:
after_receive_record | after_agent_dispatch | after_durable_send | manual. Der ursprüngliche Entwurf verwendeteimmediate | after-record | after-durable-send | manualmit einem Feld für den Grund einer Webhook-Zeitüberschreitung; diese Struktur wurde nicht implementiert. - Fähigkeitsschlüssel von
DurableFinalDeliveryRequirementMapersetzten das entworfeneMessageCapabilities-Objekt. Fähigkeiten sind flache boolesche Flags (text,media,poll,payload,silent,replyTo,thread,nativeQuote,messageSendingHooks,batch,reconcileUnknownSend,afterSendSuccess,afterCommit), die überverifyDurableFinalCapabilityProofsüberprüft werden, anstatt eine verschachtelte Struktur im Stil vontext.chunking/attachments.voicezu verwenden.
Konkrete Migrationsrisiken (weiterhin relevant)
Diese kanalspezifischen Nebeneffekte existierten bereits vor dem Refactoring und müssen auch über die neuen Sendepfade weiterhin funktionieren. Sie sind nicht hypothetisch: Jeder einzelne ist derzeit implementiert und für den Betrieb unverzichtbar.
- iMessage (
extensions/imessage/src/monitor/echo-cache.ts,persisted-echo-cache.ts): Der Monitor erfasst gesendete Nachrichten nach einem erfolgreichen Sendevorgang in einem Echo-Cache. Dauerhaft persistierte finale Sendevorgänge müssen diesen Cache weiterhin befüllen, da OpenClaw andernfalls eigene Antworten erneut als eingehende Benutzernachrichten aufnehmen kann. - Tlon (
extensions/tlon/src/monitor/index.ts): Fügt eine optionale Modellsignatur an und erfasst nach Gruppenantworten die Threads, an denen eine Beteiligung erfolgte. Eine dauerhafte Zustellung darf diese Effekte nicht umgehen. - Discord und andere vorbereitete Dispatcher steuern bereits die direkte Zustellung und das Vorschauverhalten. Ein Kanal ist erst dann durchgängig dauerhaft, wenn sein vorbereiteter Dispatcher finale Nachrichten ausdrücklich über den Sendekontext leitet. Gehen Sie nicht davon aus, dass der generische Adapter allein dies abdeckt.
- Die stille Fallback-Zustellung von Telegram muss nach der Aufteilung und Fallback-Projektion das gesamte Array der projizierten Nutzdaten zustellen, nicht nur die ersten Nutzdaten.
- LINE, Zalo, Nostr und ähnliche Hilfspfade können die Verarbeitung von Antwort-Tokens, Medien-Proxying, Caches gesendeter Nachrichten oder ausschließlich per Callback erreichbare Ziele umfassen. Sie verbleiben bei der kanaleigenen Zustellung, bis diese Semantik durch den Sendeadapter abgebildet und durch Tests abgedeckt ist.
- Hilfsfunktionen für direkte DMs können über einen Antwort-Callback verfügen, der das einzig korrekte Transportziel darstellt. Der generische ausgehende Versand darf kein Ziel anhand unverarbeiteter Plattformfelder erraten und diesen Callback überspringen.
Fehlerklassifizierung
Adapter klassifizieren Transportfehler in geschlossene Kategorien nach Art von
DeliveryFailureKind (vorübergehend, Ratenbegrenzung, Authentifizierung,
Berechtigung, nicht gefunden, ungültige Nutzdaten, Konflikt, abgebrochen,
unbekannt). Kernrichtlinie:
- Wiederholen Sie Vorgänge bei vorübergehenden Fehlern und Ratenbegrenzungsfehlern.
- Wiederholen Sie Vorgänge bei ungültigen Nutzdaten nur, wenn ein Rendering-Fallback vorhanden ist.
- Wiederholen Sie Vorgänge bei Authentifizierungs- oder Berechtigungsfehlern erst, nachdem die Konfiguration geändert wurde.
- Bei „nicht gefunden“ darf die Live-Finalisierung von der Bearbeitung auf einen neuen Sendevorgang zurückfallen, wenn der Kanal dies als sicher deklariert.
- Verwenden Sie bei einem Konflikt den Empfangsbestätigungs- und Idempotenzstatus, um festzustellen, ob die Nachricht bereits vorhanden ist.
- Jeder Fehler, der auftritt, nachdem der Plattformaufruf möglicherweise erfolgreich
war, aber bevor die Empfangsbestätigung gespeichert wurde, wird zu
unknown_after_send, sofern der Adapter nicht nachweist, dass der Plattformvorgang nicht stattgefunden hat.
Offene Fragen
- Ob Telegram den Polling-Runner von grammY (
1.43.0) letztendlich durch eine vollständig dauerhafte Polling-Quelle ersetzen sollte, die die erneute Zustellung auf Plattformebene steuert und nicht nur OpenClaws persistente Neustartmarke (safeCompletedUpdateId). - Ob der Live-Vorschaustatus im selben Datensatz wie die finale Sendeabsicht oder in einem separaten Speicher für Live-Status enthalten sein sollte.
- Ob die Echo-Unterdrückung bei Gateway-Fehlern in gemeinsam genutzten Räumen mit aktivierten Bots den ursprünglich geplanten Mechanismus zur Ursprungsmarkierung benötigt, einen einfacheren kanalspezifischen Vertrag erfordert oder außerhalb des Umfangs liegt.
- Welche Kanäle native Unterstützung für Ursprungsangaben oder Metadaten zur botübergreifenden Echo-Unterdrückung bieten und welche stattdessen eine persistente Registrierung ausgehender Nachrichten benötigen.