openclaw/plugin-sdk/channel-inbound for inbound event normalization,
formatting, roots, and orchestration. Use
openclaw/plugin-sdk/channel-outbound for native send, receipt, durable
delivery, and live preview behavior.
Core helpers
buildChannelInboundEventContext(...): projects normalized channel facts into the prompt/session context. Pass channel-owned sender/chat metadata throughchannelContext, which plugin hooks see asctx.channelContext. AugmentPluginHookChannelSenderContextorPluginHookChannelChatContextfrom this subpath for channel-specific fields.runChannelInboundEvent(...): runs ingest, classify, preflight, resolve, record, dispatch, and finalize for one inbound platform event.dispatchChannelInboundReply(...): records and dispatches an already assembled inbound reply with a delivery adapter.
ChannelInboundMediaInput fact per native attachment. When an ambient
history line or another text-only carrier must describe those facts, use
formatMediaPlaceholderText(media). It classifies each fact from kind, MIME
type, then path or URL extension; undownloaded native attachments should still
contribute one type-only fact each. Do not use the formatter to synthesize the
primary inbound body.
Normalize plugin-owned attachment records with toInboundMediaFacts(...), then
pass the resulting ordered array through the context’s media field:
transcribed, messageId, and
workspaceDir replace the legacy parallel index/workspace fields. The
MediaPath, MediaPaths, MediaUrl, MediaUrls, MediaType, MediaTypes,
MediaTranscribedIndexes, MediaWorkspaceDir, and MediaStaged context fields,
plus buildChannelInboundMediaPayload(...), remain available only as deprecated
compatibility. New plugins should not construct or read them.
Bundled/native channels that already receive the injected plugin runtime
object can call the same helpers under runtime.channel.inbound.* instead of
importing this subpath directly:
dispatchChannelInboundReply(...) inputs for compatibility
dispatchers that keep platform delivery in the delivery adapter. New send
paths should use message adapters and durable message helpers from
channel-outbound instead.
Delivery settlement contract
ChannelInboundTurnPlan.delivery owns the native send for each logical reply
payload. On the routed API, core runs reply_payload_sending, calls
preparePayload, and then assigns exactly one message_sending owner:
- a declared
durablebranch runs the hook inside shared durable delivery; - a direct
deliverbranch runs the hook in core before the native adapter; - an exceptional provider funnel can use
deliverWithProviderMessageSendingwhen it must choose durable delivery or native finalization inside that funnel.
message_sending again inside a normal deliver callback. Use
the provider-owned callback only when the branch cannot be declared before
entering the provider funnel; it is mutually exclusive with deliver and
durable. Existing direct and durable plans keep using
ChannelInboundTurnPlan; explicitly type the exceptional funnel as
ChannelInboundTurnPlan<"provider_message_sending">. Caller-assembled
dispatchChannelInboundReply(...) remains the
compatibility boundary and keeps its caller-provided dispatcher ownership.
preparePayload may return null when channel policy intentionally suppresses the
logical payload. Core records a typed non-visible result and skips durable selection,
message_sending, and native delivery, so a later modifying hook cannot resurrect
content the channel rejected.
Core also owns terminal message_sent observation when the adapter opts in.
Keep these responsibilities separate so one payload cannot produce duplicate
modifier or terminal events.
The delivery result fields have these meanings:
Set the delivery adapter’s
observeMessageSent option to true when core
should emit the canonical plugin and internal message_sent events for this
adapter’s non-durable sends. Do not return this option from deliver, and do
not emit those events in the plugin too. Durable sends already emit through
the shared outbound owner and are not duplicated.
Return one result per logical payload. finalization is not a second send and
must not rerun reply_payload_sending or message_sending. As soon as
deliver returns, core observes the finalization promise’s rejection so it
cannot become unhandled; core still awaits the original promise after reply
dispatch settles. It then emits at most one terminal observation per payload
with the finalized content and provider id. onDelivered, when present,
receives the settled result after that observation.
onDelivered also receives settled suppressed results. A suppressed result
has visibleReplySent: false, does not emit message_sent, and does not count
as a visible queued reply. This lets plugins distinguish hook cancellation
from provider failure without inventing a native message identity.
By default, routed turns record inbound metadata against
ctxPayload.SessionKey ?? route.sessionKey. Set record.sessionKey only when a
native command intentionally executes in one command session while updating a
different provider-routed target session. The override affects inbound metadata,
transcript-context merge, and record-stage diagnostics; it does not change dispatch
routing or hook correlation. An explicit override must be non-empty and contain no
surrounding whitespace.
Reject deliver or finalization when native delivery fails. If no provider
send was attempted, throw PlatformMessageNotDispatchedError from
openclaw/plugin-sdk/error-runtime; core suppresses a false message_sent
event. If a native send became visible before a later operation failed,
preserve the visible subset on the error:
visibleReplySent: false after any
preview, draft, attachment, or final message became visible.
When reply_payload_sending or message_sending is registered, those hooks
must settle before anything provider-visible is created because either hook
can rewrite or cancel the logical payload. An eager native preview would leak
pre-rewrite content or leave a cancelled draft behind. Buffer preview content
until the accepted payload reaches deliver; compatibility dispatchers that
start previews earlier must suppress that eager preview while either hook is
registered. Use the finalizable live-preview helpers from
Channel outbound API for new preview paths.
Migration
runtime.channel.turn.* runtime aliases were removed. Use:
runtime.channel.inbound.run(...)for raw inbound events.runtime.channel.inbound.dispatchReply(...)for assembled reply contexts.runtime.channel.inbound.buildContext(...)for inbound context payloads.runtime.channel.inbound.runPreparedReply(...), deprecated, only for channel-owned prepared dispatch paths that already assemble their own dispatch closure.
turn-named channel APIs. Keep model or
agent turn vocabulary inside agent/provider code; channel plugins use inbound,
message, delivery, and reply terms.