Messages and delivery
Refactoring del ciclo di vita dei messaggi
Perché è stato effettuato questo refactoring
Lo stack dei canali è cresciuto a partire da diverse correzioni locali: helper in ingresso separati per
livello di maturità (runtime.channel.inbound.run per gli adattatori semplici,
runtime.channel.inbound.runPreparedReply per quelli avanzati), helper legacy per l'inoltro delle risposte
(dispatchInboundReplyWithBase, recordInboundSessionAndDispatchReply),
streaming dell'anteprima specifico per canale e persistenza della consegna finale aggiunta
ai percorsi esistenti dei payload di risposta. Questa struttura ha prodotto troppi concetti pubblici e
troppi punti in cui la semantica di consegna poteva divergere.
La lacuna di affidabilità che ha imposto la riprogettazione:
Aggiornamento del polling di Telegram confermato -> il testo finale dell'assistente esiste -> il processo si riavvia prima che sendMessage abbia esito positivo -> la risposta finale viene persaInvariante obiettivo: quando il core stabilisce che debba esistere un messaggio in uscita visibile, l'intento di invio deve essere persistente prima di tentare la chiamata alla piattaforma e la ricevuta della piattaforma deve essere registrata dopo l'esito positivo. Ciò garantisce per impostazione predefinita il ripristino con almeno una consegna. Il comportamento con una sola consegna esiste soltanto quando un adattatore dimostra l'idempotenza nativa o riconcilia un tentativo dall'esito ignoto dopo l'invio con lo stato della piattaforma prima della ripetizione.
Cosa è stato distribuito
Il dominio interno si trova in src/channels/message/*:
| File | Responsabilità |
|---|---|
types.ts |
Contratti dei tipi per adattatore, contesto di invio, ricevuta e intento persistente |
send.ts |
withDurableMessageSendContext / sendDurableMessageBatch — il contesto di invio persistente |
receive.ts |
createMessageReceiveContext — macchina a stati dei criteri di conferma in ingresso |
live.ts |
Stato dell'anteprima in tempo reale e logica di finalizzazione sul posto o ripiego |
state.ts |
classifyDurableSendRecoveryState — classificazione del ripristino dopo un'interruzione |
receipt.ts |
Normalizza i risultati di invio della piattaforma in MessageReceipt |
capabilities.ts |
Deriva da un payload le funzionalità finali persistenti richieste |
contracts.ts |
Verifica delle prove contrattuali per le funzionalità dichiarate dell'adattatore |
adapter.ts |
defineChannelMessageAdapter |
outbound-bridge.ts |
createChannelMessageAdapterFromOutbound — racchiude le funzioni legacy sendText/sendMedia/sendPayload/sendPoll |
ingress-queue.ts |
createChannelIngressQueue — coda persistente degli eventi in ingresso |
durable-receive.ts |
createDurableInboundReceiveJournal — registro di accettazione/in sospeso/completamento/rilascio per la deduplicazione in ingresso |
inbound-reply-dispatch.ts |
dispatchChannelInboundReply e wrapper con nomi legacy |
reply-pipeline.ts |
createChannelReplyPipeline, helper per il prefisso di risposta e le callback di digitazione |
Superficie pubblica: openclaw/plugin-sdk/channel-outbound (helper per invio/ricevuta/persistenza/tempo reale/pipeline di risposta)
e openclaw/plugin-sdk/channel-inbound (contesto in ingresso, runChannelInboundEvent,
dispatchChannelInboundReply). Consulta tali pagine per esempi di adattatori, nomi dei tipi correnti
e note sulla migrazione: costituiscono la fonte autorevole per la struttura
dell'API, non le bozze riportate di seguito.
Contesto di invio
withDurableMessageSendContext fornisce al codice del canale i passaggi render, previewUpdate,
send, edit, delete, commit e fail relativi a un singolo messaggio
in uscita. sendDurableMessageBatch è il wrapper per il caso comune: esegue il rendering, invia,
quindi registra per sent/suppressed o segnala un errore in caso di esito negativo.
sendDurableMessageBatch restituisce un unico risultato discriminato:
| Stato | Significato |
|---|---|
sent |
È stato consegnato almeno un messaggio visibile sulla piattaforma |
suppressed |
Nessun messaggio della piattaforma deve essere considerato mancante (annullato da un hook, esecuzione simulata ecc.) |
partial_failed |
Almeno un messaggio è stato consegnato prima dell'errore di un payload o effetto collaterale successivo |
failed |
Non è stata prodotta alcuna ricevuta della piattaforma |
La persistenza può essere required, best_effort o disabled
(MessageDurabilityPolicy in src/channels/message/types.ts). required
si interrompe in modo sicuro quando non è possibile scrivere l'intento persistente; best_effort ricorre
a un invio diretto quando la persistenza non è disponibile; disabled mantiene il
comportamento di invio diretto precedente al refactoring. Gli helper di compatibilità legacy usano per impostazione predefinita
disabled e non deducono required semplicemente perché un canale dispone di un adattatore
in uscita generico.
Il limite che rimane pericoloso è quello successivo all'esito positivo della chiamata alla piattaforma e precedente
alla registrazione della ricevuta. Se il processo termina in quel momento, il core non può sapere se il
messaggio della piattaforma esista, a meno che l'adattatore non dichiari reconcileUnknownSend.
Questo hook classifica un invio interrotto come sent, not_sent o
unresolved; solo not_sent consente la ripetizione. I canali privi di riconciliazione
ricorrono allo stato unknown_after_send (src/channels/message/state.ts,
src/infra/outbound/delivery-queue-recovery.ts) e possono scegliere la
ripetizione con almeno una consegna soltanto se i messaggi visibili duplicati rappresentano un compromesso
accettabile e documentato per quel canale.
Contesto di ricezione
createMessageReceiveContext tiene traccia dello stato di conferma/rifiuto per ogni evento in ingresso, con un
ack() idempotente e un nack(error) esplicito. Il criterio di conferma
(ChannelMessageReceiveAckPolicy) è uno dei seguenti:
| Criterio | Quando viene confermato |
|---|---|
after_receive_record |
Il core ha persistito metadati in ingresso sufficienti per deduplicare/instradare una nuova consegna |
after_agent_dispatch |
L'esecuzione dell'agente è stata inoltrata |
after_durable_send |
L'invio persistente in uscita per questo turno è stato registrato |
manual |
Il chiamante controlla esplicitamente la tempistica della conferma (impostazione predefinita per gli adattatori che non dichiarano un criterio) |
Il polling di Telegram usa questo meccanismo per persistere una filigrana degli aggiornamenti completati in sicurezza
(safeCompletedUpdateId in extensions/telegram/src/bot-update-tracker.ts):
grammY continua a osservare ogni aggiornamento quando entra nella catena del middleware, ma
OpenClaw fa avanzare la filigrana di riavvio persistita soltanto oltre gli aggiornamenti che
hanno completato l'inoltro, così quelli non riusciti o ancora in sospeso vengono ripetuti dopo un riavvio.
L'offset getUpdates a monte di Telegram rimane sotto il controllo di grammY; non è stata realizzata
una sorgente di polling completamente persistente che controlli la riconsegna a livello di piattaforma oltre questa
filigrana (consulta le Domande aperte).
Anteprima in tempo reale
src/channels/message/live.ts modella anteprima/modifica/finalizzazione come un unico ciclo di vita:
createLiveMessageState, markLiveMessagePreviewUpdated,
markLiveMessageFinalized, markLiveMessageCancelled e
deliverFinalizableLivePreviewAdapter (crea una modifica finale da una bozza, la applica
e ricorre a un invio normale quando la modifica non è possibile o non riesce).
LiveMessageState.phase è idle | previewing | finalizing | finalized | cancelled; canFinalizeInPlace determina se un'anteprima possa diventare il messaggio
finale tramite modifica anziché mediante un nuovo invio.
Ricevute persistenti
MessageReceipt (src/channels/message/types.ts) normalizza uno o più
identificatori dei messaggi della piattaforma provenienti da un singolo invio logico in platformMessageIds, insieme
alle parts per ciascuna parte (tipo, indice, identificatore del thread, identificatore del messaggio a cui si risponde). Viene mantenuto un identificatore primario
per i thread e le modifiche successive. Ciò rende le consegne composte da più parti (testo
più contenuti multimediali, testo suddiviso, ripiego della scheda) ripetibili e deduplicabili dopo
un riavvio.
Riduzione dell'SDK pubblico
Il refactoring ha assorbito o deprecato: gli helper reply-runtime, reply-dispatch-runtime,
reply-reference, reply-chunking e reply-payload esposti come API pubblica,
inbound-reply-dispatch, channel-reply-pipeline e la maggior parte degli utilizzi pubblici
di outbound-runtime. src/plugin-sdk/channel-message.ts è ora un barrel di riesportazione
@deprecated che punta a channel-outbound /
channel-inbound; gli alias di runtime channel.turn sono stati rimossi e la vecchia
pagina della documentazione /plugins/sdk-channel-turn reindirizza a
API in ingresso dei canali. Il nuovo codice dei Plugin deve
utilizzare direttamente channel-outbound e channel-inbound.
Dove l'implementazione si è discostata dal progetto originale
La bozza progettuale riportata di seguito non è mai stata distribuita esattamente come descritta. Viene conservata per accuratezza storica; non considerare questi nomi di tipi come API corrente.
- Nessun
MessageOrigin/shouldDropOpenClawEcho. Il piano originale prevedeva un tag di originesource: "openclaw"sui messaggi di errore del Gateway, oltre a un predicato condiviso che scartasse nelle stanze condivise gli echi contrassegnati e creati dal bot prima dell'autorizzazioneallowBots. Tale tipo e predicato non esistono nel codice sorgente.allowBotsè una vera chiave di configurazione per canale (Slack, Discord, Google Chat e altri), ma il meccanismo di assegnazione dei tag di origine destinato a proteggerla non è mai stato realizzato. La soppressione degli echi degli errori del Gateway nelle stanze abilitate per i bot rimane una lacuna aperta, non una garanzia distribuita. - Nessuno spazio dei nomi unificato
core.messages.receive/send/live/state. Le funzioni distribuite si trovano direttamente insrc/channels/message/*(withDurableMessageSendContext,createMessageReceiveContext,createLiveMessageState,classifyDurableSendRecoveryState) anziché dietro una facciatacore.messages.*. - Nessun tipo di messaggio normalizzato generico
ChannelMessage/MessageTarget/MessageRelation. Il core continua a passare payload di risposta concreti (ReplyPayload) e contesti specifici del canale attraverso gli adattatori di invio, anziché una singola struttura di messaggio indipendente dalla piattaforma con una relazionekind: "reply" | "followup" | "broadcast" | "system". - I nomi dei criteri di conferma differiscono dalla bozza. Distribuiti:
after_receive_record | after_agent_dispatch | after_durable_send | manual. La bozza originale usavaimmediate | after-record | after-durable-send | manualcon un campo relativo al motivo del timeout del Webhook; tale struttura non è stata realizzata. - Le chiavi di funzionalità
DurableFinalDeliveryRequirementMaphanno sostituito l'oggetto abbozzatoMessageCapabilities. Le funzionalità sono flag booleani piatti (text,media,poll,payload,silent,replyTo,thread,nativeQuote,messageSendingHooks,batch,reconcileUnknownSend,afterSendSuccess,afterCommit) verificati tramiteverifyDurableFinalCapabilityProofsanziché una struttura annidata nello stiletext.chunking/attachments.voice.
Rischi concreti della migrazione (ancora rilevanti)
Questi effetti collaterali specifici dei canali precedono il refactoring e devono continuare a funzionare attraverso i nuovi percorsi di invio. Non sono ipotetici: ciascuno è attualmente implementato ed essenziale per il funzionamento.
- iMessage (
extensions/imessage/src/monitor/echo-cache.ts,persisted-echo-cache.ts): il monitor registra i messaggi inviati in una cache degli echi dopo un invio riuscito. Gli invii finali durevoli devono continuare a popolare tale cache, altrimenti OpenClaw può acquisire nuovamente le proprie risposte come messaggi utente in entrata. - Tlon (
extensions/tlon/src/monitor/index.ts): aggiunge una firma facoltativa del modello e registra le discussioni a cui si è partecipato dopo le risposte di gruppo. La consegna durevole non deve aggirare questi effetti. - Discord e gli altri dispatcher preparati gestiscono già la consegna diretta e il comportamento delle anteprime. Un canale non è durevole end-to-end finché il relativo dispatcher preparato non instrada esplicitamente gli invii finali attraverso il contesto di invio; non presumere che l'adattatore generico da solo garantisca la copertura.
- La consegna di ripiego silenziosa di Telegram deve consegnare l'intero array di payload proiettati, non soltanto il primo payload, dopo la suddivisione in blocchi/proiezione di ripiego.
- LINE, Zalo, Nostr e percorsi di supporto simili possono prevedere la gestione dei token di risposta, l'inoltro proxy dei contenuti multimediali, cache dei messaggi inviati o destinazioni accessibili soltanto tramite callback. La consegna rimane di competenza del canale finché tali semantiche non sono rappresentate dall'adattatore di invio e coperte dai test.
- Gli helper per i messaggi diretti possono avere una callback di risposta che costituisce l'unica destinazione di trasporto corretta. L'invio generico in uscita non deve dedurre una destinazione dai campi grezzi della piattaforma ignorando tale callback.
Classificazione degli errori
Gli adattatori classificano gli errori di trasporto in categorie chiuse in stile
DeliveryFailureKind (transitorio, limite di frequenza, autenticazione, autorizzazione, non trovato, payload
non valido, conflitto, annullato, sconosciuto). Criteri del core:
- Riprovare in caso di errori transitori e di superamento del limite di frequenza.
- Non riprovare in caso di payload non valido, salvo che esista un ripiego di rendering.
- Non riprovare in caso di errori di autenticazione o autorizzazione finché la configurazione non cambia.
- In caso di risorsa non trovata, consentire alla finalizzazione in tempo reale di passare dalla modifica a un nuovo invio quando il canale dichiara che tale operazione è sicura.
- In caso di conflitto, utilizzare lo stato della ricevuta/idempotenza per determinare se il messaggio esiste già.
- Qualsiasi errore successivo alla chiamata alla piattaforma, che potrebbe essere riuscita, ma precedente al commit
della ricevuta diventa
unknown_after_send, a meno che l'adattatore non dimostri che l'operazione sulla piattaforma non è avvenuta.
Questioni aperte
- Se Telegram debba infine sostituire l'esecutore di polling grammY (
1.43.0) con una sorgente di polling completamente durevole che controlli la riconsegna a livello di piattaforma, non soltanto il watermark persistente di riavvio di OpenClaw (safeCompletedUpdateId). - Se lo stato delle anteprime in tempo reale debba risiedere nello stesso record dell'intento di invio finale o in un archivio correlato dello stato in tempo reale.
- Se la soppressione degli echi in caso di errore del Gateway nelle stanze condivise con bot abilitati richieda il meccanismo di etichettatura dell'origine inizialmente previsto, un contratto più semplice per canale o non rientri nell'ambito.
- Quali canali dispongano di supporto nativo per origine/metadati per la soppressione degli echi tra bot e quali richiedano invece un registro persistente dei messaggi in uscita.