On this page

On this page

Messages and delivery

Command queue

OpenClaw serializes 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.

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 state, 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 uses max(8, available CPU parallelism * 4), ordinary sub-agent queues default to 8 per spawning session, and Swarm collector queues default to 32 per group).
  • Priority has a bounded head start: foreground work can overtake normal work by up to 15 seconds and background work by up to 30 seconds. After that, newer foreground arrivals cannot keep passing the older work. Lanes and shared capacity groups use the same order, preserving FIFO within each priority. Occupied slots and earlier queued work can still delay admission; running turns are never preempted.
  • CLI, embedded, and Codex runs enter the same session controller mailbox. Its selector grants one turn for the physical session before global capacity or backend preparation, so changing runtimes cannot start a competing turn.
  • Inbound messages can be persisted before their turn enters the mailbox. A running turn reads a consistent transcript prefix, so a later queued message does not invalidate its context read. Rewrites, compaction, deletion, and branch changes still invalidate that read.
  • Queued replies and progress updates retain their submitting request's authority. Completing another turn does not invalidate that queued request.
  • Inbound session runs then enter the global main lane, whose parallelism is capped by agents.defaults.maxConcurrent. Ordinary sub-agent runs instead use their immediate spawning/controller session's budget, set by agents.defaults.subagents.maxConcurrent. Swarm collector children use their group's separate budget, set by tools.swarm.maxConcurrent.
  • Embedded attempt preparation yields to the event loop after 16 stage starts or at least 8 ms of synchronous dispatch work per slice, so concurrent starts leave room for Gateway requests. A running stage is not preempted. Asynchronous stage work can still overlap and does not count toward that time budget; this does not lower the run concurrency limit or change session serialization.
  • When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting. queueAhead records the enqueue-time backlog, not a prediction of admission order after priority and aging.
  • Admission records phase transitions and queue position without formatting diagnostics on the queue path. Stuck-session diagnostics read current lane/group capacity and up to 64 current holder identities only when reporting a live wait. Slow chat-send lines include admission timings (laneMs, writerMs, placementMs) and enqueue-time backlog; completed waits do not claim historical holder identities. A session can have queueDepth=0 while waiting here: session mailbox depth and global lane occupancy are different counts. Writer and placement preparation already hold a lane slot and report their own progress reasons.
  • Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while the run waits its turn.

Defaults

When unset, all inbound channel surfaces use:

  • mode: "steer"
  • allowCollaborativeSteering: true
  • a built-in 500ms debounce for steer, followup, and collect batching
  • cap: 20
  • drop: "summarize"

Same-conversation collaborative steering is enabled by default, including Shared Owner WebUI/channel continuation in the exact same active session. When collaboration is disabled, that continuation requires current administrative authority on the shared WebUI side and current channel-owner authority on the channel side. A compatible prompt can be injected into the active runtime, so no second session run is started. The active execution keeps its original authority; steering does not give each message its own tool policy. If the active run cannot accept steering, OpenClaw waits for the active run to finish before starting the prompt.

Queue modes

/queue controls what normal inbound messages do while a session already has an active run:

  • steer: inject messages into the active runtime, including while it is executing tools. OpenClaw lets the first executable call of an assistant message start before steering can skip its unstarted sequential tail. Running tools finish, and parallel batches never skip calls for steering. After each batch settles, OpenClaw checks steering before stop hooks and makes it visible after the tool results, before the next model decision. Codex app-server receives one batched turn/steer and applies it at the next model boundary. If 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 compatible queued messages into a single followup turn after the quiet window. Messages must share routing, authorization, and execution settings and must not have distinct exclusive admission lifecycles.
  • interrupt: abort the active run for that session, then run the newest message. This cancellation does not resume the old turn through Gateway restart recovery.

Messages from channels using durable ingress admission, including Discord and Telegram, keep separate followup turns: collect behaves like followup there. Each message owns an exclusive admission lifecycle so one source cannot commit before another rejects a combined turn. Compatible Gateway chat.send inputs use non-exclusive lifecycles and can still collect.

For runtime-specific timing and dependency behavior, see Steering queue. For the explicit /steer <message> command, see Steer.

After a graceful restart, a marked running root session receives one ordinary resume input. Accepted user messages whose IDs were captured in that shutdown marker follow it in FIFO order, under their recorded senders' current authority. Queued rows left by an earlier crash remain inert. A sender that cannot be admitted gets a failed-send outcome. New messages use the normal steering and queue rules while the resumed run works; there is no special recovery-predecessor admission wait. Explicit Stop and interrupt remain terminal. See Restart recovery.

Queued Gateway input retains its authenticated requester. Each new execution resolves current applicable policy. Collecting messages requires compatible operator sources and tool permissions.

When messages.queue.allowCollaborativeSteering: false is explicitly set, another person’s input queues quietly for a new turn under that person’s identity and current policy. It does not interrupt the current turn, and no resend or status notice is needed. Set messages.queue.collaborativeSteeringFallback: "reject" to require an explicit /followup <message> instead. Set messages.queue.allowCollaborativeSteering: true to let admitted human input in the same conversation steer an active turn under that turn's unchanged identity and prepared permissions. This lets Bob influence actions using permissions Alice has that Bob does not. Ordinary guidance from the same authenticated requester retains the active execution’s prepared permissions without substituting incoming caller capabilities or tool bindings. Verified personal profiles with administrator-attested channel identity links can continue the same active session across transports. Current access and the original execution’s live authority still apply. The option does not change queue modes or explicit cross-session sessions_send(mode="steer") authorization. See Steering scope.

An accepted turn can continue after its request returns or its client disconnects. That does not extend revoked device authority or permissions removed by a current operator role. Subsequent actions still check the original source, including work held by an accepted child.

Configure globally or per channel via messages.queue:

json5
{  messages: {    queue: {      mode: "steer",      allowCollaborativeSteering: false,      cap: 20,      drop: "summarize",      byChannel: { discord: "collect", x: "followup" },      debounceMsByChannel: { discord: 1000, x: 500 },    },  },}

Both maps accept channel IDs from bundled and installed plugins. For example, messages.queue.byChannel.x sets the X plugin's queue mode. Config validation warns about IDs absent from the channel registry; install the corresponding plugin or correct the key. Queue modes and nonnegative integer debounce values are still validated.

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. Each summarized entry keeps its own sender and conversation context.
  • drop: "old": drop the oldest queued entries as needed, without preserving summaries.
  • drop: "new": reject the newest message when the queue is already full.

Messages discarded by old or new reach a terminal queue-policy outcome. Durable channel ingress records that outcome and does not replay the discarded message. If every queued entry is protected, capacity rejection remains retryable backpressure. Summarized entries retain their source receipts until the summary is adopted or discarded.

At capacity, optional ambient room events yield to user requests before these drop policies apply; incoming room events cannot evict user requests. With summarize, OpenClaw preserves short previews as background context on an eligible pending turn in the same channel, account, room, thread, and Gateway. This creates no extra model turns. Each recipient keeps the newest min(cap, 20) previews in arrival order, with message text capped at 160 characters and sender/message ID fields capped at 80 characters each. Previews are untrusted context, not requests or sender authority. Input text awaiting its write hook is replaced by an omission notice; approved text is used without restoring redacted content.

Context is attached once when the recipient executes, including across retries. It expires if that recipient is canceled or discarded; if no eligible pending recipient exists, the ambient event is discarded without a preview. old and new discard overflow ambient events without previews. Among ambient events, drop: "new" keeps the existing events, while the other policies keep the newest. Surviving messages retain FIFO order. Active turns, protected recovery work, and events requiring a reply are not eligible for ambient shedding. Each discard is logged without message content, and its channel receipt is settled so durable ingress does not retry it. Recent room history remains subject to the channel's configured history window.

Gateway followup and collect debounce windows measure elapsed time, so wall-clock corrections do not shorten or extend them.

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.

Ordinary steer does not abort in-flight tools. The Control UI's Send now link explicitly interrupts supported tools for a waiting steer while keeping the run alive; unsupported tools show why they must finish. 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.

Answering a pending question

A plain-text answer to a pending agent question goes to that question before ordinary queue handling, including when a native CLI cannot accept steering. OpenClaw checks the answer against the question creator's permissions and active run, not the model selected for your next turn. Changed permissions or a closed creator produce an explicit refusal rather than starting another turn.

If the answer may have committed but confirmation is lost, OpenClaw reports that uncertainty and does not resend it as steering or a followup. Check the conversation before retrying. A later delivery or source-cleanup failure does not make the answer replayable, and uncertainty alone does not cancel the original agent 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 win over config. Then channel-specific debounce (messages.queue.debounceMsByChannel), plugin debounce defaults, and built-in defaults are applied, in that order. cap and drop are global/session options, not per-channel config keys.

One-message followup

/followup <message> sends just that message as a new turn without changing the stored queue mode. It waits behind active and already-queued work, stays separate from collect batches, and bypasses pending-question answering. If the session has no active or queued work, it starts through normal admission. The turn retains its submitter and resolves current policy when admitted.

Queue acceptance appears only after enqueue succeeds. The Control UI shows it in the input queue, without adding an agent reply. Other clients and channels retain a textual acknowledgment. If the message starts immediately, its start notice appears when execution starts, before the answer. Rejected outcomes are reported separately, including when drop:new refuses a /followup. Ordinary overflow drops stay silent on durable channels, where the ingress ledger records the outcome; surfaces without that ledger retain their queue-full feedback. A refused /followup reports that it was not queued, without asking the sender to retry. Queue capacity, overflow, and cancellation rules still apply. Gateway-backed chat and the local embedded TUI support this command. See Steer for the contrast with steering the active run.

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 default or /queue reset clears the session override.

Queued-turn cancellation

Accepted personal browser input keeps its initiating identity while queued. The same signed-in person can stop it from another device if they still have access to control the conversation. Another person's queued input keeps its own cancellation authority, even when the conversation's assigned owner changes.

While a prompt sits in the followup/collect queue (for example a TUI or webchat chat.send arriving while another turn is active), Gateway keeps a Gateway-owned cancel identity for that client runId until the queued content runs or is dropped. The identity follows content folded into an overflow summary.

Once compatible inputs are admitted as one collected turn, the latest cancelable input owns client cancellation. Canceling an older input no longer stops the combined turn. Original operator access still governs execution: revoking that access cancels the collected turn and blocks further effects.

  • chat.abort with a specific runId cancels that turn while it is still queued, if the requester is authorized (same ownership rules as active runs).
  • chat.abort for a session without runId cancels authorized queued turns first, then aborts authorized active runs, including turns admitted or queued before their runner starts (such as OpenAI-compatible HTTP requests), as well as active channel replies. That order prevents queue drain from promoting work into a half-stopped session.
  • Clearing the entire session queue without per-requester checks is not the stop path for multi-owner sessions.
  • A session-wide Stop selects current pending input once at cancellation, including input queued during its preparation. New prompts accepted after that selection keep their own cancellation and queue custody. If the stopped turn is still settling, the new prompt waits for that owner to finish before running.
  • Queued waits are not projected as active agent runs for sessions.list and do not own active-run timeout semantics; only the active phase does.
  • A queued request that expires or is cancelled before execution settles only that request. Its saved input stays marked cancelled; it does not end the active turn, pause its goal, or add an active-run failure to the conversation.
  • Removing a specific queued message in Control UI also hides that pending prompt and its attachments. The cancellation record prevents replay after reconnect. Stop and timeout cancellations keep their recovery messages.

After a successful session reset, an unsent follow-up reply retained for retry still belongs to the original session generation. Recovery does not replay it into the reset session. Recovery without a reset and replies to new turns continue normally.

Gateway-backed clients (including openclaw tui) forward mid-run prompts and let the Gateway apply the queue mode. Esc//stop uses a session-scoped abort so lost local handles cannot leave a still-queued prompt running.

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 remain 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.

Input durability

Ordinary user input sent through chat.send to an existing session is stored in the per-agent database before the Gateway acknowledges it. This includes the Control UI, TUI, CLI, native apps, and RPC clients. Other connected clients can display the accepted input while it waits, without waiting for a new agent turn. In collect mode, appending the combined turn and marking its source inputs consumed happen in one transaction. A browser reconnect can reconcile those source inputs even if it missed their final events.

An unrelated announcement does not consume another active request's accepted input or include it in its model context. The original request keeps that input, including inputs combined by collect mode, for its own continuation.

The chat displays recorded non-Web client sources separately from the sender, for example Alice · via CLI. Web sources are omitted from these labels, including in collected messages that also contain input from another client. Reported app names describe the submitting client; they do not establish a human identity or grant permissions. Collected messages retain their contributing client sources, and older messages without recorded sources keep their existing attribution.

This preserves input, not execution permissions. Graceful shutdown captures the accepted queued input IDs in the root session's resume marker; startup admits those inputs under each sender's current authority after the resume input. Queued rows left by a crash remain inert without interrupted-input notices or automatic resubmission. The user can send a new message. The in-memory queue is not replayed. Host sleep that preserves the process can continue the existing queue normally.

Channel messages retained by durable ingress remain retryable when a queued attempt is abandoned before agent-turn adoption. Abandonment releases that attempt's inbound and queue dedupe entries before ingress retries it. Messages already adopted or consumed keep duplicate suppression, so transport redelivery does not repeat their effects.

Queued channel input is adopted only after the session owner accepts durable input and recovery custody. Preparation failures before acceptance keep ingress retryable; failures after acceptance produce a terminal error reply without replaying the input. Entering the session controller alone does not complete ingress. Repeated preparation errors stop renewing the waiting heartbeat, allowing the existing ingress watchdog to apply its retry and dead-letter policy. Normal capacity waits keep their heartbeat.

Transient session admission conflicts follow the same ingress retry policy as other retryable failures: by default, a message becomes eligible for dead-lettering only after both eight failed attempts and 24 hours, with capped retry backoff. The oldest pending message keeps its place ahead of later messages in the same lane.

Lanes and scope

  • 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 turns; set agents.defaults.maxConcurrent to allow 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 the configured heartbeat session controller still serializes work for that session.
  • Additional lanes may exist (e.g. cron, cron-nested, nested) 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 remain owned by their native runtime.
  • Ordinary sub-agent execution uses subagent:<immediate session>. agents.defaults.subagents.maxConcurrent defaults to 8 for each session; independent sessions and nested orchestrators do not share those slots. The separate maxChildrenPerAgent admission limit still applies. Codex-native subagents use Codex's own scheduler.
  • Swarm collector children use subagent:swarm:<schedulerGroupKey>, capped by the group's resolved tools.swarm.maxConcurrent (default 32). They do not occupy their parent's ordinary sub-agent lane. An ordinary child spawned by a collector uses that collector's own sub-agent lane. Swarm's maxChildrenPerGroup and maxTotalPerGroup remain separate admission limits. Lane diagnostics identify the swarm lane and its group key.
  • The session controller selects one turn at a time; retained source, delivery, and writer settlement can continue to fence its successor.
  • No external dependencies or background worker threads; pure TypeScript + promises.

Background work

Plugin background completions, including dreaming, use the Gateway's subagent lane at background priority. Its concurrency follows agents.defaults.subagents.maxConcurrent across plugins and workspaces (default: 8). Set it to 1 to run one completion at a time. Ordinary spawned sub-agent queues retain their per-spawning-session budgets.

Skill Workshop reviews use a separate background budget, with at most one review running at a time.

Schedulers that await background work do not occupy this budget themselves. Only the dispatched work holds a slot, through completion or cancellation cleanup, so a scheduler cannot block the child it is waiting for. Cancelled queued work is removed before it starts; Gateway restart or runtime retirement prevents stale completions from starting or returning results.

The Control UI System busyness overlay and diagnostics.lanes report plugin completions in the subagent row and Workshop reviews in the background row. Background owner lanes are not counted again in the dynamic session-lane totals.

Troubleshooting

  • If commands seem stuck, enable verbose logs and look for "queued for ...ms" lines to confirm the queue is draining.
  • In lane wait exceeded, activeAhead counts active tasks when the entry was queued; activeNow counts them immediately before it starts. activeAhead=1 activeNow=0 can mean the preceding task just finished, even after a long wait. It does not establish a leaked slot. Admission failures and completed tasks release their slots and drain queued successors even when diagnostics throw; lane task diagnostics failed after settlement identifies a task whose completion logging failed after its caller settled.
  • The operation-owned watchdog observes native progress and real wait deadlines independently of diagnostic logging. A stalled turn requests cancellation of its captured native attempt; unresolved cleanup continues to fence the next turn.
  • 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:
    • Completing a message dispatch does not make an active run idle. A steer handed to a running tool leaves the session processing; the tool remains visible as activeWorkKind=tool_call until it finishes.
    • 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.
    • Incoming messages waiting for admission are waiting, not processing. Diagnostics ask the controller to reconcile stale bookkeeping when no run owns it: runnable mailbox work is woken, while queued input or retained cleanup stays visibly waiting. Reconciliation preserves queue counts and never releases writer custody. The controller watchdog owns active-run recovery. A timeout requests cleanup but never proves that a writer has stopped.
    • Repeated model requests without semantic progress share one stagnation clock, starting with the first retry. A long initial response that hits its output limit therefore leaves a full recovery window for the corrective request. Successfully completed embedded tools reset the clock under their current run owner, even if the enclosing response later hits its output limit. Failed, blocked, or retired-owner tools do not reset it. Fresh transport bytes or later retries cannot renew it indefinitely. Recovery rechecks that evidence before aborting, honors owned tool and provider retry deadlines, and lets the existing run owner settle before the queue drains.
    • Repeated warning lines back off while the session remains unchanged. Watchdog progress, recovery deduplication, and cleanup remain owned by the exact operation, independent of the diagnostic heartbeat.
    • When recovery aborts an interactive turn before it replied and its request is already saved in the transcript, OpenClaw attempts one continuation instead of immediately asking the user to retry. When the next queued follow-up is from the same sender and route (for example a chat.send with queueMode: "followup"), it takes over the outstanding request; otherwise a recovery turn starts on the stalled turn's route. The recovery reply is delivered the same way as any queued follow-up from that source; a Web UI (chat.send) recovery arrives as its own run in the chat. Group-thread participant turns get the notice instead, because queued participant replies have no delivery owner. Both use the existing transcript and any newer message to finish and verify the outstanding request, reconciling pending actions without repeating completed work, or report a blocker. Other senders' queued messages and channel messages that arrived during the stalled turn run afterwards as their own turns. The "stopped making progress" notice is the last resort if that continuation also stalls or cannot be scheduled, including when the original request was not yet saved. Heartbeat and cron turns are unaffected.
Was this useful?