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; main uses min(16, max(8, available CPU parallelism)), and subagent defaults to 8).
  • runEmbeddedAgent queues by session key (lane session:<key>) to ensure only one active run exists per session.
  • Each session run is then placed into a global lane (main by default), capping overall parallelism at agents.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: 20
  • drop: "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 batched turn/steer and 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 Codex steer mode, quiet window before sending batched turn/steer. Bare numbers are milliseconds; units ms, s, m, h, and d are accepted.
  • cap: max queued messages per session. Values below 1 are 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:

  1. Inline or stored per-session /queue override.
  2. messages.queue.byChannel.<channel>.
  3. messages.queue.mode.
  4. 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 default or /queue reset removes 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.abort paired with a specific runId cancels that turn while it remains queued, provided the requester is authorized (same ownership rules as active runs).
  • chat.abort for a session lacking runId cancels 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.list and 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; set agents.defaults.maxConcurrent to permit multiple sessions in parallel.
  • Heartbeat embedded runs use the bounded cron-nested lane 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 a cron slot while their inner agent execution uses cron-nested. Shared non-cron nested flows 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 processing past 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 stay session.long_running until 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 to session.stalled at or after the abort threshold. Ownerless stale model/tool activity is not hidden as long-running.
    • session.stuck is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity.
    • session.stuck always triggers recovery that can release the affected session lane. A session.stalled classification 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 only session.stuck.
    • Repeated session.stuck and session.long_running warning log lines back off exponentially while the session remains unchanged; recovery attempts still run on every heartbeat tick regardless of that backoff.
1,470 words · updated Aug 24, 2026