mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
docs: document channel message plugin api
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -59,6 +59,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
|
||||
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
|
||||
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
|
||||
- Plugin SDK: add `openclaw/plugin-sdk/channel-message` lifecycle helpers for `defineChannelMessageAdapter`, `deliverInboundReplyWithMessageSendContext`, send/receive/live/state contracts, durable final-delivery capability derivation, capability proof helpers, and normalized message receipts.
|
||||
- Plugin SDK: add `createChannelMessageAdapterFromOutbound` so channel plugins can derive durable message adapters from proven outbound adapters without duplicating send/receipt bridge code.
|
||||
- Plugin SDK: add `actions.prepareSendPayload(...)` so channel plugins can shape message-tool sends into durable payloads while core owns queueing, hooks, retry, recovery, and acknowledgements.
|
||||
- Plugin SDK: make the legacy `channel-reply-pipeline` subpath a compatibility wrapper over the shared reply core while steering root compat deprecations toward `plugin-sdk/channel-message`.
|
||||
- Plugin SDK: move Discord, Slack, Mattermost, and Matrix live-preview finalization onto `plugin-sdk/channel-message` and attach message receipts to Telegram finalized previews plus Teams native stream finals, so preview edits and stream finals are represented in the message lifecycle instead of draft-only helpers.
|
||||
- Telegram: persist the polling restart watermark after successful update dispatch instead of at handler entry, leaving failed updates retryable while still coalescing completed offsets safely.
|
||||
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
|
||||
- Agents/verbose: use compact explain-mode tool summaries for `/verbose` and progress drafts by default, with `agents.defaults.toolProgressDetail: "raw"` and per-agent overrides for debugging raw command/detail output.
|
||||
- Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure.
|
||||
@@ -118,6 +124,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Doctor/Codex: repair legacy `openai-codex/*` routes in primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel overrides, and stale session pins to canonical `openai/*`, selecting `agentRuntime.id: "codex"` only when the Codex plugin is installed, enabled, contributes the `codex` harness, and has usable OAuth; otherwise select `agentRuntime.id: "pi"`. Thanks @vincentkoc.
|
||||
- Video generation: wait up to 20 minutes for slow fal/MiniMax queue-backed jobs, stop forwarding unsupported Google Veo generated-audio options, and normalize MiniMax `720P` requests to its supported `768P` resolution with the usual override warning/details instead of failing fallback.
|
||||
- Video generation: accept provider-specific aspect-ratio and resolution hints at the tool boundary, normalize `720P` to MiniMax's supported `768P`, and stop sending Google `generateAudio` on Gemini video requests so provider fallback can recover from model-specific parameter differences. Thanks @vincentkoc.
|
||||
- Channels/durable delivery: preserve channel-specific final reply semantics when using durable sends, including Telegram selected quotes and silent error replies plus WhatsApp message-sending cancellations.
|
||||
- Channels/message lifecycle: build legacy channel delivery results from message receipts and add receipts to BlueBubbles, Feishu, Google Chat, iMessage, IRC, LINE, Nextcloud Talk, QQ Bot, Signal, Synology Chat, Tlon, Twitch, WhatsApp, Zalo, and Zalo Personal send results and owner-path reply delivery plus Discord, Matrix, Mattermost, Slack, and Teams send results while preserving existing message id compatibility.
|
||||
- iMessage: run durable final replies through the iMessage outbound sanitizer before sending, matching direct auto-reply delivery and preventing assistant-internal scaffolding from leaking through queued delivery.
|
||||
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
|
||||
- Hooks/session-memory: run reset memory capture off the command reply path and make model-generated memory filename slugs opt-in with `llmSlug: true`, so `/new` and `/reset` no longer block WhatsApp and other message-channel reset replies on hook housekeeping or a nested model call. Thanks @vincentkoc.
|
||||
- CLI/plugins: handle closed stdin during `plugins uninstall` confirmation prompt and exit 1 with actionable `--force` guidance instead of crashing with Node exit 13 unsettled top-level await. Fixes #73562. (#73566) Thanks @ai-hpc.
|
||||
@@ -339,6 +348,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugin tools: honor explicit tool denylists while selecting plugin tool runtimes, so denied plugin tools are not materialized for direct command or gateway surfaces before later policy filtering. Thanks @vincentkoc.
|
||||
- Plugin tools: filter factory-returned tools by manifest per-tool optional policy, so optional sibling tools from a shared runtime factory stay hidden unless explicitly allowed. Thanks @vincentkoc.
|
||||
- Agents/transcripts: retry context-overflow compaction from the current transcript only after the inbound user turn was actually persisted, and keep WebChat agent-run live delivery from writing duplicate Pi-managed assistant turns. Fixes #76424. (#77033)
|
||||
- Messaging: queue assembled channel-turn final replies before sending to reduce response loss when the gateway restarts between assistant completion and channel delivery. Refs #77000.
|
||||
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.
|
||||
- Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595.
|
||||
- Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
2164ea491c61e643f0a9c68f7b9bd2e41ab338eb93bbdf301da2fae548722581 plugin-sdk-api-baseline.json
|
||||
c07c3719218a12482e2a76e6b9654da2ddddf75d8d70145cdaef3da2b2eaccef plugin-sdk-api-baseline.jsonl
|
||||
fe061b6f35adb2b152d8f48244a94d4934b335143cc5f5aebb8cc96e5ba8b287 plugin-sdk-api-baseline.json
|
||||
495248d5981456192aaf7da2ed23d5951eaa6d9e59d70c716ab91c3da3620e73 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -27,6 +27,14 @@
|
||||
"source": "OpenClaw App SDK API design",
|
||||
"target": "OpenClaw 应用 SDK API 设计"
|
||||
},
|
||||
{
|
||||
"source": "Message lifecycle refactor",
|
||||
"target": "消息生命周期重构"
|
||||
},
|
||||
{
|
||||
"source": "Channel message API",
|
||||
"target": "频道消息 API"
|
||||
},
|
||||
{
|
||||
"source": "Azure Speech",
|
||||
"target": "Azure Speech"
|
||||
@@ -727,6 +735,18 @@
|
||||
"source": "Codex Harness Context Engine Port",
|
||||
"target": "Codex Harness Context Engine Port"
|
||||
},
|
||||
{
|
||||
"source": "Plugin refactor plan",
|
||||
"target": "插件重构计划"
|
||||
},
|
||||
{
|
||||
"source": "Retry policy",
|
||||
"target": "重试策略"
|
||||
},
|
||||
{
|
||||
"source": "Channel turn kernel",
|
||||
"target": "频道轮次内核"
|
||||
},
|
||||
{
|
||||
"source": "/gateway/configuration#strict-validation",
|
||||
"target": "/gateway/configuration#strict-validation"
|
||||
|
||||
@@ -756,6 +756,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
<Accordion title="Long polling vs webhook">
|
||||
Default is long polling. For webhook mode set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret`; optional `webhookPath`, `webhookHost`, `webhookPort` (defaults `/telegram-webhook`, `127.0.0.1`, `8787`).
|
||||
|
||||
In long-polling mode OpenClaw persists its restart watermark only after an update dispatches successfully. If a handler fails, that update remains retryable in the same process and is not written as completed for restart dedupe.
|
||||
|
||||
The local listener binds to `127.0.0.1:8787`. For public ingress, either put a reverse proxy in front of the local port or set `webhookHost: "0.0.0.0"` intentionally.
|
||||
|
||||
Webhook mode validates request guards, the Telegram secret token, and the JSON body before returning `200` to Telegram.
|
||||
|
||||
1128
docs/concepts/message-lifecycle-refactor.md
Normal file
1128
docs/concepts/message-lifecycle-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -206,6 +206,7 @@ parent stays quiet until the child completion event delivers the real reply.
|
||||
|
||||
## Related
|
||||
|
||||
- [Message lifecycle refactor](/concepts/message-lifecycle-refactor) - target durable send and receive design
|
||||
- [Streaming](/concepts/streaming) — real-time message delivery
|
||||
- [Retry](/concepts/retry) — message delivery retry behavior
|
||||
- [Queue](/concepts/queue) — message processing queue
|
||||
|
||||
@@ -242,6 +242,7 @@ Use the same shape under another compact progress channel key, for example `chan
|
||||
|
||||
## Related
|
||||
|
||||
- [Message lifecycle refactor](/concepts/message-lifecycle-refactor) - target shared preview, edit, stream, and finalization design
|
||||
- [Progress drafts](/concepts/progress-drafts) — visible work-in-progress messages that update during long turns
|
||||
- [Messages](/concepts/messages) — message lifecycle and delivery
|
||||
- [Retry](/concepts/retry) — retry behavior on delivery failure
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"group": "Messages and delivery",
|
||||
"pages": [
|
||||
"concepts/messages",
|
||||
"concepts/message-lifecycle-refactor",
|
||||
"concepts/streaming",
|
||||
"concepts/progress-drafts",
|
||||
"concepts/retry",
|
||||
@@ -1205,6 +1206,7 @@
|
||||
"plugins/building-plugins",
|
||||
"plugins/hooks",
|
||||
"plugins/sdk-channel-plugins",
|
||||
"plugins/sdk-channel-message",
|
||||
"plugins/sdk-provider-plugins",
|
||||
"plugins/adding-capabilities",
|
||||
"plugins/compatibility",
|
||||
|
||||
@@ -466,7 +466,7 @@ Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-a
|
||||
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
|
||||
```
|
||||
|
||||
To inspect saved profiles without dumping secrets, run `openclaw models auth list` (optionally `--provider <id>` or `--json`). See [Models CLI](/cli/models#openclaw-models-auth-list) for details.
|
||||
To inspect saved profiles without dumping secrets, run `openclaw models auth list` (optionally `--provider <id>` or `--json`). See [Models CLI](/cli/models#auth-profiles) for details.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
424
docs/plugins/sdk-channel-message.md
Normal file
424
docs/plugins/sdk-channel-message.md
Normal file
@@ -0,0 +1,424 @@
|
||||
---
|
||||
summary: "Message lifecycle API for channel plugins, including durable sends, receipts, live preview, receive ack policy, and legacy migration"
|
||||
title: "Channel message API"
|
||||
read_when:
|
||||
- You are building or refactoring a messaging channel plugin
|
||||
- You need durable final reply delivery, receipts, live preview finalization, or receive acknowledgement policy
|
||||
- You are migrating from legacy reply pipeline or inbound reply dispatch helpers
|
||||
---
|
||||
|
||||
# Channel Message API
|
||||
|
||||
Channel plugins should expose one `message` adapter from
|
||||
`openclaw/plugin-sdk/channel-message`. The adapter describes the native message
|
||||
lifecycle that the platform supports:
|
||||
|
||||
```text
|
||||
receive -> route and record -> agent turn -> durable final send
|
||||
send -> render batch -> platform I/O -> receipt -> lifecycle side effects
|
||||
live preview -> final edit or fallback -> receipt
|
||||
```
|
||||
|
||||
Core owns queueing, durability, generic retry policy, hooks, receipts, and the
|
||||
shared `message` tool. The plugin owns native send/edit/delete calls, target
|
||||
normalization, platform threading, selected quotes, notification flags, account
|
||||
state, and platform-specific side effects.
|
||||
|
||||
Use this page together with [Building channel plugins](/plugins/sdk-channel-plugins).
|
||||
|
||||
The `channel-message` subpath is intentionally cheap enough for hot plugin
|
||||
bootstrap files such as `channel.ts`: it exposes adapter contracts, capability
|
||||
proofs, receipts, and compatibility facades without loading outbound delivery.
|
||||
Runtime delivery helpers are available from
|
||||
`openclaw/plugin-sdk/channel-message-runtime` for monitor/send code paths that
|
||||
are already doing asynchronous message I/O.
|
||||
|
||||
## Minimal Adapter
|
||||
|
||||
Most new channel plugins can start with a small adapter:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
defineChannelMessageAdapter,
|
||||
createMessageReceiptFromOutboundResults,
|
||||
} from "openclaw/plugin-sdk/channel-message";
|
||||
|
||||
export const demoMessageAdapter = defineChannelMessageAdapter({
|
||||
id: "demo",
|
||||
durableFinal: {
|
||||
capabilities: {
|
||||
text: true,
|
||||
replyTo: true,
|
||||
thread: true,
|
||||
messageSendingHooks: true,
|
||||
},
|
||||
},
|
||||
send: {
|
||||
text: async ({ cfg, to, text, accountId, replyToId, threadId, signal }) => {
|
||||
const sent = await sendDemoMessage({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: threadId == null ? undefined : String(threadId),
|
||||
signal,
|
||||
});
|
||||
|
||||
return {
|
||||
receipt: createMessageReceiptFromOutboundResults({
|
||||
results: [{ channel: "demo", messageId: sent.id, conversationId: to }],
|
||||
kind: "text",
|
||||
threadId: threadId == null ? undefined : String(threadId),
|
||||
replyToId: replyToId ?? undefined,
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then attach it to the channel plugin:
|
||||
|
||||
```typescript
|
||||
export const demoPlugin = createChatChannelPlugin({
|
||||
base: {
|
||||
id: "demo",
|
||||
message: demoMessageAdapter,
|
||||
// other channel plugin fields
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Only declare capabilities that the adapter really preserves. Every declared
|
||||
capability should have a contract test.
|
||||
|
||||
## Outbound Bridge
|
||||
|
||||
If the channel already has a compatible `outbound` adapter, prefer deriving the
|
||||
message adapter instead of duplicating send code:
|
||||
|
||||
```typescript
|
||||
import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-message";
|
||||
|
||||
const demoMessageAdapter = createChannelMessageAdapterFromOutbound({
|
||||
id: "demo",
|
||||
outbound: demoOutboundAdapter,
|
||||
});
|
||||
```
|
||||
|
||||
The bridge converts old outbound send results into `MessageReceipt` values. New
|
||||
code should pass receipts end to end and only derive legacy ids at compatibility
|
||||
edges with `listMessageReceiptPlatformIds(...)` or
|
||||
`resolveMessageReceiptPrimaryId(...)`.
|
||||
If no receive policy is supplied, `createChannelMessageAdapterFromOutbound(...)`
|
||||
uses `manual` receive acknowledgement policy. That makes plugin-owned platform
|
||||
acknowledgement explicit without changing channels that acknowledge webhooks,
|
||||
sockets, or polling offsets outside generic receive context.
|
||||
|
||||
## Message Tool Sends
|
||||
|
||||
The shared `message(action="send")` path should use the same core delivery
|
||||
lifecycle as final replies. If a channel needs provider-specific shaping for the
|
||||
tool send, implement `actions.prepareSendPayload(...)` instead of sending from
|
||||
`actions.handleAction(...)`.
|
||||
|
||||
`prepareSendPayload(...)` receives the normalized core `ReplyPayload` plus the
|
||||
full action context. Return a payload with channel-specific data in
|
||||
`payload.channelData.<channel>` and let core call `sendMessage(...)`,
|
||||
`deliverOutboundPayloads(...)`, the write-ahead queue, message-sending hooks,
|
||||
retry, recovery, and ack cleanup.
|
||||
|
||||
Return `null` only when the send cannot be represented as a durable payload, for
|
||||
example because it contains a non-serializable component factory. Core will keep
|
||||
the legacy plugin action fallback for compatibility, but new channel send
|
||||
features should be expressible as durable payload data.
|
||||
|
||||
```typescript
|
||||
export const demoActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: () => ({ actions: ["send"], capabilities: ["presentation"] }),
|
||||
prepareSendPayload: ({ ctx, payload }) => {
|
||||
if (ctx.action !== "send") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
channelData: {
|
||||
...payload.channelData,
|
||||
demo: {
|
||||
...(payload.channelData?.demo as object | undefined),
|
||||
nativeCard: ctx.params.card,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The outbound adapter then reads `payload.channelData.demo` inside `sendPayload`.
|
||||
This keeps platform-specific rendering in the plugin while core still owns
|
||||
persist, retry, recover, hooks, and ack.
|
||||
|
||||
Prepared `message(action="send")` payloads and generic final-reply delivery use
|
||||
core delivery with best-effort queueing by default. Required durable queueing is
|
||||
only valid after core verifies the channel can reconcile a send whose outcome is
|
||||
unknown after a crash. If the adapter cannot implement `reconcileUnknownSend`,
|
||||
keep the prepared send path best-effort; core will still try the write-ahead
|
||||
queue, but queue persistence or uncertain crash recovery is not part of the
|
||||
required delivery contract.
|
||||
|
||||
## Durable Final Capabilities
|
||||
|
||||
Durable final delivery is opt in per side effect. Core will only use generic
|
||||
durable delivery when the adapter declares every capability needed by the
|
||||
payload and delivery options.
|
||||
|
||||
| Capability | Declare when |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `text` | The adapter can send text and return a receipt. |
|
||||
| `media` | Media sends return receipts for every visible platform message. |
|
||||
| `payload` | The adapter preserves rich reply payload semantics, not only text and one media URL. |
|
||||
| `replyTo` | Native reply targets reach the platform. |
|
||||
| `thread` | Native thread, topic, or channel thread targets reach the platform. |
|
||||
| `silent` | Notification suppression reaches the platform. |
|
||||
| `nativeQuote` | Selected quote metadata reaches the platform. |
|
||||
| `messageSendingHooks` | Core message-sending hooks can cancel or rewrite content before platform I/O. |
|
||||
| `batch` | Multi-part rendered batches are replayable as one durable plan. |
|
||||
| `reconcileUnknownSend` | The adapter can resolve `unknown_after_send` recovery without blind replay. |
|
||||
| `afterSendSuccess` | Channel-local after-send side effects run once. |
|
||||
| `afterCommit` | Channel-local after-commit side effects run once. |
|
||||
|
||||
Best-effort final delivery does not require `reconcileUnknownSend`; it uses the
|
||||
shared lifecycle when the adapter preserves the payload's visible semantics, and
|
||||
falls back to direct platform I/O if queue persistence is unavailable. Required
|
||||
durable final delivery must explicitly require `reconcileUnknownSend`. If the
|
||||
adapter cannot determine whether a started/unknown send reached the platform,
|
||||
do not declare that capability; core will reject required durable delivery
|
||||
before queueing.
|
||||
|
||||
When a caller needs durable delivery, derive requirements instead of building
|
||||
maps by hand:
|
||||
|
||||
```typescript
|
||||
import { deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-message";
|
||||
|
||||
const requiredCapabilities = deriveDurableFinalDeliveryRequirements({
|
||||
payload,
|
||||
replyToId,
|
||||
threadId,
|
||||
silent,
|
||||
payloadTransport: true,
|
||||
extraCapabilities: {
|
||||
nativeQuote: hasSelectedQuote(payload),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`messageSendingHooks` is required by default. Set `messageSendingHooks: false`
|
||||
only for a path that intentionally cannot run global message-sending hooks.
|
||||
|
||||
## Durable Send Contract
|
||||
|
||||
A durable final send has stricter semantics than legacy channel-owned delivery:
|
||||
|
||||
- Create the durable intent before platform I/O.
|
||||
- If durable delivery returns a handled result, do not fall back to legacy send.
|
||||
- Treat hook cancellation and no-send results as terminal.
|
||||
- Treat `unsupported` as a pre-intent result only.
|
||||
- For required durability, fail before platform I/O if the queue cannot record
|
||||
that platform send has started.
|
||||
- For required final delivery and required prepared message-tool sends,
|
||||
preflight `reconcileUnknownSend`; recovery must be able to ack an
|
||||
already-sent message or replay only after the adapter proves the original send
|
||||
did not happen.
|
||||
- For `best_effort`, queue write failures may fall back to direct platform I/O.
|
||||
- Forward abort signals to media loading and platform sends.
|
||||
- Run after-commit hooks after queue ack; direct best-effort fallback runs them
|
||||
after successful platform I/O because there is no durable queue commit.
|
||||
- Return receipts for every visible platform message id.
|
||||
- Use `reconcileUnknownSend` when a platform can check whether an uncertain send
|
||||
already reached the user.
|
||||
|
||||
This contract avoids duplicate sends after crashes and avoids bypassing
|
||||
message-sending cancellation hooks.
|
||||
|
||||
## Receipts
|
||||
|
||||
`MessageReceipt` is the new internal record of what the platform accepted:
|
||||
|
||||
```typescript
|
||||
type MessageReceipt = {
|
||||
primaryPlatformMessageId?: string;
|
||||
platformMessageIds: string[];
|
||||
parts: MessageReceiptPart[];
|
||||
threadId?: string;
|
||||
replyToId?: string;
|
||||
editToken?: string;
|
||||
deleteToken?: string;
|
||||
sentAt: number;
|
||||
raw?: readonly MessageReceiptSourceResult[];
|
||||
};
|
||||
```
|
||||
|
||||
Use `createMessageReceiptFromOutboundResults(...)` when adapting an existing
|
||||
send result. Use `createPreviewMessageReceipt(...)` when a live preview message
|
||||
becomes the final receipt. Avoid adding new owner-local `messageIds` fields.
|
||||
Legacy `ChannelDeliveryResult.messageIds` is still produced at compatibility
|
||||
edges.
|
||||
|
||||
## Live Preview
|
||||
|
||||
Channels that stream draft previews or progress updates should declare live
|
||||
capabilities:
|
||||
|
||||
```typescript
|
||||
const demoMessageAdapter = defineChannelMessageAdapter({
|
||||
id: "demo",
|
||||
live: {
|
||||
capabilities: {
|
||||
draftPreview: true,
|
||||
previewFinalization: true,
|
||||
progressUpdates: true,
|
||||
quietFinalization: true,
|
||||
},
|
||||
finalizer: {
|
||||
capabilities: {
|
||||
finalEdit: true,
|
||||
normalFallback: true,
|
||||
discardPending: true,
|
||||
previewReceipt: true,
|
||||
retainOnAmbiguousFailure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Use `defineFinalizableLivePreviewAdapter(...)` and
|
||||
`deliverWithFinalizableLivePreviewAdapter(...)` for runtime finalization. The
|
||||
finalizer decides whether the final reply edits the preview in place, sends a
|
||||
normal fallback, discards pending preview state, keeps an ambiguous failed edit
|
||||
without duplicating the message, and returns the final receipt.
|
||||
|
||||
## Receive Ack Policy
|
||||
|
||||
Inbound receivers that control platform acknowledgement timing should declare
|
||||
receive policy:
|
||||
|
||||
```typescript
|
||||
const demoMessageAdapter = defineChannelMessageAdapter({
|
||||
id: "demo",
|
||||
receive: {
|
||||
defaultAckPolicy: "after_agent_dispatch",
|
||||
supportedAckPolicies: ["after_receive_record", "after_agent_dispatch"],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Adapters that do not declare receive policy default to:
|
||||
|
||||
```typescript
|
||||
{
|
||||
receive: {
|
||||
defaultAckPolicy: "manual",
|
||||
supportedAckPolicies: ["manual"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use the default when the platform has no acknowledgement to defer, already
|
||||
acknowledges before asynchronous processing, or needs protocol-specific response
|
||||
semantics. Declare one of the staged policies only when the receiver actually
|
||||
uses receive context to move platform acknowledgement later.
|
||||
|
||||
Policies:
|
||||
|
||||
| Policy | Use when |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `after_receive_record` | The platform can be acknowledged after the inbound event is parsed and recorded. |
|
||||
| `after_agent_dispatch` | The platform should wait until the agent dispatch has been accepted. |
|
||||
| `after_durable_send` | The platform should wait until final delivery has a durable decision. |
|
||||
| `manual` | The plugin owns acknowledgement because platform semantics do not match a generic stage. |
|
||||
|
||||
Use `createMessageReceiveContext(...)` in receivers that defer ack state, and
|
||||
`shouldAckMessageAfterStage(...)` when the receiver needs to test whether a
|
||||
stage has satisfied the configured policy.
|
||||
|
||||
## Contract Tests
|
||||
|
||||
Capability declarations are part of the plugin contract. Back them with tests:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
verifyChannelMessageAdapterCapabilityProofs,
|
||||
verifyChannelMessageLiveCapabilityAdapterProofs,
|
||||
verifyChannelMessageLiveFinalizerProofs,
|
||||
verifyChannelMessageReceiveAckPolicyAdapterProofs,
|
||||
} from "openclaw/plugin-sdk/channel-message";
|
||||
|
||||
it("backs declared message capabilities", async () => {
|
||||
await expect(
|
||||
verifyChannelMessageAdapterCapabilityProofs({
|
||||
adapterName: "demo",
|
||||
adapter: demoMessageAdapter,
|
||||
proofs: {
|
||||
text: async () => {
|
||||
const result = await demoMessageAdapter.send!.text!(textCtx);
|
||||
expect(result.receipt.platformMessageIds).toContain("msg-1");
|
||||
},
|
||||
replyTo: async () => {
|
||||
await demoMessageAdapter.send!.text!({ ...textCtx, replyToId: "parent-1" });
|
||||
expect(sendDemoMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replyToId: "parent-1",
|
||||
}),
|
||||
);
|
||||
},
|
||||
messageSendingHooks: () => {
|
||||
expect(demoMessageAdapter.durableFinal!.capabilities!.messageSendingHooks).toBe(true);
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toContainEqual({ capability: "text", status: "verified" });
|
||||
});
|
||||
```
|
||||
|
||||
Add live and receive proof suites when the adapter declares those features. A
|
||||
missing proof should fail the test rather than silently widening the durable
|
||||
surface.
|
||||
|
||||
## Deprecated Compatibility APIs
|
||||
|
||||
These APIs remain importable for third-party compatibility. Do not use them for
|
||||
new channel code.
|
||||
|
||||
| Deprecated API | Replacement |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `openclaw/plugin-sdk/channel-reply-pipeline` | `openclaw/plugin-sdk/channel-message` |
|
||||
| `createChannelTurnReplyPipeline(...)` | `createChannelMessageReplyPipeline(...)` for compatibility dispatchers, or a `message` adapter for new channel code |
|
||||
| `deliverDurableInboundReplyPayload(...)` | `deliverInboundReplyWithMessageSendContext(...)` from `openclaw/plugin-sdk/channel-message-runtime` |
|
||||
| `dispatchInboundReplyWithBase(...)` | `dispatchChannelMessageReplyWithBase(...)` only for compatibility dispatchers |
|
||||
| `recordInboundSessionAndDispatchReply(...)` | `recordChannelMessageReplyDispatch(...)` only for compatibility dispatchers |
|
||||
| `resolveChannelSourceReplyDeliveryMode(...)` | `resolveChannelMessageSourceReplyDeliveryMode(...)` |
|
||||
| `deliverFinalizableDraftPreview(...)` | `defineFinalizableLivePreviewAdapter(...)` plus `deliverWithFinalizableLivePreviewAdapter(...)` |
|
||||
| `DraftPreviewFinalizerDraft` | `LivePreviewFinalizerDraft` |
|
||||
| `DraftPreviewFinalizerResult` | `LivePreviewFinalizerResult` |
|
||||
|
||||
Compatibility dispatchers can still use `createReplyPrefixContext(...)`,
|
||||
`createReplyPrefixOptions(...)`, and `createTypingCallbacks(...)` through the
|
||||
message facade. New lifecycle code should avoid the old
|
||||
`channel-reply-pipeline` subpath.
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
1. Add `message: defineChannelMessageAdapter(...)` or
|
||||
`message: createChannelMessageAdapterFromOutbound(...)` to the channel plugin.
|
||||
2. Return `MessageReceipt` from text, media, and payload sends.
|
||||
3. Declare only capabilities backed by native behavior and tests.
|
||||
4. Replace hand-written durable requirement maps with
|
||||
`deriveDurableFinalDeliveryRequirements(...)`.
|
||||
5. Move preview finalization through the live preview helpers when the channel
|
||||
edits draft messages in place.
|
||||
6. Declare receive ack policy only when the receiver can really defer platform
|
||||
acknowledgement.
|
||||
7. Keep legacy reply dispatch helpers only at compatibility edges.
|
||||
@@ -34,6 +34,46 @@ shared `message` tool in core. Your plugin owns:
|
||||
Core owns the shared message tool, prompt wiring, the outer session-key shape,
|
||||
generic `:thread:` bookkeeping, and dispatch.
|
||||
|
||||
New channel plugins should also expose a `message` adapter with
|
||||
`defineChannelMessageAdapter` from `openclaw/plugin-sdk/channel-message`. The
|
||||
adapter declares which durable final-send capabilities the native transport
|
||||
actually supports and points text/media sends at the same transport functions as
|
||||
the legacy `outbound` adapter. Only declare a capability when a contract test
|
||||
proves the native side effect and returned receipt.
|
||||
For the full API contract, examples, capability matrix, receipt rules, live
|
||||
preview finalization, receive ack policy, tests, and migration table, see
|
||||
[Channel message API](/plugins/sdk-channel-message).
|
||||
If the existing `outbound` adapter already has the right send methods and
|
||||
capability metadata, use `createChannelMessageAdapterFromOutbound(...)` to
|
||||
derive the `message` adapter instead of hand-writing another bridge.
|
||||
Adapter sends should return `MessageReceipt` values. When compatibility code
|
||||
still needs legacy ids, derive them with `listMessageReceiptPlatformIds(...)`
|
||||
or `resolveMessageReceiptPrimaryId(...)` instead of keeping parallel
|
||||
`messageIds` fields in new lifecycle code.
|
||||
Preview-capable channels should also declare `message.live.capabilities` with
|
||||
the exact live lifecycle they own, such as `draftPreview`,
|
||||
`previewFinalization`, `progressUpdates`, `nativeStreaming`, or
|
||||
`quietFinalization`. Channels that finalize a draft preview in place should
|
||||
also declare `message.live.finalizer.capabilities`, such as `finalEdit`,
|
||||
`normalFallback`, `discardPending`, `previewReceipt`, and
|
||||
`retainOnAmbiguousFailure`, and route the runtime logic through
|
||||
`defineFinalizableLivePreviewAdapter(...)` plus
|
||||
`deliverWithFinalizableLivePreviewAdapter(...)`. Keep those capabilities backed
|
||||
by `verifyChannelMessageLiveCapabilityAdapterProofs(...)` and
|
||||
`verifyChannelMessageLiveFinalizerProofs(...)` tests so native preview,
|
||||
progress, edit, fallback/retention, cleanup, and receipt behavior cannot drift
|
||||
silently.
|
||||
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(...)`.
|
||||
|
||||
Legacy reply/turn helpers such as `createChannelTurnReplyPipeline`,
|
||||
`dispatchInboundReplyWithBase`, and `recordInboundSessionAndDispatchReply`
|
||||
remain available for compatibility dispatchers. Do not use those names for new
|
||||
channel code; new plugins should start with the `message` adapter, receipts, and
|
||||
receive/send lifecycle helpers on `openclaw/plugin-sdk/channel-message`.
|
||||
|
||||
If your channel supports typing indicators outside inbound replies, expose
|
||||
`heartbeat.sendTyping(...)` on the channel plugin. Core calls it with the
|
||||
resolved heartbeat delivery target before the heartbeat model run starts and
|
||||
@@ -50,6 +90,13 @@ Prefer returning an action-keyed map such as
|
||||
inherit another action's media args. A flat array still works for params that
|
||||
are intentionally shared across every exposed action.
|
||||
|
||||
If your channel needs provider-specific shaping for `message(action="send")`,
|
||||
prefer `actions.prepareSendPayload(...)`. Put native cards, blocks, embeds, or
|
||||
other durable data under `payload.channelData.<channel>` and let core perform
|
||||
the actual send through the outbound/message adapter. Use
|
||||
`actions.handleAction(...)` for send only as a compatibility fallback for
|
||||
payloads that cannot be serialized and retried.
|
||||
|
||||
If your platform stores extra scope inside conversation ids, keep that parsing
|
||||
in the plugin with `messaging.resolveSessionConversation(...)`. That is the
|
||||
canonical hook for mapping `rawId` to the base conversation id, optional thread
|
||||
|
||||
@@ -312,17 +312,23 @@ The kernel does not call the platform directly. The channel hands the kernel a `
|
||||
type ChannelTurnDeliveryAdapter = {
|
||||
deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
|
||||
onError?(err: unknown, info: { kind: string }): void;
|
||||
durable?: false | DurableInboundReplyDeliveryOptions;
|
||||
};
|
||||
|
||||
type ChannelDeliveryResult = {
|
||||
messageIds?: string[];
|
||||
receipt?: MessageReceipt;
|
||||
threadId?: string;
|
||||
replyToId?: string;
|
||||
visibleReplySent?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
`deliver` is called once per buffered reply chunk. Return platform message ids when the channel has them so the dispatcher can preserve thread anchors and edit later chunks. For observe-only turns, return `{ visibleReplySent: false }` or use `createNoopChannelTurnDeliveryAdapter()`.
|
||||
`deliver` is called once per buffered reply chunk. During the message-lifecycle migration, assembled channel-turn delivery is channel-owned by default: an omitted `durable` field means the kernel must call `deliver` directly and must not route through generic outbound delivery. Set `durable` only after the channel has been audited to prove the generic send path preserves the old delivery behavior, including reply/thread targets, media handling, sent-message/self-echo caches, status cleanup, and returned message ids. `durable: false` remains a compatibility spelling for "use the channel-owned callback", but unmigrated channels should not need to add it. Return platform message ids when the channel has them so the dispatcher can preserve thread anchors and edit later chunks; newer delivery paths should also return `receipt` so recovery, preview finalization, and duplicate suppression can move off `messageIds`. For observe-only turns, return `{ visibleReplySent: false }` or use `createNoopChannelTurnDeliveryAdapter()`.
|
||||
|
||||
Channels using `runPrepared` with a fully channel-owned dispatcher do not have a `ChannelTurnDeliveryAdapter`. Those dispatchers are not durable by default. They should keep their direct delivery path until they explicitly opt in to the new send context with a complete target, replay-safe adapter, receipt contract, and channel side-effect hooks.
|
||||
|
||||
Public compatibility helpers such as `recordInboundSessionAndDispatchReply`, `dispatchInboundReplyWithBase`, and direct-DM helpers must stay behavior-preserving during migration. They should not call generic durable delivery before caller-owned `deliver` or `reply` callbacks.
|
||||
|
||||
## Record options
|
||||
|
||||
@@ -388,6 +394,7 @@ Backward compatibility rules apply: new fact fields are additive, admission kind
|
||||
|
||||
## Related
|
||||
|
||||
- [Message lifecycle refactor](/concepts/message-lifecycle-refactor) for the planned send/receive/live lifecycle that will wrap this kernel
|
||||
- [Building channel plugins](/plugins/sdk-channel-plugins) for the broader channel plugin contract
|
||||
- [Plugin runtime helpers](/plugins/sdk-runtime) for other `runtime.*` surfaces
|
||||
- [Plugin internals](/plugins/architecture-internals) for load pipeline and registry mechanics
|
||||
|
||||
@@ -56,7 +56,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/account-resolution` | Account lookup + default-fallback helpers |
|
||||
| `plugin-sdk/account-helpers` | Narrow account-list/account-action helpers |
|
||||
| `plugin-sdk/channel-pairing` | `createChannelPairingController` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | `createChannelReplyPipeline`, `resolveChannelSourceReplyDeliveryMode` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | Legacy reply pipeline helpers. New channel reply pipeline code should use `createChannelMessageReplyPipeline` and `resolveChannelMessageSourceReplyDeliveryMode` from `plugin-sdk/channel-message`. |
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives plus Zod and direct JSON/TypeBox builders |
|
||||
| `plugin-sdk/bundled-channel-config-schema` | Bundled OpenClaw channel config schemas for maintained bundled plugins only |
|
||||
@@ -64,9 +64,11 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
|
||||
| `plugin-sdk/command-gating` | Narrow command authorization gate helpers |
|
||||
| `plugin-sdk/channel-policy` | `resolveChannelGroupRequireMention` |
|
||||
| `plugin-sdk/channel-lifecycle` | `createAccountStatusSink`, `createChannelRunQueue`, draft stream lifecycle/finalization helpers |
|
||||
| `plugin-sdk/channel-lifecycle` | `createAccountStatusSink`, `createChannelRunQueue`, and legacy draft stream lifecycle helpers. New preview finalization code should use `plugin-sdk/channel-message`. |
|
||||
| `plugin-sdk/channel-message` | Cheap message lifecycle contract helpers such as `defineChannelMessageAdapter`, `createChannelMessageAdapterFromOutbound`, `createReplyPrefixContext`, `resolveChannelMessageSourceReplyDeliveryMode`, compatibility facades, durable-final capability derivation, capability proof helpers for send/receipt/side-effect capabilities, `MessageReceiveContext`, receive ack policy proofs, `defineFinalizableLivePreviewAdapter`, `deliverWithFinalizableLivePreviewAdapter`, live-preview and live-finalizer capability proofs, durable recovery state, `RenderedMessageBatch`, message receipt types, and receipt id helpers. See [Channel message API](/plugins/sdk-channel-message). Legacy `createChannelTurnReplyPipeline` remains only for compatibility dispatchers. |
|
||||
| `plugin-sdk/channel-message-runtime` | Runtime delivery helpers that may load outbound delivery, including `deliverInboundReplyWithMessageSendContext`, `sendDurableMessageBatch`, `withDurableMessageSendContext`, `dispatchChannelMessageReplyWithBase`, and `recordChannelMessageReplyDispatch`. Use from monitor/send runtime modules, not hot plugin bootstrap files. |
|
||||
| `plugin-sdk/inbound-envelope` | Shared inbound route + envelope builder helpers |
|
||||
| `plugin-sdk/inbound-reply-dispatch` | Shared inbound record-and-dispatch helpers |
|
||||
| `plugin-sdk/inbound-reply-dispatch` | Legacy shared inbound record-and-dispatch helpers, visible/final dispatch predicates, and deprecated `deliverDurableInboundReplyPayload` compatibility for prepared channel dispatchers. New channel receive/dispatch code should import runtime lifecycle helpers from `plugin-sdk/channel-message-runtime`. |
|
||||
| `plugin-sdk/messaging-targets` | Target parsing/matching helpers |
|
||||
| `plugin-sdk/outbound-media` | Shared outbound media loading helpers |
|
||||
| `plugin-sdk/outbound-send-deps` | Lightweight outbound send dependency lookup for channel adapters |
|
||||
|
||||
Reference in New Issue
Block a user