跳到正文
FunCoding

搜索

搜索文档、文章、Skill 和 MCP

Channel message adapter

The channel message adapter surface: live and finalizer capabilities, progress visibility, commentary delivery, and native TTS voice delivery

Declare and wire the message adapter that core uses to send through your channel. Part of the Building channel plugins guide.

Message adapter

Expose a message adapter with defineChannelMessageAdapter from openclaw/plugin-sdk/channel-outbound. Declare only the durable final-send capabilities your native transport actually supports, backed by a contract test that proves the native side effect and returned receipt. Point text/media sends at the same transport functions the legacy outbound adapter uses. For the full API contract, capability matrix, receipt rules, live preview finalization, receive ack policy, tests, and migration table, see Channel outbound API.

If your existing outbound adapter already has the right send methods and capability metadata, derive the message adapter with createChannelMessageAdapterFromOutbound(...) instead of hand-writing another bridge. Adapter sends return MessageReceipt values. For legacy ids, derive them with listMessageReceiptPlatformIds(...) or resolveMessageReceiptPrimaryId(...) instead of keeping parallel messageIds fields.

For turn adapters that aggregate confirmed visible sends, use createAcceptedChannelDeliveryResult(...) from openclaw/plugin-sdk/channel-inbound. It combines native results followed by logical deliveryResults, including a partial-delivery error's accepted subset. A logical result's receipt takes precedence over its legacy message IDs. The result carries a receipt, messageIds (including an empty array), and visibleReplySent: true; routing fields stay in the receipt. Optional content is passed through, and kind and replyToId use the receipt builder's rules. For batches that join accepted text with newlines, use createChannelDeliveryAccumulator({ kind?, replyToId? }) from openclaw/plugin-sdk/channel-outbound. Call add(source, acceptedText?) only after each physical send succeeds. size counts accepted sends, and result() returns their combined receipt and nonempty text, or a no_visible_result suppression for an empty batch. On failure, throw partialError(error) to retain earlier sends and a nested partial-delivery error's accepted subset; failures before any acceptance pass through unchanged. Keep transport acceptance side effects, other content-joining rules, and whether an identityless outcome needs a receipt in the adapter.

Channel actions and adapter capabilities come from the selected plugin registration. An omitted actions, message, or outbound surface is not filled from another plugin with the same channel ID. Prepared delivery handlers created inside a registry scope retain that handle when invoked after the caller leaves the scope.

Declare live and finalizer capabilities precisely - core uses these to decide what a channel can do, and drift between the declared and actual behavior is a contract test failure:

SurfaceValues
message.live.capabilitiesdraftPreview, previewFinalization, progressUpdates, nativeStreaming, quietFinalization
message.live.finalizer.capabilitiesfinalEdit, normalFallback, discardPending, previewReceipt, retainOnAmbiguousFailure

Channels with previews should use createLivePreviewLifecycle(...) from openclaw/plugin-sdk/channel-outbound for final acceptance, promotion, and cleanup. Supply real transport operations and explicit delivery results instead of keeping channel-local final/cleanup flags. Native streaming and persistent cards retain their transport-specific finalization; they are not required to pretend to be deletable drafts. See Progress and preview delivery ownership.

Keep declared capabilities backed by verifyChannelMessageLiveCapabilityAdapterProofs(...) and verifyChannelMessageLiveFinalizerProofs(...) tests so native progress, edit, fallback/retention, cleanup, and receipt behavior cannot drift silently.

Progress visibility acceptance

Progress callbacks report what the operator can see, not merely what a plugin queued. Return true after accepting visible progress and false while delivery is pending or when no visible update occurred. Existing synchronous and asynchronous callbacks that return void remain backward-compatible and are treated as visible; new acceptance-aware implementations should use an explicit boolean.

Quiet progress presentation

Native progress renderers must retain approval requests when tool rows are disabled, alongside authored progress text and plan rows. Intermediate tool failures and nonzero command exits follow the tool-row visibility setting; they must not bypass quiet mode. Terminal task errors still use normal error delivery. The shared progress compositor applies this policy to its snapshots.

resolveChannelStreamingPreviewToolProgress(entry, defaultValue?, mode?) keeps its shipped default of true when the second argument is omitted or undefined. Bundled channels pass mode !== "progress" as the second argument and their resolved streaming mode as the third argument, so unconfigured progress drafts hide ordinary tool rows while partial and block previews show them.

The compositor and formatter's presentation: "summary" option and the checklist formatter's plain: true option are deprecated but retain their explicit output until the next breaking SDK release. New callers should omit them and use streaming.progress.toolProgress to control tool rows with the standard progress markers.

When consuming prepared agent items, create the compositor with preparedItems: true. pushItemEvent then owns visible tool progress; raw tool, command-output, and patch callbacks retain diagnostic bookkeeping without adding duplicate rows. Omit this option for existing plugins that use raw callbacks. Their arguments, detail mode, custom line builder, and terminal command/patch rendering remain supported. This is an adapter capability, not a user configuration setting.

Telegram and Discord additionally pass showWorkStatus: true. With the detailed tool log hidden, the compositor keeps one current-operation status and bounded subagent status rows from prepared items. This projection excludes arguments, command titles, output, and private child prose; it retains the real item IDs for updates and retractions. Other adapters keep their existing presentation unless they opt into this capability.

For default-on drafts, pass progressRequiresReply: true in reply options. Dispatch uses its already-admitted reply expectation to keep optional quiet turns private; queued turns use their own expectation, not the preceding turn’s. This does not suppress required durable tool results such as approvals or media.

Quiet acknowledgement and coalesced progress

createStatusReactionController({ presentation: "acknowledgement", ... }) keeps the initial reaction through work and success, skips inactivity warnings, and retains the existing error/cleanup lifecycle. The default activity policy continues to expose detailed lifecycle reactions.

For edited or native progress, createDraftStreamLoop and finalizable draft controls accept coalesceInFlight: true to keep background updates arriving during a send in the next throttle window. Explicit flush() still bypasses the delay for attention and finalization. Cancel pending updates and await in-flight work before closing or rotating a stream.

Use createFinalizableDraftLifecycle for physical deletion custody rather than maintaining a plugin-local retry queue. retire(id) claims a detached preview; retire(id, { defer: true }) records it without deleting it immediately. cleanupPending() retries retired IDs without deleting the current preview. When transport cleanup policy must change, pass a synchronous prepareCleanup callback to cleanupPending; it runs in order with clears, before deletion. Rejected deletions remain owned for a later cleanup attempt.

For synchronous turn rotation, reset() advances generation, reopens delivery, and resets the current message and pending updates. Published messages remain the adapter's responsibility. reset("discard") also retires creates from earlier generations when they settle. Call createMessage(send, publish) inside the serialized send loop; its synchronous publish callback installs only current-generation receipts. Capture generation before edits and recheck it before publishing their results. retireCurrent(stopForClear) similarly fences awaited cleanup. Adapters that already settle their sends before rotation can use resetMessage() to reset identity and pending/throttle state without reopening delivery or advancing the generation.

Pass "keep" as the throttle argument to resetMessage("keep") or reset("discard", "keep") when rotation must preserve the existing throttle window and scheduled flush. The default resets both. Transports that decide stale-preview disposition during cleanup can pass { defer: true } as the third argument to createMessage; stale discarded receipts then enter deletion custody without an immediate deletion attempt.

Commentary delivery ownership

Set commentaryPayloadsEnabled: true when the channel supports durable commentary messages. Channels that normally render commentary in one evolving progress draft can also provide shouldDeliverCommentaryPayloads. Core freezes verbose visibility for the turn, registers that getter through onVerboseProgressVisibility, evaluates the delivery callback once before dispatch, and snapshots that result for the whole turn. Session changes apply on the next turn. The callback is inert unless commentaryPayloadsEnabled is also true; without that static opt-in, core neither evaluates the callback nor freezes the registered visibility getter.

Return false while the draft owns normal progress and true when verbose progress makes that draft yield to durable commentary. Keep the callback synchronous and read only channel-owned, already prepared state. Omitting it preserves durable delivery for existing plugins that use the static opt-in. The callback does not control reasoning, partial replies, tool progress, or final answers.

Inbound receivers that defer platform acknowledgements should declare message.receive.defaultAckPolicy and supportedAckPolicies instead of hiding ack timing in monitor-local state. Cover every declared policy with verifyChannelMessageReceiveAckPolicyAdapterProofs(...).

TTS voice delivery

Declare native voice-note behavior under capabilities.tts.voice. Set synthesisTarget: "voice-note" when TTS providers should produce a native voice-note format. Set captionedFinalText: true only when the outbound voice operation accepts visible final text and enforces its transport's caption and overflow rules. Core then holds final-mode streamed text for that operation and falls back to text when the voice payload is proven unsent.

The legacy dispatchInboundReplyWithBase helper remains available from the deprecated openclaw/plugin-sdk/inbound-reply-dispatch compatibility shim. Do not use it for new channel code; start with the message adapter, receipts, and receive/send lifecycle helpers on openclaw/plugin-sdk/channel-outbound instead.