Files
openclaw/docs/concepts/messages.md
2026-05-15 21:29:39 +01:00

215 lines
9.0 KiB
Markdown

---
summary: "Message flow, sessions, queueing, and reasoning visibility"
read_when:
- Explaining how inbound messages become replies
- Clarifying sessions, queueing modes, or streaming behavior
- Documenting reasoning visibility and usage implications
title: "Messages"
---
OpenClaw handles inbound messages through a pipeline of session resolution, queueing, streaming, tool execution, and reasoning visibility. This page maps the path from inbound message to reply.
## Message flow (high level)
```
Inbound message
-> routing/bindings -> session key
-> queue (if a run is active)
-> agent run (streaming + tools)
-> outbound replies (channel limits + chunking)
```
Key knobs live in configuration:
- `messages.*` for prefixes, queueing, and group behavior.
- `agents.defaults.*` for block streaming and chunking defaults.
- Channel overrides (`channels.whatsapp.*`, `channels.telegram.*`, etc.) for caps and streaming toggles.
See [Configuration](/gateway/configuration) for full schema.
## Inbound dedupe
Channels can redeliver the same message after reconnects. OpenClaw keeps a
short-lived cache keyed by channel/account/peer/session/message id so duplicate
deliveries do not trigger another agent run.
## Inbound debouncing
Rapid consecutive messages from the **same sender** can be batched into a single
agent turn via `messages.inbound`. Debouncing is scoped per channel + conversation
and uses the most recent message for reply threading/IDs.
Config (global default + per-channel overrides):
```json5
{
messages: {
inbound: {
debounceMs: 2000,
byChannel: {
whatsapp: 5000,
slack: 1500,
discord: 1500,
},
},
},
}
```
Notes:
- Debounce applies to **text-only** messages; media/attachments flush immediately.
- Control commands bypass debouncing so they remain standalone. Channels that explicitly opt in to same-sender DM coalescing can keep DM commands inside the debounce window so a split-send payload can join the same agent turn.
## Sessions and devices
Sessions are owned by the gateway, not by clients.
- Direct chats collapse into the agent 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. Recommendation: 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](/concepts/session).
## Tool result metadata
Tool result `content` is the model-visible result. Tool result `details` is
runtime metadata for UI rendering, diagnostics, media delivery, and plugins.
OpenClaw keeps that boundary explicit:
- `toolResult.details` is stripped before provider replay and compaction input.
- Persisted session transcripts keep only bounded `details`; oversized metadata
is replaced with a compact summary marked `persistedDetailsTruncated: true`.
- Plugins and tools should put text the model must read in `content`, not only
in `details`.
## Inbound bodies and history context
OpenClaw separates the **prompt body** from the **command body**:
- `BodyForAgent`: primary model-facing text for the current message. Channel
plugins should keep this focused on the sender's current prompt-bearing text.
- `Body`: legacy prompt fallback. This may include channel envelopes and
optional history wrappers, but current channels should not rely on it as the
primary model input when `BodyForAgent` is available.
- `CommandBody`: raw user text for directive/command parsing.
- `RawBody`: legacy alias for `CommandBody` (kept for compatibility).
When a channel supplies history, it uses a shared wrapper:
- `[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 (same style used for history entries). This keeps real-time and queued/history
messages consistent in the agent 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.
Directive stripping only applies to the **current message** section so history
remains intact. Channels that wrap history should set `CommandBody` (or
`RawBody`) to the original message text and keep `Body` as the combined prompt.
Structured history, reply, forwarded, and channel metadata are rendered as
user-role untrusted context blocks during prompt assembly.
History buffers are configurable via `messages.groupChat.historyLimit` (global
default) and per-channel overrides like `channels.slack.historyLimit` or
`channels.telegram.accounts.<id>.historyLimit` (set `0` to disable).
## Queueing and followups
If a run is already active, inbound messages are steered into the current run by
default. `messages.queue` selects whether active-run messages steer, queue for
later, collect into one later turn, or interrupt the active run.
- Configure via `messages.queue` (and `messages.queue.byChannel`).
- Default mode is `steer`, with a 500ms debounce for Codex steering batches and
followup/collect queues.
- Modes: `steer`, `followup`, `collect`, and `interrupt`.
Details: [Command queue](/concepts/queue) and [Steering queue](/concepts/queue-steering).
## 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, long-running work is governed by the session, tool, and runtime
lifecycle so all channels report and recover from slow turns consistently.
## 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.
Key settings:
- `agents.defaults.blockStreamingDefault` (`on|off`, default off)
- `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: `*.blockStreaming` and `*.blockStreamingCoalesce` (non-Telegram channels require explicit `*.blockStreaming: true`)
Details: [Streaming + chunking](/concepts/streaming).
## Reasoning visibility and tokens
OpenClaw can expose or hide model reasoning:
- `/reasoning on|off|stream` controls visibility.
- Reasoning content still counts toward token usage when produced by the model.
- Telegram supports reasoning stream into a transient draft bubble that is deleted after final delivery; use `/reasoning on` for persistent reasoning output.
Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/reference/token-use).
## Prefixes, threading, and replies
Outbound message formatting is centralized in `messages`:
- `messages.responsePrefix`, `channels.<channel>.responsePrefix`, and `channels.<channel>.accounts.<id>.responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
- Reply threading via `replyToMode` and per-channel defaults
Details: [Configuration](/gateway/config-agents#messages) and channel docs.
## Silent replies
The exact silent token `NO_REPLY` / `no_reply` 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.
OpenClaw resolves that behavior by conversation type:
- Direct conversations never receive `NO_REPLY` prompt 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 only for automatic group replies.
In `message_tool` visible-reply mode, silence means the model does not call
`message(action=send)`.
- Internal orchestration allows silence by default.
OpenClaw also uses silent replies for internal runner failures that happen
before any assistant reply in non-direct chats, so groups/channels do not see
gateway error boilerplate. Direct chats show compact failure copy by default;
raw runner details are shown only when `/verbose` is `on` or `full`.
Defaults live under `agents.defaults.silentReply`; `surfaces.<id>.silentReply`
can override group/internal policy per surface.
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](/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
- [Channels](/channels) — messaging platform integrations