Messages and delivery
Рефакторинг життєвого циклу повідомлень
Чому відбувся цей рефакторинг
Стек каналів виріс із кількох локальних виправлень: окремих допоміжних засобів
для вхідних повідомлень за рівнями зрілості (runtime.channel.inbound.run для
простих адаптерів, runtime.channel.inbound.runPreparedReply для розширених),
застарілих допоміжних засобів диспетчеризації відповідей
(dispatchInboundReplyWithBase, recordInboundSessionAndDispatchReply),
потокового передавання попереднього перегляду для окремих каналів і
забезпечення надійності остаточного доставлення, доданого до наявних шляхів
корисного навантаження відповіді. Така структура створювала забагато публічних
концепцій і забагато місць, у яких семантика доставлення могла розходитися.
Прогалина в надійності, яка змусила переглянути дизайн:
Telegram polling update acked -> assistant final text exists -> process restarts before sendMessage succeeds -> final response is lostЦільовий інваріант: щойно ядро вирішує, що має існувати видиме вихідне повідомлення, намір надсилання має стати надійно збереженим до спроби виклику платформи, а квитанція платформи має бути зафіксована після успішного завершення. Це типово забезпечує відновлення з доставленням принаймні один раз. Поведінка «рівно один раз» існує лише там, де адаптер підтверджує нативну ідемпотентність або звіряє спробу з невідомим результатом після надсилання зі станом платформи до повторного відтворення.
Що було реалізовано
Внутрішня предметна область розміщена в src/channels/message/*:
| Файл | За що відповідає |
|---|---|
types.ts |
Контракти типів адаптера, контексту надсилання, квитанції та надійно збереженого наміру |
send.ts |
withDurableMessageSendContext / sendDurableMessageBatch — контекст надійного надсилання |
receive.ts |
createMessageReceiveContext — машина станів політики підтвердження вхідних подій |
live.ts |
Стан оперативного попереднього перегляду та логіка завершення на місці або переходу до резервного варіанта |
state.ts |
classifyDurableSendRecoveryState — класифікація відновлення після переривання |
receipt.ts |
Нормалізує результати надсилання платформи в MessageReceipt |
capabilities.ts |
Визначає необхідні можливості надійного остаточного доставлення з корисного навантаження |
contracts.ts |
Перевірка підтверджень контрактів для оголошених можливостей адаптера |
adapter.ts |
defineChannelMessageAdapter |
outbound-bridge.ts |
createChannelMessageAdapterFromOutbound — обгортає застарілі функції sendText/sendMedia/sendPayload/sendPoll |
ingress-queue.ts |
createChannelIngressQueue — надійна черга вхідних подій |
durable-receive.ts |
createDurableInboundReceiveJournal — журнал прийняття/очікування/завершення/звільнення для дедуплікації вхідних повідомлень |
inbound-reply-dispatch.ts |
dispatchChannelInboundReply і обгортки із застарілими назвами |
reply-pipeline.ts |
createChannelReplyPipeline, допоміжні засоби префікса відповіді та зворотного виклику індикатора введення |
Публічна поверхня: openclaw/plugin-sdk/channel-outbound (допоміжні засоби
надсилання/квитанцій/надійності/оперативної роботи/конвеєра відповідей) і
openclaw/plugin-sdk/channel-inbound (вхідний контекст,
runChannelInboundEvent, dispatchChannelInboundReply). На цих сторінках
наведено приклади адаптерів, поточні назви типів і примітки щодо міграції —
саме вони є джерелом істини щодо структури API, а не наведені нижче ескізи.
Контекст надсилання
withDurableMessageSendContext надає коду каналу етапи render,
previewUpdate, send, edit, delete, commit і fail для одного
вихідного повідомлення. sendDurableMessageBatch — це обгортка для типового
випадку: візуалізувати, надіслати, а потім зафіксувати за стану
sent/suppressed або завершити з помилкою.
sendDurableMessageBatch повертає один дискримінований результат:
| Стан | Значення |
|---|---|
sent |
Доставлено принаймні одне видиме повідомлення платформи |
suppressed |
Жодне повідомлення платформи не слід вважати відсутнім (скасовано хуком, пробний запуск тощо) |
partial_failed |
Принаймні одне повідомлення доставлено до збою наступного корисного навантаження або побічної дії |
failed |
Не створено жодної квитанції платформи |
Режим надійності може бути required, best_effort або disabled
(MessageDurabilityPolicy у src/channels/message/types.ts). required
завершується відмовою, якщо надійно зберегти намір неможливо; best_effort
переходить до безпосереднього надсилання, коли постійне зберігання недоступне;
disabled зберігає поведінку безпосереднього надсилання, що існувала до
рефакторингу. Застарілі допоміжні засоби сумісності типово використовують
disabled і не визначають required лише на підставі того, що канал має
універсальний вихідний адаптер.
Небезпечною залишається межа після успішного виклику платформи, але до
фіксації квитанції. Якщо процес завершується в цей момент, ядро не може знати,
чи існує повідомлення платформи, якщо адаптер не оголошує
reconcileUnknownSend. Цей хук класифікує перерване надсилання як sent,
not_sent або unresolved; повторне відтворення дозволено лише для
not_sent. Канали без звіряння переходять до стану unknown_after_send
(src/channels/message/state.ts,
src/infra/outbound/delivery-queue-recovery.ts) і можуть вибрати повторне
відтворення з доставленням принаймні один раз, лише якщо дублювання видимих
повідомлень є прийнятним і документованим компромісом для такого каналу.
Контекст отримання
createMessageReceiveContext відстежує стан підтвердження/відхилення кожної
вхідної події за допомогою ідемпотентного ack() і явного nack(error).
Політика підтвердження (ChannelMessageReceiveAckPolicy) має одне з таких
значень:
| Політика | Коли підтверджує |
|---|---|
after_receive_record |
Ядро зберегло достатньо метаданих вхідної події для дедуплікації/маршрутизації повторного доставлення |
after_agent_dispatch |
Виконання агента було диспетчеризовано |
after_durable_send |
Надійне вихідне надсилання для цього ходу було зафіксовано |
manual |
Викликач явно керує часом підтвердження (типове значення для адаптерів, які не оголошують політику) |
Опитування Telegram використовує це для збереження позначки безпечно
завершеного оновлення (safeCompletedUpdateId у
extensions/telegram/src/bot-update-tracker.ts): grammY і далі спостерігає
кожне оновлення під час його входження до ланцюжка проміжного ПЗ, але
OpenClaw просуває збережену позначку перезапуску лише за оновлення, які
завершили диспетчеризацію, тому невдалі або ще не завершені оновлення
відтворюються після перезапуску. Висхідне зміщення getUpdates Telegram і
далі належить grammY; повністю надійне джерело опитування, яке керує повторним
доставленням на рівні платформи поза межами цієї позначки, не реалізовано
(див. «Відкриті питання»).
Оперативний попередній перегляд
src/channels/message/live.ts моделює попередній перегляд, редагування та
завершення як один життєвий цикл: createLiveMessageState,
markLiveMessagePreviewUpdated, markLiveMessageFinalized,
markLiveMessageCancelled і deliverFinalizableLivePreviewAdapter
(сформувати остаточне редагування з чернетки, застосувати його та перейти до
звичайного надсилання, якщо редагування неможливе або завершується помилкою).
LiveMessageState.phase має значення idle | previewing | finalizing | finalized | cancelled; canFinalizeInPlace визначає, чи може попередній перегляд стати
остаточним повідомленням шляхом редагування замість нового надсилання.
Надійні квитанції
MessageReceipt (src/channels/message/types.ts) нормалізує один або кілька
ідентифікаторів повідомлень платформи з одного логічного надсилання в
platformMessageIds разом із parts для кожної частини (тип, індекс,
ідентифікатор гілки, ідентифікатор повідомлення, на яке відповідають).
Основний ідентифікатор зберігається для гілок і подальших редагувань. Саме це
робить складені доставлення (текст із медіафайлами, текст, розділений на
частини, резервне подання картки) придатними для повторного відтворення та
дедуплікації після перезапуску.
Скорочення публічного SDK
Рефакторинг поглинув або оголосив застарілими: допоміжні засоби
reply-runtime, reply-dispatch-runtime, reply-reference, reply-chunking,
reply-payload, доступні як публічний API, inbound-reply-dispatch,
channel-reply-pipeline і більшість публічних використань
outbound-runtime. src/plugin-sdk/channel-message.ts тепер є
@deprecated-файлом повторного експорту, який указує на channel-outbound /
channel-inbound; псевдоніми середовища виконання channel.turn видалено, а
стара сторінка документації /plugins/sdk-channel-turn переспрямовує на
API вхідних каналів. Новий код Plugin має
безпосередньо використовувати channel-outbound і channel-inbound.
Де реалізація відхилилася від початкового дизайну
Наведений нижче ескіз дизайну ніколи не був реалізований буквально в описаному вигляді. Його збережено для історичної точності; не вважайте ці назви типів поточним API.
- Немає
MessageOrigin/shouldDropOpenClawEcho. Початковий план передбачав тег походженняsource: "openclaw"для повідомлень про збій Gateway, а також спільний предикат, який відкидає позначені відлуння, створені ботом, у спільних кімнатах до авторизаціїallowBots. Цього типу та предиката в кодовій базі немає. СамallowBotsє реальною конфігураційною властивістю окремих каналів (Slack, Discord, Google Chat та інших), але механізм позначення походження, призначений для його захисту, так і не було створено. Приглушення відлуння повідомлень про збій Gateway у кімнатах із дозволеними ботами залишається відкритою прогалиною, а не реалізованою гарантією. - Немає уніфікованого простору імен
core.messages.receive/send/live/state. Реалізовані функції розміщені безпосередньо вsrc/channels/message/*(withDurableMessageSendContext,createMessageReceiveContext,createLiveMessageState,classifyDurableSendRecoveryState), а не за фасадомcore.messages.*. - Немає універсального нормалізованого типу повідомлення
ChannelMessage/MessageTarget/MessageRelation. Ядро й далі передає конкретні корисні навантаження відповіді (ReplyPayload) і контексти окремих каналів через адаптери надсилання замість єдиної незалежної від платформи структури повідомлення з відношеннямkind: "reply" | "followup" | "broadcast" | "system". - Назви політик підтвердження відрізняються від ескізу. Реалізовано:
after_receive_record | after_agent_dispatch | after_durable_send | manual. Початковий ескіз використовувавimmediate | after-record | after-durable-send | manualіз полем причини перевищення часу очікування Webhook; цю структуру не було реалізовано. - Ключі можливостей
DurableFinalDeliveryRequirementMapзамінили ескізний об’єктMessageCapabilities. Можливості представлені плоскими булевими прапорцями (text,media,poll,payload,silent,replyTo,thread,nativeQuote,messageSendingHooks,batch,reconcileUnknownSend,afterSendSuccess,afterCommit), які перевіряються черезverifyDurableFinalCapabilityProofs, а не вкладеною структурою на зразокtext.chunking/attachments.voice.
Конкретні ризики міграції (досі актуальні)
Ці специфічні для каналів побічні ефекти існували до рефакторингу й мають і надалі працювати через нові шляхи надсилання. Вони не гіпотетичні: кожен із них реалізований і критично важливий уже сьогодні.
- iMessage (
extensions/imessage/src/monitor/echo-cache.ts,persisted-echo-cache.ts): після успішного надсилання монітор записує надіслані повідомлення до кешу відлуння. Остаточні надійні надсилання мають і надалі наповнювати цей кеш, інакше OpenClaw може повторно обробляти власні відповіді як вхідні повідомлення користувачів. - Tlon (
extensions/tlon/src/monitor/index.ts): додає необов’язковий підпис моделі та записує гілки, у яких була участь, після відповідей у групах. Надійна доставка не повинна обходити ці ефекти. - Discord та інші підготовлені диспетчери вже відповідають за безпосередню доставку та поведінку попереднього перегляду. Канал не є наскрізно надійним, доки його підготовлений диспетчер явно не спрямовує остаточні повідомлення через контекст надсилання; не вважайте, що самого універсального адаптера достатньо.
- Резервна беззвучна доставка Telegram після поділу на частини або резервного проєктування має доставляти весь масив спроєктованого корисного навантаження, а не лише його перший елемент.
- LINE, Zalo, Nostr та подібні допоміжні шляхи можуть обробляти токени відповідей, проксіювання медіафайлів, кеші надісланих повідомлень або цілі, доступні лише через зворотний виклик. Вони залишаються під керуванням каналу, доки ця семантика не буде представлена адаптером надсилання та покрита тестами.
- Допоміжні засоби прямих приватних повідомлень можуть мати зворотний виклик відповіді, який є єдиною правильною транспортною ціллю. Універсальний механізм вихідних повідомлень не повинен вгадувати ціль за необробленими полями платформи й пропускати цей зворотний виклик.
Класифікація помилок
Адаптери класифікують помилки транспорту за закритими категоріями на кшталт
DeliveryFailureKind (тимчасова помилка, обмеження частоти, автентифікація,
дозвіл, не знайдено, некоректне корисне навантаження, конфлікт, скасовано,
невідомо). Політика ядра:
- Повторювати спроби після тимчасових помилок і помилок обмеження частоти.
- Не повторювати спроби після помилок некоректного корисного навантаження, якщо немає резервного варіанта відтворення.
- Не повторювати спроби після помилок автентифікації або дозволів, доки не зміниться конфігурація.
- Якщо об’єкт не знайдено, дозволити завершенню в реальному часі перейти від редагування до нового надсилання, якщо канал оголошує це безпечним.
- У разі конфлікту використовувати стан квитанції та ідемпотентності, щоб визначити, чи повідомлення вже існує.
- Будь-яка помилка після виклику платформи, який міг завершитися успішно, але до
фіксації квитанції, стає
unknown_after_send, якщо адаптер не доведе, що операція на платформі не відбулася.
Відкриті питання
- Чи варто Telegram зрештою замінити виконавець опитування grammY (
1.43.0) на повністю надійне джерело опитування, яке керує повторною доставкою на рівні платформи, а не лише збереженою позначкою перезапуску OpenClaw (safeCompletedUpdateId). - Чи має стан попереднього перегляду в реальному часі зберігатися в тому самому записі, що й намір остаточного надсилання, чи в окремому сховищі стану в реальному часі.
- Чи потребує придушення відлуння в разі збою Gateway у спільних кімнатах із ботами початково запланованого механізму позначення походження, простішого контракту для кожного каналу, чи це виходить за межі завдання.
- Які канали мають вбудовану підтримку походження або метаданих для придушення відлуння між ботами, а яким потрібен збережений реєстр вихідних повідомлень.