On this page
On this page
Messages and delivery
Messages
Inbound messages move through routing, dedupe/debounce, an agent run, and outbound delivery:
Key config surfaces:
messages.*for prefixes, queueing, inbound debounce, and group behavior.agents.defaults.*for block streaming, chunking, and silent-reply defaults.- Channel overrides (
channels.telegram.*,channels.whatsapp.*, etc.) for per-channel caps and streaming toggles.
See Configuration for the full schema.
Inbound dedupe
Channels can redeliver the same message after a reconnect. OpenClaw keeps an in-memory cache keyed by agent scope, channel route (channel + peer + account + thread), and message id, so a redelivered message does not trigger a second agent run. The cache entry expires after 20 minutes or once 5000 entries are tracked, whichever comes first.
Inbound debouncing
Rapid text messages from the same sender can be batched into one agent turn via messages.inbound. Debouncing is scoped per channel + conversation and uses the most recent message for reply threading/IDs. It is a quiet-window heuristic, not a guarantee that every part of a long message will arrive in one turn.
- Debounce applies to text-only messages; media/attachments flush immediately.
- Control commands (stop/abort/status, etc.) bypass debouncing so they dispatch immediately.
- Telegram batches ordinary text by default after a 300ms quiet window. Other channels have no generic debounce delay unless configured.
messages.inbound.byChannel.<channel>takes precedence overmessages.inbound.debounceMs; either overrides the channel default. Set0to disable ordinary burst batching.- For non-forwarded Telegram text, messages of at least 4000 characters allow up to 1500ms for continuations. Short and long messages share the same batch, without requiring consecutive message IDs. This automatic long-paste assembly remains active when ordinary batching is disabled.
- iMessage follows the same generic debounce policy.
imsg0.13.1 and newer coalesces Apple URL-preview split-sends before OpenClaw receives them, so no iMessage-specific debounce setting is needed.
Changes to messages.inbound.debounceMs and messages.inbound.byChannel apply without
reconnecting Discord, Feishu, iMessage, Mattermost, Microsoft Teams, Signal, Slack,
Telegram, or WhatsApp. Newly admitted inbound work uses the committed delay. A config change alone does not reschedule a pending batch;
later messages can update its idle delay within the original maximum deadline.
Explicit transport timing overrides remain fixed. Telegram's forwarded-message
collection window remains separate.
Sessions and devices
Sessions are owned by the gateway, not by clients.
- Direct chats collapse into the agent's main session key.
- Groups/channels get their own session keys.
- The session store and transcripts live on the gateway host.
Multiple devices/channels can map to the same session, but history is not fully synced back to every client. Use one primary device for long conversations to avoid divergent context. The Control UI and TUI always show the gateway-backed session transcript, so they are the source of truth.
If a run fails or times out before an assistant reply is saved, its transcript receives one visible failure notice. Nested runs retain their own failure notices even after the requesting turn has ended.
Details: Session management.
Prompt bodies and history context
Channel plugins populate several text fields on the inbound context, from most to least preferred:
| Field | Purpose |
|---|---|
BodyForAgent |
Model-facing text for the current turn. Falls back to CommandBody / RawBody / Body when unset. |
BodyForCommands |
Clean text used for directive/command parsing. Falls back to CommandBody / RawBody / Body when unset. |
CommandBody |
Legacy intermediate body; prefer BodyForCommands. |
RawBody |
Deprecated alias for CommandBody. |
Body |
Legacy prompt body; may include channel envelopes and history wrappers. |
When a channel supplies history, it wraps it with:
[Chat messages since your last reply - for context][Current message - respond to this]
For non-direct chats (groups/channels/rooms), the current message body is prefixed with the sender label, matching the style used for history entries. Directive stripping only applies to the current-message section, so history stays intact. Channels that wrap history should set BodyForCommands (or the legacy CommandBody / RawBody) to the original message text and keep Body as the combined prompt.
History buffers are pending-only: they include group messages that did not trigger a run (for example, mention-gated messages) and exclude messages already in the session transcript. Structured history, reply, forwarded, and channel metadata render as untrusted user-role context blocks during prompt assembly.
Configure history size with messages.groupChat.historyLimit (global default) or per-channel overrides such as channels.slack.historyLimit and channels.telegram.accounts.<id>.historyLimit (set 0 to disable).
Tool result metadata
Tool result content is the model-visible result; details is runtime metadata for UI rendering, diagnostics, media delivery, and plugins.
toolResult.detailsis stripped before provider replay and before compaction input.- Persisted session transcripts keep only bounded
details; oversized metadata is replaced with a compact summary markedpersistedDetailsTruncated: true. - Display history retains tool status flags and session keys, plus command exit codes, durations, and bounded working directories when the tool provides them.
- Plugins and tools should put text the model must read in
content, not only indetails.
When a tool-error warning is the agent's only reply, WebChat displays and retains it. The warning does not by itself change a completed agent run into a runtime failure; the failed tool result remains recorded separately.
Queueing and followups
When a run is already active, inbound messages steer into it by default. messages.queue controls the mode:
| Mode | Behavior |
|---|---|
steer (default) |
Inject the new prompt into the active run. |
followup |
Run the message after the active run finishes. |
collect |
Batch compatible messages into one later turn. |
interrupt |
Abort the active run, then start the newest prompt. |
Messages with separate durable ingress admission, including Discord and Telegram messages, stay in separate followup turns even in collect mode. Compatible Gateway chat.send inputs can still combine.
The queue uses a built-in 500ms debounce for steer, followup, and collect batching. messages.queue.cap defaults to 20 queued messages, and messages.queue.drop defaults to summarize (old and new are also available). Configure per-channel overrides via messages.queue.byChannel and messages.queue.debounceMsByChannel.
Details: Command queue and Steering queue.
Channel run ownership
Channel plugins may preserve ordering, debounce input, and apply transport backpressure before a message enters the session queue. They should not impose a separate timeout around the agent turn itself. Once a message is routed to a session, the session, tool, and runtime lifecycle govern long-running work so all channels report and recover from slow turns consistently.
Once a turn is durably accepted, an unexpected failure before its answer produces a compact error reply in direct chats and explicitly addressed conversations where automatic replies are enabled. Progress acknowledgments do not replace that final outcome. The turn remains failed and is not replayed as a new inbound message; delivery policies and replies already sent through the message tool still apply.
With the OpenClaw runtime, an assistant turn that errors or is aborted after producing partial text, without tool calls, appears as a short failure marker in the next model request. Its unfinished text is not replayed, and the stored failed turn stays unchanged. Empty and placeholder-only failures remain excluded; failed tool calls keep their existing pairing rules. The marker does not establish whether an earlier action completed.
Streaming, chunking, and batching
Block streaming sends partial replies as the model produces text blocks; chunking respects channel text limits and avoids splitting fenced code.
agents.defaults.blockStreamingDefault(on|off, defaultoff)agents.defaults.blockStreamingBreak(text_end|message_end)agents.defaults.blockStreamingChunk(minChars|maxChars|breakPreference)agents.defaults.blockStreamingCoalesce(idle-based batching)agents.defaults.humanDelay(human-like pause between block replies)- Channel overrides:
*.streaming.block.enabledand*.streaming.block.coalesceon bundled channels; stale flat keys are migrated byopenclaw doctor --fix. Block streaming is off unless explicitly enabled, on every channel including Telegram. QQ Bot is the exception: it has nostreaming.blockkeys and streams block replies unlesschannels.qqbot.streaming.modeis"off".
Details: Streaming + chunking.
Reasoning visibility and tokens
/reasoning on|off|streamcontrols visibility.- Reasoning content still counts toward token usage when the model produces it.
- Telegram supports streaming reasoning into a transient draft bubble that is deleted after final delivery; use
/reasoning onfor persistent reasoning output.
Details: Thinking + reasoning directives and Token use.
Prefixes, threading, and replies
- Channels with reply-prefix support use
channels.<channel>.responsePrefixand, when multi-account configuration is supported,channels.<channel>.accounts.<id>.responsePrefix. Account values win, including""to disable an inherited prefix. Use"auto"for the agent identity name or templates such as"[{model}]"for the selected model. Automatic-reply support is channel-specific. Doctor copies the global fallback into supported configured channel blocks when those canonical fields are unset;messages.responsePrefixremains as a fallback for implicit and custom channels. - Explicit
messagetool and CLI text sends also apply the resolved prefix, without duplicating a prefix already present. They resolve identity placeholders but do not select a model; a prefix containing unresolved model, provider, or thinking-level placeholders is omitted entirely. - Reply threading via
replyToModeand per-channel defaults.
Details: Configuration and channel docs.
Agent reactions to WebChat prompts
During an admitted WebChat turn, the agent can acknowledge the current prompt with the existing message tool:
Omit the target and message ID: the host binds this action to the committed current
prompt and the running agent's identity. Set remove: true with the same emoji to
remove that agent's reaction. Repeating an add or removal is a no-op. The reaction
appears in session history and live updates; it does not mirror to an inherited
external channel. External reactions still require the normal channel routing and
external message ID.
Queued turns bind the reaction to their own committed prompt when execution begins. Steering updates the current prompt only after the new input is committed and consumed by the running agent; pending or rejected input does not retarget a reaction. Collected inputs use their committed combined prompt.
Acknowledgment reactions do not replace the final answer. Omit final or set
final: false when more work follows. Only an explicit final: true addition,
requested as the complete response, can count as source-reply completion. Removals,
no-ops, dry runs, and failed reactions never count. Normal message-tool permissions
still apply, and the current-prompt capability expires with its admitted turn.
Silent replies
The silent token NO_REPLY (case-insensitive, so no_reply also matches) is reserved for sessions connected to external message channels and is never delivered as user-visible text. Subagents, the Control UI, and other internal sessions must return a result or continue unfinished work; a silent token cannot complete their task. When a turn also has pending tool media, such as generated TTS audio, OpenClaw strips the silent text but still delivers the media attachment.
Silence policy resolves by conversation type:
- Direct conversations never receive
NO_REPLYprompt guidance. An undelivered required answer still needs recovery; the token cannot waive that obligation. - Accepted group/channel requests require a reply by default, including unmentioned messages admitted with
requireMention: false. Mention and access gates still decide which messages reach the agent. To allow unaddressed requests to finish silently, explicitly setsilentReply.group: "allow"at one of the configuration scopes below; mentions and authorized commands still require a response. - Ambient room events can remain silent. In
message_toolvisible-reply mode, an optional turn stays silent by not callingmessage(action=send). Private subagent completions record the parent's reviewed outcome internally; they do not need a silent token to keep that result private.
Defaults live under agents.defaults.silentReply.group; surfaces.<id>.silentReply.group can override group policy per surface. Doctor removes the retired internal setting during config migration.
Generic internal runner failures stay quiet for optional turns that have not shown visible output, including groups explicitly configured to allow silence. Required turns still receive an error. Classified recovery guidance, such as missing-auth, rate-limit, or overload notices, remains deliverable, and visible progress receives a failure outcome rather than being left unfinished. Direct chats show compact failure copy by default; raw runner details show only when /verbose full is enabled.
Reply requirements come from admission, not model control tokens. Confirmed delivery or still-owned delivery prevents duplicate recovery; see Reply shaping.
Related
- Channel inbound API - receive orchestration and acknowledgment policy
- Channel outbound API - durable sends and delivery receipts
- Streaming - real-time message delivery
- Retry - message delivery retry behavior
- Queue - message processing queue
- Channels - messaging platform integrations