Command Queue: Modes, Defaults, and Per-Session Overrides
Learn how OpenClaw queues auto-reply runs to prevent interference and enable safe parallelism. Understand lane-aware FIFO processing, concurrency defaults, and per-session overrides for developers and operators.
Read this when
- Changing auto-reply execution or concurrency
- Explaining /queue modes or message steering behavior
- Inspecting background work and command-lane diagnostics
OpenClaw funnels incoming auto-reply runs from every channel through a small in-process queue. This prevents multiple agent runs from interfering with one another while still permitting safe parallelism across different sessions.
Why
- Auto-reply runs can be costly (LLM calls) and may clash when several inbound messages arrive in quick succession.
- Serializing reduces contention for shared resources (session state, logs, CLI stdin) and lowers the likelihood of hitting upstream rate limits.
How it works
- A lane-aware FIFO queue processes each lane with a configurable concurrency cap (default 1 for lanes without explicit settings;
mainusesmin(16, max(8, available CPU parallelism)), andsubagentdefaults to 8). runEmbeddedAgentqueues by session key (lanesession:<key>) to ensure only one active run exists per session.- Each session run is then placed into a global lane (
mainby default), capping overall parallelism atagents.defaults.maxConcurrent. - With verbose logging enabled, queued runs log a brief note if they waited more than roughly 2s before execution.
- Typing indicators fire immediately upon enqueue (where the channel supports them), so the user experience stays unchanged while the run waits its turn.
Defaults
When left unset, all inbound channel surfaces use:
mode: "steer"- a built-in 500ms debounce for steer, followup, and collect batching
cap: 20drop: "summarize"
Same-turn steering is the default behavior. A prompt arriving mid-run is injected into the active runtime when steering is possible, so no second session run is initiated. If the active run cannot accept steering, OpenClaw waits for it to complete before starting the prompt.
Queue modes
/queue determines what normal inbound messages do while a session already has an active run:
steer: inject messages into the active runtime. OpenClaw lets an already-running tool finish, skips sequential calls that have not started, and makes the steer visible before the next tool launch or model decision. Parallel calls continue once their batch has crossed its launch checkpoint. Codex app-server receives one batchedturn/steerand applies it at the next model boundary. 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.
For runtime-specific timing and dependency behavior, see Steering queue. For the explicit /steer <message> command, see Steer.
Configure globally or per channel via messages.queue:
{
messages: {
queue: {
mode: "steer",
cap: 20,
drop: "summarize",
byChannel: { discord: "collect" },
debounceMsByChannel: { discord: 1000 },
},
},
}
Queue options
Per-session /queue options apply to queued delivery. The debounce option also sets the Codex steering quiet window in steer mode:
debounce: 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.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.
The queue uses a built-in 500ms debounce. cap defaults to 20, and drop defaults to summarize.
Steer and streaming
When channel streaming is partial 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. Skipped OpenClaw tool calls receive synthetic paired error results so the transcript remains valid. 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.
For options, inline or stored /queue options take precedence over config. Following that, channel-specific debounce (messages.queue.debounceMsByChannel), plugin debounce defaults, and built-in defaults are applied, in that sequence. cap and drop function as global/session options, not per-channel configuration keys.
Per-session overrides
- Issue
/queue <steer|followup|collect|interrupt>as a standalone command to persist the queue mode for the current session. - Options are combinable:
/queue collect debounce:0.5s cap:25 drop:summarize /queue defaultor/queue resetremoves the session override.
Queued-turn cancellation
When a prompt is held in the followup/collect queue (for instance a TUI or webchat chat.send arriving while another turn is in progress), Gateway maintains a Gateway-owned cancel identity for that client runId until the queued content executes or is discarded. The identity carries over to content merged into an overflow summary.
chat.abortpaired with a specificrunIdcancels that turn while it remains queued, provided the requester is authorized (same ownership rules as active runs).chat.abortfor a session lackingrunIdcancels authorized queued turns first, then aborts authorized active runs. This ordering stops queue drain from moving work into a partially stopped session.- Clearing the whole session queue without per-requester checks is not the stop mechanism for multi-owner sessions.
- Queued waits are not represented as active agent runs for
sessions.listand do not carry active-run timeout semantics; only the active phase does.
Gateway-backed clients (including openclaw tui) pass mid-run prompts forward and let Gateway enforce the queue mode. Esc//stop uses a session-scoped abort so lost local handles cannot leave a still-queued prompt executing.
openclaw chat and openclaw tui --local apply the same four modes in the embedded runtime. Local steer injects into an active embedded run when that runtime accepts steering and otherwise becomes a followup; followup and collect stay local pending work; interrupt aborts the active local run before starting the newest message. The explicit /steer <message> command is not a local-mode command.
Scope and guarantees
- Applies to auto-reply agent runs across all inbound channels using the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.).
- Default lane (
main) is process-wide for inbound turns; setagents.defaults.maxConcurrentto permit multiple sessions in parallel. - Heartbeat embedded runs use the bounded
cron-nestedlane for global admission so slow background work does not block inbound replies, while their configured heartbeat session lane still serializes work for that session. - 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. 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 appear stuck, enable verbose logs and search for "queued for ...ms" lines to verify the queue is draining.
- 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
processingpast the built-in warning threshold with no observed reply, tool, status, block, or ACP progress are classified by current activity:- Active work with recent progress logs as
session.long_running. Owned silent model calls also staysession.long_runninguntil the built-in abort threshold so slow or non-streaming providers are not reported as stalled too early. - Active work with no recent progress logs as
session.stalled; owned model calls, blocked tool calls, and stalled embedded runs switch tosession.stalledat or after the abort threshold. Ownerless stale model/tool activity is not hidden as long-running. session.stuckis reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity.session.stuckalways triggers recovery that can release the affected session lane. Asession.stalledclassification past the abort threshold (blocked tool call, stalled model call, or stalled embedded run) can also trigger active-abort recovery, so both classifications can unstick a queue, not onlysession.stuck.- Repeated
session.stuckandsession.long_runningwarning log lines back off exponentially while the session remains unchanged; recovery attempts still run on every heartbeat tick regardless of that backoff.
- Active work with recent progress logs as