We serialize inbound auto-reply runs (all channels) through a tiny in-process queue to prevent multiple agent runs from colliding, while still allowing safe parallelism across sessions.Documentation Index
Fetch the complete documentation index at: https://openclaw.ai2me.io/llms.txt
Use this file to discover all available pages before exploring further.
Why
- Auto-reply runs can be expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
How it works
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8).
runEmbeddedAgentenqueues by session key (lanesession:<key>) to guarantee only one active run per session.- Each session run is then queued into a global lane (
mainby default) so overall parallelism is capped byagents.defaults.maxConcurrent. - When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting.
- Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn.
Defaults
When unset, all inbound channel surfaces use:mode: "steer"debounceMs: 500cap: 20drop: "summarize"
Queue modes
/queue controls what normal inbound messages do while a session already has
an active run:
steer: inject messages into the active runtime. OpenClaw delivers all pending steering messages after the current assistant turn finishes executing its tool calls, before the next LLM call; Codex app-server receives one batchedturn/steer. If the run is not actively streaming or steering is unavailable, OpenClaw waits until the active run ends before starting the prompt.followup: do not steer. Enqueue each message for a later agent turn after the current run ends.collect: do not steer. Coalesce queued messages into a single followup turn after the quiet window. If messages target different channels/threads, they drain individually to preserve routing.interrupt: abort the active run for that session, then run the newest message.
/steer <message>
command, see Steer.
Configure globally or per channel via messages.queue:
Queue options
Options apply to queued delivery.debounceMs also sets the Codex steering
quiet window in steer mode:
debounceMs: quiet window before draining queued followups or collect batches; in Codexsteermode, quiet window before sending batchedturn/steer. Bare numbers are milliseconds; unitsms,s,m,h, anddare accepted by/queueoptions.cap: max queued messages per session. Values below1are ignored.drop: "summarize": default. Drop the oldest queued entries as needed, keep compact summaries, and inject them as a synthetic followup prompt.drop: "old": drop the oldest queued entries as needed, without preserving summaries.drop: "new": reject the newest message when the queue is already full.
debounceMs: 500, cap: 20, drop: summarize.
Steer and streaming
When channel streaming ispartial or block, steering can look like several
short visible replies while the active run reaches runtime boundaries:
partial: the preview may finalize early, then a new preview starts after steering is accepted.block: draft-sized blocks can create the same sequential appearance.- Without streaming, steering falls back to a followup after the active run when the runtime cannot accept same-turn steering.
steer does not abort in-flight tools. Use /queue interrupt when the newest
message should abort the current run.
Precedence
For mode selection, OpenClaw resolves:- Inline or stored per-session
/queueoverride. messages.queue.byChannel.<channel>.messages.queue.mode.- Default
steer.
/queue options win over config. Then
channel-specific debounce (messages.queue.debounceMsByChannel), plugin
debounce defaults, global messages.queue options, and built-in defaults are
applied. cap and drop are global/session options, not per-channel config
keys.
Per-session overrides
- Send
/queue <steer|followup|collect|interrupt>as a standalone command to store the queue mode for the current session. - Options can be combined:
/queue collect debounce:0.5s cap:25 drop:summarize /queue defaultor/queue resetclears the session override.
Scope and guarantees
- Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.).
- Default lane (
main) is process-wide for inbound + main heartbeats; setagents.defaults.maxConcurrentto allow multiple sessions in parallel. - Additional lanes may exist (e.g.
cron,cron-nested,nested,subagent) so background jobs can run in parallel without blocking inbound replies. Isolated cron agent turns hold acronslot while their inner agent execution usescron-nested; both usecron.maxConcurrentRuns. Shared non-cronnestedflows keep their own lane behavior. These detached runs are tracked as background tasks. - Per-session lanes guarantee that only one agent run touches a given session at a time.
- No external dependencies or background worker threads; pure TypeScript + promises.
Troubleshooting
- If commands seem stuck, enable verbose logs and look for “queued for …ms” lines to confirm the queue is draining.
- If you need queue depth, enable verbose logs and watch for queue timing lines.
- Codex app-server runs that accept a turn and then stop emitting progress are interrupted by the Codex adapter so the active session lane can release instead of waiting for the outer run timeout.
- When diagnostics are enabled, sessions that remain in
processingpastdiagnostics.stuckSessionWarnMswith no observed reply, tool, status, block, or ACP progress are classified by current activity. Active work logs assession.long_running; active work with no recent progress logs assession.stalled;session.stuckis reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity, and only that path can release the affected session lane so queued work drains. Repeatedsession.stuckdiagnostics back off while the session remains unchanged.