Messages and delivery
Рефакторинг жизненного цикла сообщений
Эта страница описывает целевой дизайн для замены разрозненных помощников входящих сообщений каналов, отправки ответов, потоковой передачи предпросмотра и исходящей доставки одним устойчивым жизненным циклом сообщений.
Кратко:
- Базовыми примитивами ядра должны быть receive и send, а не reply.
- Ответ — это только связь исходящего сообщения.
- Ход — это удобство для обработки входящих сообщений, а не владелец доставки.
- Отправка должна быть основана на контексте:
begin, рендеринг, предпросмотр или потоковая передача, финальная отправка, фиксация, сбой. - Прием тоже должен быть основан на контексте: нормализация, дедупликация, маршрутизация, запись, диспетчеризация, подтверждение платформы, сбой.
- Публичный SDK Plugin должен свестись к одной небольшой поверхности исходящих сообщений канала.
Проблемы
Текущий стек каналов вырос из нескольких обоснованных локальных потребностей:
- Простые входящие адаптеры используют
runtime.channel.inbound.run. - Богатые адаптеры используют
runtime.channel.inbound.runPreparedReply. - Устаревшие помощники используют
dispatchInboundReplyWithBase,recordInboundSessionAndDispatchReply, помощники полезной нагрузки ответа, разбиение ответа на фрагменты, ссылки на ответы и помощники исходящего runtime. - Потоковая передача предпросмотра живет в диспетчерах, специфичных для каналов.
- Устойчивость финальной доставки добавляется вокруг существующих путей полезной нагрузки ответа.
Такая форма исправляет локальные ошибки, но оставляет OpenClaw слишком много публичных понятий и слишком много мест, где семантика доставки может расходиться.
Проблема надежности, которая это выявила:
Telegram polling update acked -> assistant final text exists -> process restarts before sendMessage succeeds -> final response is lostЦелевой инвариант шире, чем Telegram: как только ядро решает, что видимое исходящее сообщение должно существовать, намерение должно быть сохранено до попытки отправки на платформу, а квитанция платформы должна быть зафиксирована после успеха. Это дает OpenClaw восстановление с семантикой at-least-once. Поведение exactly-once существует только для адаптеров, которые могут доказать нативную идемпотентность или согласовать попытку с неизвестным результатом после отправки с состоянием платформы перед повторным воспроизведением.
Это конечное состояние для этого рефакторинга, а не описание каждого текущего пути. Во время миграции существующие исходящие помощники все еще могут откатываться к прямой отправке, когда best-effort запись в очередь завершается сбоем. Рефакторинг завершен только тогда, когда устойчивые финальные отправки fail closed или явно отказываются от этого с документированной неустойчивой политикой.
Цели
- Один жизненный цикл ядра для всех путей приема и отправки сообщений каналов.
- Устойчивые финальные отправки по умолчанию в новом жизненном цикле сообщений после того, как адаптер объявляет replay-safe поведение.
- Общая семантика предпросмотра, редактирования, потоковой передачи, финализации, повторных попыток, восстановления и квитанций.
- Небольшая поверхность SDK Plugin, которую сторонние plugins смогут изучать и поддерживать.
- Совместимость для существующих вызывающих сторон совместимости входящих ответов во время миграции.
- Четкие точки расширения для новых возможностей каналов.
- Без платформенно-специфичных ветвлений в ядре.
- Без сообщений канала с дельтами токенов. Потоковая передача каналов остается предпросмотром сообщения, редактированием, добавлением или доставкой завершенного блока.
- Структурированные метаданные происхождения OpenClaw для операционного/системного вывода, чтобы видимые сбои Gateway не возвращались в общие комнаты с включенными ботами как новые prompts.
Не цели
- Не переводить каждый существующий канал на устойчивую доставку сообщений в первой фазе.
- Не принуждать каждый канал к одинаковому нативному транспортному поведению.
- Не обучать ядро темам Telegram, нативным потокам Slack, редактированиям Matrix, карточкам Feishu, голосу QQ или активностям Teams.
- Не публиковать все внутренние помощники миграции как стабильный API SDK.
- Не делать так, чтобы повторные попытки заново воспроизводили завершенные неидемпотентные операции платформы.
Эталонная модель
У Vercel Chat есть хорошая публичная ментальная модель:
ChatThreadChannelMessage- методы адаптера, такие как
postMessage,editMessage,deleteMessage,stream,startTyping, и получение истории - адаптер состояния для дедупликации, блокировок, очередей и постоянного хранения
OpenClaw должен заимствовать словарь, а не копировать поверхность.
Что нужно OpenClaw сверх этой модели:
- Устойчивые намерения исходящей отправки до прямых транспортных вызовов.
- Явные контексты отправки с началом, фиксацией и сбоем.
- Контексты приема, знающие политику подтверждения платформы.
- Квитанции, которые переживают перезапуск и могут управлять редактированием, удалением, восстановлением и подавлением дубликатов.
- Более маленький публичный SDK. Встроенные plugins могут использовать внутренние помощники runtime, но сторонние plugins должны видеть один согласованный API сообщений.
- Специфичное для агента поведение: сессии, расшифровки, потоковая передача блоков, прогресс инструментов, approvals, медиа-директивы, тихие ответы и история упоминаний в группах.
Промисов в стиле thread.post() недостаточно для OpenClaw. Они скрывают
границу транзакции, которая решает, можно ли восстановить отправку.
Модель ядра
Новый домен должен жить во внутреннем пространстве имен ядра, например
src/channels/message/*.
У него четыре понятия:
core.messages.receive(...)core.messages.send(...)core.messages.live(...)core.messages.state(...)receive владеет жизненным циклом входящих сообщений.
send владеет жизненным циклом исходящих сообщений.
live владеет состоянием предпросмотра, редактирования, прогресса и потока.
state владеет устойчивым хранением намерений, квитанциями, идемпотентностью, восстановлением, блокировками и
дедупликацией.
Термины сообщений
Сообщение
Нормализованное сообщение нейтрально к платформе:
type ChannelMessage = { id: string; channel: string; accountId?: string; direction: "inbound" | "outbound"; target: MessageTarget; sender?: MessageActor; body?: MessageBody; attachments?: MessageAttachment[]; relation?: MessageRelation; origin?: MessageOrigin; timestamp?: number; raw?: unknown;};Цель
Цель описывает, где живет сообщение:
type MessageTarget = { kind: "direct" | "group" | "channel" | "thread"; id: string; label?: string; spaceId?: string; parentId?: string; threadId?: string; nativeChannelId?: string;};Связь
Ответ — это связь, а не корень API:
type MessageRelation = | { kind: "reply"; inboundMessageId?: string; replyToId?: string; threadId?: string; quote?: MessageQuote; } | { kind: "followup"; sessionKey?: string; previousMessageId?: string; } | { kind: "broadcast"; reason?: string; } | { kind: "system"; reason: | "approval" | "task" | "hook" | "cron" | "subagent" | "message_tool" | "cli" | "control_ui" | "automation" | "error"; };Это позволяет одному и тому же пути отправки обрабатывать обычные ответы, уведомления Cron, prompts approval, завершения задач, отправки message-tool, отправки из CLI или Control UI, результаты subagent и автоматические отправки.
Происхождение
Происхождение описывает, кто создал сообщение и как OpenClaw должен обрабатывать эхо этого сообщения. Оно отделено от связи: сообщение может быть ответом пользователю и при этом быть операционным выводом, созданным OpenClaw.
type MessageOrigin = | { source: "openclaw"; schemaVersion: 1; kind: "gateway_failure"; code: "agent_failed_before_reply" | "missing_api_key" | "model_login_expired"; echoPolicy: "drop_bot_room_echo"; } | { source: "user" | "external_bot" | "platform" | "unknown"; };Ядро владеет смыслом вывода, созданного OpenClaw. Каналы владеют тем, как это происхождение кодируется в их транспорт.
Первое обязательное использование — вывод сбоев Gateway. Люди все еще должны видеть
сообщения вроде "Agent failed before reply" или "Missing API key", но помеченный
операционный вывод OpenClaw не должен приниматься как ввод, созданный ботом, в общих
комнатах, когда включен allowBots.
Квитанция
Квитанции являются первоклассными:
type MessageReceipt = { primaryPlatformMessageId?: string; platformMessageIds: string[]; parts: MessageReceiptPart[]; threadId?: string; replyToId?: string; editToken?: string; deleteToken?: string; url?: string; sentAt: number; raw?: unknown;}; type MessageReceiptPart = { platformMessageId: string; kind: "text" | "media" | "voice" | "card" | "preview" | "unknown"; index: number; threadId?: string; replyToId?: string; editToken?: string; deleteToken?: string; url?: string; raw?: unknown;};Квитанции — это мост от устойчивого намерения к будущему редактированию, удалению, финализации предпросмотра, подавлению дубликатов и восстановлению.
Квитанция может описывать одно сообщение платформы или доставку из нескольких частей. Разбитый на фрагменты текст, медиа плюс текст, голос плюс текст и fallback карточек должны сохранять все идентификаторы платформы, все еще предоставляя primary id для трединга и последующих редактирований.
Контекст приема
Прием не должен быть простым вызовом помощника. Ядру нужен контекст, который знает дедупликацию, маршрутизацию, запись сессии и политику подтверждения платформы.
type MessageReceiveContext = { id: string; channel: string; accountId?: string; input: ChannelMessage; ack: ReceiveAckController; route: MessageRouteController; session: MessageSessionController; log: MessageLifecycleLogger; dedupe(): Promise<ReceiveDedupeResult>; resolve(): Promise<ResolvedInboundMessage>; record(resolved: ResolvedInboundMessage): Promise<RecordResult>; dispatch(recorded: RecordResult): Promise<DispatchResult>; commit(result: DispatchResult): Promise<void>; fail(error: unknown): Promise<void>;};Поток приема:
platform event -> begin receive context -> normalize -> classify -> dedupe and self-echo gate -> route and authorize -> record inbound session metadata -> dispatch agent run -> durable outbound sends happen through send context -> commit receive -> ack platform when policy allowsAck — это не одна вещь. Контракт приема должен держать эти сигналы раздельно:
- Transport ack: сообщает webhook или сокету платформы, что OpenClaw принял конверт события. Некоторым платформам это требуется до диспетчеризации.
- Polling offset ack: продвигает cursor, чтобы то же событие не было получено снова. Это не должно продвигаться дальше работы, которую нельзя восстановить.
- Inbound record ack: подтверждает, что OpenClaw сохранил достаточно входящих метаданных для дедупликации и маршрутизации повторной доставки.
- User-visible receipt: необязательное поведение чтения/статуса/typing; никогда не граница устойчивости.
ReceiveAckPolicy управляет только транспортным подтверждением или подтверждением polling. Он не должен
переиспользоваться для квитанций о прочтении или статусных реакций.
Перед авторизацией бота прием должен применять общую политику эха OpenClaw, когда канал может декодировать метаданные происхождения сообщения:
function shouldDropOpenClawEcho(params: { origin?: MessageOrigin; isBotAuthor: boolean; isRoomish: boolean;}): boolean { return ( params.isBotAuthor && params.isRoomish && params.origin?.source === "openclaw" && params.origin.kind === "gateway_failure" && params.origin.echoPolicy === "drop_bot_room_echo" );}Это отбрасывание основано на теге, а не на тексте. Сообщение в комнате, созданное ботом, с тем же
видимым текстом сбоя Gateway, но без метаданных происхождения OpenClaw, все равно
проходит обычную авторизацию allowBots.
Политика ack явная:
type ReceiveAckPolicy = | { kind: "immediate"; reason: "webhook-timeout" | "platform-contract" } | { kind: "after-record" } | { kind: "after-durable-send" } | { kind: "manual" };Telegram polling теперь использует политику ack контекста приема для своего сохраняемого
watermark перезапуска. Tracker все еще наблюдает обновления grammY по мере их входа в
цепочку middleware, но OpenClaw сохраняет только безопасный завершенный update id после
успешной диспетчеризации, оставляя неудачные или более ранние ожидающие обновления доступными для повторного воспроизведения после
перезапуска. Вышестоящий getUpdates fetch offset Telegram по-прежнему контролируется
библиотекой polling, поэтому оставшаяся более глубокая доработка — полностью устойчивый источник polling,
если нам потребуется повторная доставка на уровне платформы за пределами restart
watermark OpenClaw. Webhook-платформам может требоваться немедленный HTTP ack, но им все равно нужны
входящая дедупликация и устойчивые намерения исходящей отправки, потому что webhooks могут доставляться повторно.
Контекст отправки
Отправка также основана на контексте:
type MessageSendContext = { id: string; channel: string; accountId?: string; message: ChannelMessage; intent: DurableSendIntent; attempt: number; signal: AbortSignal; previousReceipt?: MessageReceipt; preview?: LiveMessageState; log: MessageLifecycleLogger; render(): Promise<RenderedMessageBatch>; previewUpdate(rendered: RenderedMessageBatch): Promise<LiveMessageState>; send(rendered: RenderedMessageBatch): Promise<MessageReceipt>; edit(receipt: MessageReceipt, rendered: RenderedMessageBatch): Promise<MessageReceipt>; delete(receipt: MessageReceipt): Promise<void>; commit(receipt: MessageReceipt): Promise<void>; fail(error: unknown): Promise<void>;};Предпочтительная оркестрация:
await core.messages.withSendContext(message, async (ctx) => { const rendered = await ctx.render(); if (ctx.preview?.canFinalizeInPlace) { return await ctx.edit(ctx.preview.receipt, rendered); } return await ctx.send(rendered);});Вспомогательная функция разворачивается в:
begin durable intent -> render -> optional preview/edit/stream work -> mark sending -> final platform send or final edit -> mark committing with raw receipt -> commit receipt -> ack durable intent -> fail durable intent on classified failureНамерение должно существовать до ввода-вывода транспорта. Перезапуск после начала, но до фиксации, восстанавливаем.
Опасная граница находится после успешной отправки на платформе и до фиксации квитанции. Если
процесс завершается там, OpenClaw не может знать, существует ли сообщение на платформе,
если адаптер не предоставляет нативную идемпотентность или путь сверки квитанций.
Такие попытки должны возобновляться в unknown_after_send, а не слепо повторяться. Каналы
без сверки могут выбрать повтор по принципу «как минимум один раз» только если дублирующиеся видимые
сообщения являются приемлемым, задокументированным компромиссом для этого канала и связи.
Текущий мост сверки SDK требует, чтобы адаптер объявил
reconcileUnknownSend, затем просит durableFinal.reconcileUnknownSend
классифицировать неизвестную запись как sent, not_sent или unresolved; только not_sent
разрешает повтор, а неразрешенные записи остаются терминальными или повторяют только
проверку сверки.
Политика долговечности должна быть явной:
type MessageDurabilityPolicy = "required" | "best_effort" | "disabled";required означает, что ядро должно завершаться отказом, когда не может записать долговечное намерение.
best_effort может продолжить выполнение, когда постоянное хранение недоступно. disabled сохраняет
старое поведение прямой отправки. Во время миграции устаревшие обертки и публичные
вспомогательные функции совместимости по умолчанию используют disabled; они не должны выводить required из
того факта, что у канала есть общий исходящий адаптер.
Контексты отправки также владеют локальными для канала эффектами после отправки. Миграция небезопасна, если долговечная доставка обходит локальное поведение, которое ранее было привязано к пути прямой отправки канала. Примеры включают кэши подавления self-echo, маркеры участия в треде, нативные якоря редактирования, рендеринг подписи модели и специфичные для платформы защиты от дубликатов. Эти эффекты должны либо перейти в адаптер отправки, адаптер рендеринга, либо именованный хук контекста отправки до того, как этот канал сможет включить долговечную общую финальную доставку.
Вспомогательные функции отправки должны возвращать квитанции до самого вызывающего кода. Долговечные
обертки не могут проглатывать идентификаторы сообщений или заменять результат доставки канала на
undefined; буферизованные диспетчеры используют эти идентификаторы для якорей тредов, последующих правок,
финализации предпросмотра и подавления дубликатов.
Резервные отправки работают с пакетами, а не с одиночными полезными нагрузками. Переписывания silent-reply, резервная отправка медиа, резервная отправка карточек и проекция фрагментов могут все создавать больше одного доставляемого сообщения, поэтому контекст отправки должен либо доставить весь спроецированный пакет, либо явно задокументировать, почему допустима только одна полезная нагрузка.
type RenderedMessageBatch = { units: RenderedMessageUnit[]; atomicity: "all_or_retry_remaining" | "best_effort_parts"; idempotencyKey: string;}; type RenderedMessageUnit = { index: number; kind: "text" | "media" | "voice" | "card" | "preview" | "unknown"; payload: unknown; required: boolean;};Когда такой резервный путь долговечен, весь спроецированный пакет должен быть представлен
одним долговечным намерением отправки или другим атомарным планом пакета. Записывать каждую полезную нагрузку
по одной недостаточно: сбой между полезными нагрузками может оставить частично видимый
резервный результат без долговечной записи для оставшихся полезных нагрузок. Восстановление должно знать,
у каких единиц уже есть квитанции, и либо повторять только отсутствующие единицы, либо пометить
пакет как unknown_after_send, пока адаптер не сверит его.
Живой контекст
Поведение предпросмотра, редактирования, прогресса и потока должно быть единым жизненным циклом с явным включением.
type MessageLiveAdapter = { begin?(ctx: MessageSendContext): Promise<LiveMessageState>; update?( ctx: MessageSendContext, state: LiveMessageState, update: LiveMessageUpdate, ): Promise<LiveMessageState>; finalize?( ctx: MessageSendContext, state: LiveMessageState, final: RenderedMessageBatch, ): Promise<MessageReceipt>; cancel?( ctx: MessageSendContext, state: LiveMessageState, reason: LiveCancelReason, ): Promise<void>;};Живое состояние достаточно долговечно для восстановления или подавления дубликатов:
type LiveMessageState = { mode: "partial" | "block" | "progress" | "native"; receipt?: MessageReceipt; visibleSince?: number; canFinalizeInPlace: boolean; lastRenderedHash?: string; staleAfterMs?: number;};Это должно покрывать текущее поведение:
- Telegram отправляет и редактирует предпросмотр, со свежим финальным сообщением после устаревания предпросмотра.
- Discord отправляет и редактирует предпросмотр, отменяет при медиа/ошибке/явном ответе.
- Slack использует нативный поток или черновой предпросмотр в зависимости от формы треда.
- Финализация черновой публикации Mattermost.
- Финализация чернового события Matrix или редактирование с удалением при несоответствии.
- Нативный поток прогресса Microsoft Teams.
- Поток QQ Bot или накопленный резервный результат.
Поверхность адаптера
Цель публичного SDK должна быть одним подпутем:
Целевая форма:
type ChannelMessageAdapter = { receive?: MessageReceiveAdapter; send: MessageSendAdapter; live?: MessageLiveAdapter; origin?: MessageOriginAdapter; render?: MessageRenderAdapter; capabilities: MessageCapabilities;};Адаптер отправки:
type MessageSendAdapter = { send(ctx: MessageSendContext, rendered: RenderedMessageBatch): Promise<MessageReceipt>; edit?( ctx: MessageSendContext, receipt: MessageReceipt, rendered: RenderedMessageBatch, ): Promise<MessageReceipt>; delete?(ctx: MessageSendContext, receipt: MessageReceipt): Promise<void>; classifyError?(ctx: MessageSendContext, error: unknown): DeliveryFailureKind; reconcileUnknownSend?(ctx: MessageSendContext): Promise<MessageReceipt | null>; afterSendSuccess?(ctx: MessageSendContext, receipt: MessageReceipt): Promise<void>; afterCommit?(ctx: MessageSendContext, receipt: MessageReceipt): Promise<void>;};Адаптер приема:
type MessageReceiveAdapter<TRaw = unknown> = { normalize(raw: TRaw, ctx: MessageNormalizeContext): Promise<ChannelMessage>; classify?(message: ChannelMessage): Promise<MessageEventClass>; preflight?(message: ChannelMessage, event: MessageEventClass): Promise<MessagePreflightResult>; ackPolicy?(message: ChannelMessage, event: MessageEventClass): ReceiveAckPolicy;};До preflight-авторизации ядро должно запускать общий предикат OpenClaw для echo
всякий раз, когда origin.decode возвращает метаданные происхождения OpenClaw. Адаптер приема
предоставляет факты платформы, такие как автор-бот и форма комнаты; ядро владеет решением
об отбрасывании и порядком, чтобы каналы не реализовывали текстовые фильтры заново.
Адаптер происхождения:
type MessageOriginAdapter<TRaw = unknown, TNative = unknown> = { encode?(origin: MessageOrigin): TNative | undefined; decode?(raw: TRaw): MessageOrigin | undefined;};Ядро устанавливает MessageOrigin. Каналы только преобразуют его в нативные
метаданные транспорта и обратно. Slack сопоставляет это с chat.postMessage({ metadata }) и
входящим message.metadata; Matrix может сопоставлять это с дополнительным содержимым события; каналы
без нативных метаданных могут использовать реестр квитанций/исходящих сообщений, когда это
лучшее доступное приближение.
Возможности:
type MessageCapabilities = { text: { maxLength?: number; chunking?: boolean }; attachments?: { upload: boolean; remoteUrl: boolean; voice?: boolean; }; threads?: { reply: boolean; topic?: boolean; nativeThread?: boolean; }; live?: { edit: boolean; delete: boolean; nativeStream?: boolean; progress?: boolean; }; delivery?: { idempotencyKey?: boolean; retryAfter?: boolean; receiptRequired?: boolean; };};Сокращение публичного SDK
Новая публичная поверхность должна поглотить или объявить устаревшими эти концептуальные области:
reply-runtimereply-dispatch-runtimereply-referencereply-chunkingreply-payloadinbound-reply-dispatchchannel-reply-pipeline- большинство публичных использований
outbound-runtime - специальные вспомогательные функции жизненного цикла чернового потока
Подпути совместимости могут оставаться обертками, но новым сторонним plugins они не должны быть нужны.
Встроенные plugins могут сохранять внутренние импорты вспомогательных функций через зарезервированные подпути
runtime во время миграции. Публичная документация должна направлять авторов plugins к
plugin-sdk/channel-outbound, когда он появится.
Связь с входящим каналом
runtime.channel.inbound.* является runtime-мостом во время миграции.
Он должен стать адаптером совместимости:
channel.inbound.run -> messages.receive context -> session dispatch -> messages.send context for visible outputchannel.inbound.runPreparedReply также должен сначала остаться:
channel-owned dispatcher -> messages.receive record/finalize bridge -> messages.live for preview/progress -> messages.send for final deliveryСтарая runtime-поверхность channel.turn была удалена. Runtime-вызывающие используют
channel.inbound.*; документация каналов и подпути SDK используют существительные inbound/message.
Ограничения совместимости
Во время миграции общая долговечная доставка включается явно для любого канала, чей существующий callback доставки имеет побочные эффекты помимо «отправить эту полезную нагрузку».
Устаревшие точки входа по умолчанию недолговечны:
channel.inbound.runиdispatchChannelInboundReplyиспользуют callback доставки канала, если этот канал явно не предоставляет проверенный объект политики/параметров долговечности.channel.inbound.runPreparedReplyостается принадлежащим каналу, пока подготовленный диспетчер явно не вызовет контекст отправки.- Публичные вспомогательные функции совместимости, такие как
recordInboundSessionAndDispatchReply,dispatchInboundReplyWithBaseи direct-DM helpers, никогда не внедряют общую долговечную доставку до предоставленного вызывающим кодом callbackdeliverилиreply.
Для типов мостов миграции durable: undefined означает «не долговечно». Долговечный
путь включается только явным значением политики/параметров. durable: false может оставаться совместимым написанием, но реализация не должна
требовать, чтобы каждый немигрированный канал добавлял его.
Текущий код моста должен сохранять явность решения о долговечности:
- Надежная финальная доставка возвращает дискриминированный статус.
handled_visibleиhandled_no_sendявляются терминальными;unsupportedиnot_applicableмогут откатиться к доставке, принадлежащей каналу;failedпередает ошибку отправки. - Универсальная надежная финальная доставка ограничивается возможностями адаптера, такими как тихая доставка, сохранение цели ответа, сохранение нативного цитирования и хуки отправки сообщений. При отсутствии паритета следует выбирать доставку, принадлежащую каналу, а не универсальную отправку, которая меняет видимое пользователю поведение.
- Надежные отправки на базе очереди предоставляют ссылку на намерение доставки. Существующие
поля сеанса
pendingFinalDelivery*могут переносить идентификатор намерения во время перехода; конечное состояние — хранилищеMessageSendIntentвместо замороженного текста ответа плюс специальных полей контекста.
Не включайте универсальный надежный путь для канала, пока все перечисленное ниже не станет истинным:
- Универсальный адаптер отправки выполняет тот же рендеринг и транспортное поведение, что и старый прямой путь.
- Локальные побочные эффекты после отправки сохраняются через контекст отправки.
- Адаптер возвращает квитанции или результаты доставки со всеми идентификаторами сообщений платформы.
- Подготовленные пути диспетчера либо вызывают новый контекст отправки, либо остаются задокументированными как находящиеся вне надежной гарантии.
- Резервная доставка обрабатывает каждый спроецированный payload, а не только первый.
- Надежная резервная доставка записывает весь массив спроецированных payload как одно воспроизводимое намерение или пакетный план.
Конкретные риски миграции, которые нужно сохранить:
- Доставка монитора iMessage записывает отправленные сообщения в echo cache после успешной отправки. Надежные финальные отправки все еще должны заполнять этот кэш, иначе OpenClaw может повторно принять собственные финальные ответы как входящие пользовательские сообщения.
- Tlon добавляет необязательную сигнатуру модели и записывает участвующие threads после групповых ответов. Универсальная надежная доставка не должна обходить эти эффекты; либо перенесите их в адаптеры рендеринга/отправки/финализации Tlon, либо оставьте Tlon на пути, принадлежащем каналу.
- Discord и другие подготовленные диспетчеры уже владеют прямой доставкой и поведением предпросмотра. На них не распространяется надежная гарантия assembled-turn, пока их подготовленные диспетчеры явно не направят финальные сообщения через контекст отправки.
- Тихая резервная доставка Telegram должна доставлять полный массив спроецированных payload. Упрощенный путь с одним payload может отбросить дополнительные резервные payload после проекции.
- LINE, Zalo, Nostr и другие существующие assembled/helper-пути могут иметь обработку reply-token, проксирование медиа, кэши отправленных сообщений, очистку loading/status или цели только для callback. Они остаются на доставке, принадлежащей каналу, пока эта семантика не будет представлена адаптером отправки и проверена тестами.
- Direct-DM helpers могут иметь callback ответа, который является единственной корректной транспортной
целью. Универсальный исходящий путь не должен угадывать по
OriginatingToилиToи пропускать этот callback. - Вывод сбоя OpenClaw gateway должен оставаться видимым людям, но помеченные
room echoes, созданные ботом, должны отбрасываться до авторизации
allowBots. Каналы не должны реализовывать это через фильтры префиксов видимого текста, кроме как в качестве короткой экстренной временной меры; надежный контракт — структурированные метаданные origin.
Внутреннее хранилище
Надежная очередь должна хранить намерения отправки сообщений, а не payload ответов.
type DurableSendIntent = { id: string; idempotencyKey: string; channel: string; accountId?: string; message: ChannelMessage; batch?: RenderedMessageBatch; liveState?: LiveMessageState; status: | "pending" | "sending" | "committing" | "unknown_after_send" | "sent" | "failed" | "cancelled"; attempt: number; nextAttemptAt?: number; receipt?: MessageReceipt; partialReceipt?: MessageReceipt; failure?: DeliveryFailure; createdAt: number; updatedAt: number;};Цикл восстановления:
load pending or sending intents -> acquire idempotency lock -> skip if receipt already committed -> reconstruct send context -> render if needed -> reconcile unknown_after_send if needed -> call adapter send/edit/finalize -> commit receipt, mark unknown_after_send, or schedule retryОчередь должна хранить достаточно идентичности, чтобы после перезапуска воспроизвести отправку через тот же аккаунт, thread, цель, политику форматирования и правила медиа.
Классы сбоев
Адаптеры каналов классифицируют транспортные сбои по закрытым категориям:
type DeliveryFailureKind = | "transient" | "rate_limit" | "auth" | "permission" | "not_found" | "invalid_payload" | "conflict" | "cancelled" | "unknown";Политика ядра:
- Повторять
transientиrate_limit. - Не повторять
invalid_payload, если не существует резервного рендеринга. - Не повторять
authилиpermissionдо изменения конфигурации. - Для
not_foundразрешить live-финализации откатиться с редактирования к новой отправке, когда канал объявляет это безопасным. - Для
conflictиспользовать правила квитанции/идемпотентности, чтобы решить, существует ли сообщение уже. - Любая ошибка после того, как адаптер мог завершить platform I/O, но до commit квитанции
становится
unknown_after_send, если адаптер не может доказать, что операция на платформе не произошла.
Сопоставление каналов
| Канал | Целевая миграция |
|---|---|
| Telegram | Получение политики подтверждений плюс устойчивые финальные отправки. Рабочий адаптер владеет отправкой плюс редактированием предпросмотра, финальной отправкой устаревшего предпросмотра, темами, пропуском предпросмотра ответа с цитированием, резервной отправкой медиа и обработкой retry-after. |
| Discord | Адаптер отправки оборачивает существующую устойчивую доставку полезной нагрузки. Рабочий адаптер владеет редактированием черновика, черновиком прогресса, отменой предпросмотра медиа/ошибок, сохранением цели ответа и получением идентификаторов сообщений. Проверьте эхо сбоев Gateway, созданные ботом, в общих комнатах; используйте исходящий реестр или другой нативный эквивалент, если Discord не может переносить метаданные происхождения в обычных сообщениях. |
| Slack | Адаптер отправки обрабатывает обычные сообщения чата. Рабочий адаптер выбирает нативный поток, когда форма треда это поддерживает, иначе использует предпросмотр черновика. Квитанции сохраняют временные метки тредов. Адаптер происхождения сопоставляет сбои Gateway OpenClaw с chat.postMessage.metadata Slack и отбрасывает помеченные эхо из комнат ботов до авторизации allowBots. |
| Адаптер отправки владеет отправкой текста/медиа с устойчивыми финальными намерениями. Адаптер получения обрабатывает упоминание группы и личность отправителя. Рабочий адаптер может отсутствовать, пока у WhatsApp не появится редактируемый транспорт. | |
| Matrix | Рабочий адаптер владеет редактированием событий черновика, финализацией, редактированием с удалением, ограничениями зашифрованных медиа и резервным поведением при несовпадении цели ответа. Адаптер получения владеет гидратацией и дедупликацией зашифрованных событий. Адаптер происхождения должен кодировать происхождение сбоя Gateway OpenClaw в содержимое события Matrix и отбрасывать эхо комнат настроенных ботов до обработки allowBots. |
| Mattermost | Рабочий адаптер владеет одним черновым постом, сворачиванием прогресса/инструментов, финализацией на месте и резервной новой отправкой. |
| Microsoft Teams | Рабочий адаптер владеет нативным прогрессом и поведением блочного потока. Адаптер отправки владеет активностями и квитанциями вложений/карточек. |
| Feishu | Адаптер рендеринга владеет рендерингом текста/карточек/сырого содержимого. Рабочий адаптер владеет потоковыми карточками и подавлением дублирующего финала. Адаптер отправки владеет комментариями, тематическими сессиями, медиа и подавлением голосовых сообщений. |
| QQ Bot | Рабочий адаптер владеет потоковой передачей C2C, тайм-аутом накопителя и резервной финальной отправкой. Адаптер рендеринга владеет медиа-тегами и текстом как голосом. |
| Signal | Простой адаптер получения плюс адаптер отправки. Без рабочего адаптера, если signal-cli не добавит надежную поддержку редактирования. |
| iMessage | Простой адаптер получения плюс адаптер отправки. Отправка iMessage должна сохранять заполнение эхо-кэша монитора, прежде чем устойчивые финалы смогут обходить доставку через монитор. |
| Google Chat | Простой адаптер получения плюс адаптер отправки, где связь треда сопоставлена с пространствами и идентификаторами тредов. Проверьте поведение комнаты с allowBots=true для помеченных эхо сбоев Gateway OpenClaw. |
| LINE | Простой адаптер получения плюс адаптер отправки, где ограничения токена ответа смоделированы как возможность цели/связи. |
| Nextcloud Talk | Мост получения SDK плюс адаптер отправки. |
| IRC | Простой адаптер получения плюс адаптер отправки, без устойчивых квитанций редактирования. |
| Nostr | Адаптер получения плюс адаптер отправки для зашифрованных личных сообщений; квитанциями являются идентификаторы событий. |
| QA-канал | Адаптер контрактных тестов для поведения получения, отправки, рабочего режима, повторов и восстановления. |
| Synology Chat | Простой адаптер получения плюс адаптер отправки. |
| Tlon | Адаптер отправки должен сохранять рендеринг подписи модели и отслеживание тредов с участием, прежде чем будет включена универсальная устойчивая финальная доставка. |
| Twitch | Простой адаптер получения плюс адаптер отправки с классификацией ограничений частоты. |
| Zalo | Простой адаптер получения плюс адаптер отправки. |
| Zalo Personal | Простой адаптер получения плюс адаптер отправки. |
План миграции
Фаза 1: Внутренний домен сообщений
- Добавьте типы
src/channels/message/*для сообщений, целей, связей, происхождений, квитанций, возможностей, устойчивых намерений, контекста получения, контекста отправки, рабочего контекста и классов сбоев. - Добавьте
origin?: MessageOriginв тип полезной нагрузки миграционного моста, используемый текущей доставкой ответов, затем перенесите это поле вChannelMessageи типы отрендеренных сообщений по мере того, как рефакторинг заменяет полезные нагрузки ответов. - Держите это внутренним, пока адаптеры и тесты не подтвердят форму.
- Добавьте чистые модульные тесты для переходов состояний и сериализации.
Фаза 2: Ядро устойчивой отправки
- Перенесите существующую исходящую очередь с устойчивости полезной нагрузки ответа на устойчивые намерения отправки сообщений.
- Позвольте устойчивому намерению отправки нести массив спроецированных полезных нагрузок или план пакета, а не только одну полезную нагрузку ответа.
- Сохраните текущее поведение восстановления очереди через совместимое преобразование.
- Сделайте так, чтобы
deliverOutboundPayloadsвызывалmessages.send. - Сделайте устойчивость финальной отправки поведением по умолчанию и закрывайте сбоем, когда устойчивое намерение нельзя записать в новом жизненном цикле сообщения, после того как адаптер объявит безопасность воспроизведения. Существующие пути входящего раннера и совместимости SDK остаются прямой отправкой по умолчанию на этой фазе.
- Последовательно записывайте квитанции.
- Возвращайте квитанции и результаты доставки исходному вызывающему диспетчера, вместо того чтобы рассматривать устойчивую отправку как конечный побочный эффект.
- Сохраняйте происхождение сообщения через устойчивые намерения отправки, чтобы восстановление, воспроизведение и отправки частями сохраняли операционное происхождение OpenClaw.
Фаза 3: Мост входящих сообщений канала
- Повторно реализуйте
channel.inbound.runиdispatchChannelInboundReplyповерхmessages.receiveиmessages.send. - Сохраните стабильность текущих типов фактов.
- Сохраните прежнее поведение по умолчанию. Канал с собранным ходом становится устойчивым только тогда, когда его адаптер явно подключается с политикой устойчивости, безопасной для воспроизведения.
- Сохраните
durable: falseкак совместимый аварийный выход для путей, которые финализируют нативные редактирования и пока не могут безопасно воспроизводиться, но не полагайтесь на маркерыfalseдля защиты немигрированных каналов. - Включайте устойчивость собранного хода по умолчанию только в новом жизненном цикле сообщений, после того как сопоставление канала докажет, что универсальный путь отправки сохраняет прежнюю семантику доставки канала.
Фаза 4: Мост подготовленного диспетчера
- Заменить
deliverDurableInboundReplyPayloadмостом с контекстом отправки. - Сохранить старый помощник как обертку.
- Сначала перенести Telegram, WhatsApp, Slack, Signal, iMessage и Discord, потому что у них уже есть работа с устойчивыми окончательными ответами или более простые пути отправки.
- Считать каждый подготовленный диспетчер непокрытым, пока он явно не подключится к контексту отправки. В документации и записях changelog нужно писать «собранные ходы канала» или называть перенесенные пути каналов, а не заявлять обо всех автоматических окончательных ответах.
- Сохранить поведение
recordInboundSessionAndDispatchReply, помощников прямых DM и похожих публичных помощников совместимости. Позже они могут открыть явное подключение к контексту отправки, но не должны автоматически пытаться выполнить универсальную устойчивую доставку до callback доставки, которым владеет вызывающая сторона.
Фаза 5: единый жизненный цикл live
- Построить
messages.liveс двумя адаптерами доказательства:- Telegram для отправки, редактирования и отправки устаревшего окончательного сообщения.
- Matrix для финализации черновика и fallback редактирования.
- Затем перенести Discord, Slack, Mattermost, Teams, QQ Bot и Feishu.
- Удалять дублированный код финализации preview только после того, как у каждого канала появятся тесты паритета.
Фаза 6: публичный SDK
- Добавить
openclaw/plugin-sdk/channel-outbound. - Задокументировать его как предпочтительный API plugin канала.
- Обновить экспорты пакета, инвентарь entrypoint, сгенерированные базовые линии API и документацию SDK plugin.
- Включить
MessageOrigin, хуки кодирования/декодирования origin и общий предикатshouldDropOpenClawEchoв поверхность SDK channel-outbound. - Сохранить обертки совместимости для старых подпутей.
- Пометить SDK-помощники с reply в названии как устаревшие в документации после миграции встроенных plugins.
Фаза 7: все отправители
Перенести всех исходящих производителей не-ответов на messages.send:
- уведомления cron и heartbeat
- завершения задач
- результаты hook
- запросы подтверждения и результаты подтверждения
- отправки инструмента сообщений
- объявления о завершении subagent
- явные отправки CLI или Control UI
- пути автоматизации/рассылки
Здесь модель перестает быть «ответами агента» и становится «OpenClaw отправляет сообщения».
Фаза 8: удаление совместимости с названиями turn
- Сохранить обертки с inbound/message в названии как окно совместимости.
- Опубликовать заметки о миграции.
- Запустить тесты совместимости SDK plugin со старыми импортами.
- Удалять или скрывать старые внутренние помощники только после того, как они больше не нужны ни одному встроенному plugin и у сторонних контрактов есть стабильная замена.
План тестирования
Модульные тесты:
- Сериализация и восстановление намерения устойчивой отправки.
- Повторное использование ключа идемпотентности и подавление дублей.
- Коммит receipt и пропуск replay.
- Восстановление
unknown_after_send, которое выполняет сверку перед replay, когда адаптер поддерживает сверку. - Политика классификации ошибок.
- Упорядочивание политики ack при получении.
- Сопоставление связей для отправок reply, followup, system и broadcast.
- Фабрика origin для ошибки Gateway и предикат
shouldDropOpenClawEcho. - Сохранение origin через нормализацию payload, chunking, сериализацию устойчивой очереди и восстановление.
Интеграционные тесты:
- Простой адаптер
channel.inbound.runпо-прежнему записывает и отправляет. - Доставка legacy assembled-event не становится устойчивой, если канал явно не подключился.
- Мост
channel.inbound.runPreparedReplyпо-прежнему записывает и финализирует. - Публичные помощники совместимости по умолчанию вызывают callback доставки, которым владеет вызывающая сторона, и не выполняют generic-send до этих callback.
- Устойчивая fallback-доставка воспроизводит весь массив спроецированных payload после перезапуска и не может оставить поздние payload незаписанными после раннего сбоя.
- Устойчивая доставка assembled-event возвращает идентификаторы сообщений платформы буферизованному диспетчеру.
- Пользовательские delivery hooks по-прежнему возвращают идентификаторы сообщений платформы, когда устойчивая доставка отключена или недоступна.
- Окончательный ответ переживает перезапуск между завершением assistant и отправкой на платформу.
- Черновик preview финализируется на месте, когда это разрешено.
- Черновик preview отменяется или редактируется, когда media/error/несоответствие reply-target требует обычной доставки.
- Block streaming и preview streaming не доставляют один и тот же текст одновременно.
- Media, отправленные рано через streaming, не дублируются в окончательной доставке.
Тесты каналов:
- Ответ в теме Telegram с polling ack, отложенным до безопасной completed watermark контекста получения.
- Восстановление polling Telegram для принятых, но не доставленных updates, покрытое сохраненной моделью safe-completed offset.
- Устаревший preview Telegram отправляет свежий окончательный ответ и очищает preview.
- Silent fallback Telegram отправляет каждый спроецированный fallback payload.
- Устойчивость silent fallback Telegram атомарно записывает полный спроецированный fallback array, а не одно single-payload устойчивое намерение на каждую итерацию цикла.
- Discord отменяет preview при media/error/явном reply.
- Окончательные сообщения подготовленного диспетчера Discord проходят через контекст отправки до того, как документация или changelog заявят об устойчивости окончательного ответа Discord.
- Устойчивые окончательные отправки iMessage заполняют echo cache отправленного сообщения монитора.
- Legacy-пути доставки LINE, Zalo и Nostr не обходятся generic durable send, пока не появятся тесты паритета их адаптеров.
- Доставка callback Direct-DM/Nostr остается авторитетной, если она явно не перенесена на полный target сообщения и replay-safe адаптер отправки.
- Помеченные Slack сообщения сбоя Gateway OpenClaw остаются видимыми исходящими, помеченные
echoes bot-room отбрасываются до
allowBots, а непомеченные bot messages с тем же видимым текстом по-прежнему проходят обычную авторизацию bot. - Fallback нативного stream Slack к draft preview в DM верхнего уровня.
- Финализация preview Matrix и fallback редактирования.
- Помеченные Matrix echoes room от настроенных bot
accounts для сбоя Gateway OpenClaw отбрасываются до обработки
allowBots. - Аудиты cascade сбоя Gateway в общих комнатах Discord и Google Chat покрывают
режимы
allowBotsдо заявления об универсальной защите там. - Финализация черновика Mattermost и fallback свежей отправки.
- Финализация нативного progress Teams.
- Подавление дублирующего окончательного сообщения Feishu.
- Fallback timeout аккумулятора QQ Bot.
- Устойчивые окончательные отправки Tlon сохраняют рендеринг model-signature и отслеживание участвовавшего thread.
- Простые устойчивые окончательные отправки WhatsApp, Signal, iMessage, Google Chat, LINE, IRC, Nostr, Nextcloud Talk, Synology Chat, Tlon, Twitch, Zalo и Zalo Personal.
Валидация:
- Целевые файлы Vitest во время разработки.
pnpm check:changedв Testbox для всей измененной поверхности.- Более широкий
pnpm checkв Testbox перед landing полного рефакторинга или после изменений публичного SDK/export. - Live или qa-channel smoke минимум для одного канала с поддержкой редактирования и одного простого канала только с отправкой перед удалением оберток совместимости.
Открытые вопросы
- Должен ли Telegram со временем заменить источник grammY runner полностью устойчивым polling source, который может управлять повторной доставкой на уровне платформы, а не только сохраненной watermark перезапуска OpenClaw.
- Должно ли состояние durable live preview храниться в той же записи очереди, что и намерение окончательной отправки, или в соседнем хранилище live-state.
- Как долго обертки совместимости остаются задокументированными после выпуска
plugin-sdk/channel-outbound. - Должны ли сторонние plugins реализовывать адаптеры получения напрямую или только
предоставлять хуки normalize/send/live через
defineChannelMessageAdapter. - Какие поля receipt безопасно раскрывать в публичном SDK по сравнению с внутренним состоянием runtime.
- Следует ли моделировать побочные эффекты, такие как self-echo caches и маркеры participated-thread, как хуки контекста отправки, шаги finalize, принадлежащие адаптеру, или подписчиков receipt.
- У каких каналов есть нативные метаданные origin, каким нужны сохраненные исходящие registries, а какие не могут предоставить надежное подавление cross-bot echo.
Критерии приемки
- Каждый встроенный канал сообщений отправляет окончательный видимый вывод через
messages.send. - Каждый входящий канал сообщений входит через
messages.receiveили задокументированную обертку совместимости. - Каждый канал preview/edit/stream использует
messages.liveдля состояния черновика и финализации. channel.inboundявляется только оберткой.- SDK-помощники с reply в названии являются экспортами совместимости, а не рекомендуемым путем.
- Устойчивое восстановление может воспроизводить ожидающие окончательные отправки после перезапуска без потери окончательного ответа или дублирования уже закоммиченных отправок; отправки, чей результат платформы неизвестен, сверяются перед replay или документируются как at-least-once для этого адаптера.
- Устойчивые окончательные отправки закрываются с ошибкой, когда не удается записать durable intent, если вызывающая сторона явно не выбрала задокументированный non-durable режим.
- Legacy-помощники совместимости SDK по умолчанию используют прямую доставку, принадлежащую каналу; generic durable send доступен только по явному opt-in.
- Receipts сохраняют все идентификаторы сообщений платформы для multipart-доставок и primary id для удобства threading/edit.
- Устойчивые обертки сохраняют локальные для канала побочные эффекты до замены прямых delivery callbacks.
- Подготовленные диспетчеры не считаются устойчивыми, пока их путь окончательной доставки явно не использует контекст отправки.
- Fallback-доставка обрабатывает каждый спроецированный payload.
- Устойчивая fallback-доставка записывает каждый спроецированный payload в одно replayable intent или batch plan.
- Вывод сбоя Gateway, инициированный OpenClaw, виден людям, но помеченные echoes room от bot-authored отбрасываются до авторизации bot в каналах, которые объявляют поддержку контракта origin.
- Документация объясняет отправку, получение, live, состояние, receipts, relations, failure policy, migration и test coverage.