Message Flow, Sessions, Queueing, and Reasoning Visibility
Learn how inbound messages are routed, deduplicated, and debounced before agent runs, and how outbound delivery works. Essential for developers configuring message handling and channel overrides.
Read this when
- Explaining how inbound messages become replies
- Clarifying sessions, queueing modes, or streaming behavior
- Documenting reasoning visibility and usage implications
Inbound messages travel through routing, deduplication and debouncing, an agent run, and outbound delivery:
Inbound message
-> routing/bindings -> session key
-> dedupe + debounce
-> queue (if a run is already active)
-> agent run (streaming + tools)
-> outbound replies (channel limits + chunking)
Important configuration surfaces include:
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.
Refer to the Configuration page for the complete schema.
Inbound dedupe
After a reconnect, channels may redeliver the same message. OpenClaw uses an in-memory cache keyed by agent scope, channel route (channel + peer + account + thread), and message id, so a redelivered message won't launch a second agent run. The cache entry expires after 20 minutes or once 5000 entries are tracked, whichever happens first.
Inbound debouncing
Rapid consecutive text messages from the same sender can be combined into one agent turn using messages.inbound. Debouncing is scoped per channel and conversation, and it uses the most recent message for reply threading and IDs.
{
messages: {
inbound: {
debounceMs: 2000,
byChannel: {
discord: 1500,
slack: 1500,
whatsapp: 5000,
},
},
},
}
- Debouncing only applies to text-only messages; media and attachments flush immediately.
- Control commands (stop, abort, status, etc.) skip debouncing and dispatch right away.
- Disabled by default:
messages.inbound.debounceMshas no built-in default, so debouncing only activates once you set it globally or per channel. - 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 required.
Sessions and devices
Sessions belong to the gateway, not to clients.
- Direct chats collapse into the agent's main session key.
- Groups and channels get their own session keys.
- The session store and transcripts reside on the gateway host.
Multiple devices or 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.
Details: Session management.
Prompt bodies and history context
Channel plugins populate several text fields on the inbound context, ordered 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. - Plugins and tools should put text the model must read in
content, not only indetails.
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. |
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
Before a message enters the session queue, channel plugins can preserve message order, debounce input, and apply transport backpressure. They must not introduce a separate timeout for the agent's turn. Once a message reaches a session, the session, tool, and runtime lifecycle handle long-running tasks so all channels report and recover from slow turns consistently.
Streaming, chunking, and batching
Block streaming delivers partial replies as the model generates text blocks; chunking respects per-channel text limits and does not split 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
- Outbound prefixes live at
channels.<channel>.responsePrefixandchannels.<channel>.accounts.<id>.responsePrefix. Account values win. Doctor copies the global fallback into configured channel blocks when those canonical fields are unset;messages.responsePrefixremains as a fallback for implicit and custom channels. - Reply threading via
replyToModeand per-channel defaults.
Details: Configuration and channel docs.
Silent replies
The silent token NO_REPLY (case-insensitive, so no_reply also matches) means "do not deliver a user-visible reply." 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. If a direct run accidentally returns a bare silent token, OpenClaw suppresses it instead of rewriting or delivering it. - Groups/channels allow silence by default. In
message_toolvisible-reply mode, silence means the model does not callmessage(action=send). - Internal orchestration allows silence by default.
Defaults live under agents.defaults.silentReply; surfaces.<id>.silentReply can override group/internal policy per surface.
OpenClaw also uses silent replies for generic internal runner failures in non-direct chats, so groups/channels do not see gateway error boilerplate. Classified failures with user-facing recovery copy, such as missing auth, rate-limit, or overload notices, can still be delivered. Direct chats show compact failure copy by default; raw runner details show only when /verbose full is enabled.
Bare silent replies are dropped on all surfaces, so parent sessions stay quiet instead of rewriting sentinel text into fallback chatter.
Related
- Message lifecycle refactor - target durable send and receive design
- Streaming - real-time message delivery
- Retry - message delivery retry behavior
- Queue - message processing queue
- Channels - messaging platform integrations