You message your agent three times while it's mid-task. What should happen — interrupt it, queue them, fold them in, or merge them? OpenClaw's command queue answers this with lanes, concurrency caps, and four inbound modes. You'll build the lane-aware scheduler, the four modes, and the drop policies that keep it from drowning.
By Lesson 5 your gateway can run an agent: a message comes in, it builds a prompt, calls the model, runs tools, streams a reply back. Nice and tidy — when there is exactly one message at a time. But people don't message one at a time. You ask your assistant to "summarize that long email," and while it's reading the email you fire off "actually, keep it under three sentences," and then "and make it friendly." Three messages, one conversation, and the agent is still working on the first.
Watch what goes wrong if you let each message just start its own run. Run one reads the conversation, thinks, and begins writing back: "Here is your summary…" Meanwhile run two also started — it read the same conversation a moment later, is also writing, and appends its own "Here is your summary…" The two runs interleave. Both are writing to the one shared transcript — the list of "user said… / assistant said…" turns that is the conversation's memory — and their words braid together into nonsense. Worse, run two never saw run one's answer, so it can't build on it; it just clobbers it.
But "only one run at a time" is too blunt if you take it globally. You might be chatting with the agent on your phone and have a scheduled job running in the background and a teammate talking to it in a different channel. Those are different conversations — different transcripts — so there is no race between them. Forcing them to wait in one global line would make the whole system feel sluggish for no reason.
So the queue's job has three parts, and the whole rest of this lesson is just filling them in. Serialize within a conversation (one run at a time per conversation, so transcripts never collide). Parallelize across conversations (different conversations run side by side). And decide what to do with a message that arrives mid-run — does it wait, fold into the running agent, merge with its neighbors, or stop the agent cold? Get those three right and three rapid-fire messages become a feature, not a corruption bug.
The queue's first job — serialize within a conversation, parallelize across — has a clean shape: a lane-aware FIFO queue. Let's unpack that two words at a time.
FIFO means first in, first out: jobs come out in the order they went in, like a single-file line at a counter. Lane-aware means there isn't one global line — there are many lines, one per conversation, and a job is filed into a lane by its session key. The session key is just the identity of a conversation: roughly "this channel, this chat." Two messages from the same chat share a session key, so they land in the same lane; messages from different chats get different keys and different lanes.
Now, "at most one per lane" is the default, but it isn't the only setting. Each lane carries a concurrency cap — the maximum number of runs allowed active in that lane at once. OpenClaw's defaults are deliberate: an unconfigured lane caps at 1 (the strict serialize-everything default), the main lane — your foreground chat — caps at 4, and the subagent lane (spawned helper agents) caps at 8. The numbers grow as the work gets more independent: foreground turns within a chat are fairly sequential, but eight subagents doing eight unrelated sub-tasks can safely fly in parallel.
Sitting above all of this is one more dial: a global cap, agents.defaults.maxConcurrent, that bounds the total number of runs active across every lane at once. Per-lane caps protect each conversation; the global cap protects the machine — it stops a thundering herd of conversations from all firing model calls at the same instant and melting your rate limits or your CPU.
Let's make the invariant concrete by building the scheduler itself: file jobs into lanes by session key, and prove that within a lane runs never overlap while across lanes they happily do.
Two conversations, A and B, shown as horizontal tracks. Messages arrive as tokens and turn into runs (filled bars). Inside a lane only ONE bar is active at a time; the rest wait in arrival order. Across lanes the bars happily overlap. Drag the arrival rate up and watch each lane back up while the other keeps flowing.
The lane scheduler handles order. But it leaves the most interesting question open: a message arrives while the agent is mid-run on that same conversation — what should happen to it? There isn't one right answer; it depends on what you meant. So OpenClaw exposes four inbound modes, and the queue applies whichever one is active.
Map them to the email example. You said "summarize that email," then "keep it under three sentences." With steer, the second message slips into the running agent and it shortens the summary it's already writing — one smooth answer. With followup, it finishes the long summary first, then does a second turn applying the constraint — two answers, both honored. With collect, if you'd typed all three lines in a flurry, the queue waits for you to stop, then treats "summarize / under three sentences / and friendly" as one combined request. With interrupt, if you'd instead said "never mind, what's the weather?", the email run is pointless — abort it and answer the weather.
Why is steer the default? Because it matches the most common human intent: while the agent works, you usually want to refine, not restart or stack up replies. Steering feels like talking to a person who is already on the task — "oh, and make it shorter" lands while they're still writing. The other modes are there for the cases steer doesn't fit. Let's implement the decision itself: given a mode and the current run state, what action does the queue take?
"Inject the message into the running agent" sounds magical. It isn't — and the un-magical detail is exactly what makes it safe. Steering does not reach into the agent and yank it mid-thought. In particular, it does not abort in-flight tool calls. If the agent is halfway through reading a file or calling an API, that work runs to completion. Killing a tool call in the middle could leave a half-written file or a dangling request — steering never risks that.
Instead, steering waits for a natural seam. Recall the agent loop from Lesson 4: the agent calls the model, the model asks for some tools, the tools run, their results come back, and then the model is called again with those results. The injected message is delivered after the current batch of tool calls completes and before the next model call begins. That gap — tools done, next think not yet started — is the seam. The new instruction rides in with the tool results, so the very next thing the model sees includes your steer.
What you actually see depends on streaming, which we met in Lesson 5. With partial/block streaming on — where the gateway emits the reply in chunks as it forms — steering produces visible, sequential replies: you watch the agent's answer bend toward your new instruction in near real time. It feels like a conversation, because it is one.
Without streaming, the runtime sometimes can't accept a steer in the same turn — there's no live seam to slip into because the whole turn is computed in one shot. For that case there is a fallback: when same-turn steering isn't possible, the queue gracefully degrades, handling the message on the next turn instead (effectively a followup) rather than failing or losing it. Steer when you can; fall back cleanly when you can't.
Here's a failure mode the first three modes don't solve. You're typing a multi-line thought and you hit Enter after each line out of habit: "summarize the email" … "actually under three sentences" … "and make it friendly." Three messages in two seconds. Under followup that's three separate runs stacked up; under steer it's three injections into a run that barely started. Either way you've spent three times the work on what was really one request you happened to type in pieces.
The collect mode fixes this with an idea borrowed straight from electronics and UI engineering: debounce. A debounced action doesn't fire on every event; it waits for a quiet window — a stretch of time with no new events — and only then acts on everything that piled up. A bouncing switch settles; a flurry of keystrokes settles; then you respond once.
The duration is configurable, and OpenClaw accepts friendly units: you can write the window in milliseconds, seconds, minutes, hours, or days (the unit suffixes are ms, s, m, h, d). A chat assistant might use half a second; a digest job that batches notifications might use minutes or hours. Same mechanism, wildly different time scales — the debounce window is just "how long is quiet enough to assume the human is done."
Collect is the mode that turns the queue from a faithful order-keeper into something that understands human typing rhythm. It trades a tiny bit of latency — you wait out the quiet window — for a big win in coherence and cost: one thoughtful answer to your whole thought, instead of three half-answers racing each other.
Modes decide what to do with a message that arrives during a run. But there's a darker question underneath: what if messages arrive faster than the agent can ever drain them? A lane that just keeps appending will grow without bound — an unbounded queue that quietly eats memory until the process falls over. This is the classic backpressure problem: when the producer outpaces the consumer, you need a rule for pushing back.
OpenClaw's first line of defense is a per-session cap on how many pending messages a lane may hold — default 20. The cap is the promise that a single runaway conversation can never consume unbounded memory. But a cap alone only says "no more than this"; it doesn't say which messages survive when you're full. That's the drop policy.
Why is summarize the default? Because it's the least surprising. With old, the agent silently forgets things you said — it might answer as if you never mentioned a constraint. With new, your latest message just disappears, which feels broken. Summarize keeps the queue bounded and preserves a breadcrumb of what was shed, so the agent's view degrades gracefully instead of going silently wrong. Bounded memory, minimal surprise.
Let's build the three policies and prove the property that matters: every one of them keeps the queue at or under the cap, while the naive "just append" version blows past it.
summarize the default drop policy rather than old?We now have four modes plus a cap and three drop policies. But who chooses? You might want steer everywhere by default, followup in one noisy channel, and collect for a single burst-prone conversation. OpenClaw resolves this with a clear precedence order: when several settings could apply, the most specific one wins.
/queue override — you typed a command in this conversation. Most specific; always wins.messages.queue.byChannel for this conversation's channel. "In Slack, always followup."messages.queue.mode. The site-wide default you configured.steer. What you get if nothing above is set.This is the same precedence logic you've seen in CSS, in config files, in shell environments: a cascade from broad defaults down to narrow overrides, with the narrowest winning. It means you can set one sensible global mode and then carve out exceptions per channel or per conversation without touching the global setting. Specificity is authority.
The per-session override is driven by a small command language. In a conversation you can type:
commands # set this conversation's mode /queue steer /queue collect # set mode plus its parameters in one line /queue collect debounce:0.5s cap:25 drop:summarize # clear the per-session override (fall back to channel/global/default) /queue default /queue reset
So /queue collect debounce:0.5s cap:25 drop:summarize says, for this conversation only: coalesce bursts, with a half-second quiet window, hold up to 25 messages, and summarize on overflow. And /queue reset tears that override back down so the conversation falls through to the channel setting, then the global mode, then the built-in steer. The cascade is always there; the override just sits on top.
steer, the Slack channel is configured to followup, and in one Slack thread you typed /queue collect. What mode does that thread use?We've talked about the main lane — your foreground chat — and the subagent lane. But the gateway also runs work that no human is watching in real time, and that work needs its own lanes so it can never starve the conversation you're actively having.
Picture the alternative without lanes: a scheduled job wakes up to run your nightly digest at the exact moment you message the agent. If they shared one line, your message would wait behind a long batch job — the system would feel frozen precisely when you're using it. Lanes prevent that by giving background work separate tracks that proceed in parallel with foreground chat.
main and subagent, OpenClaw carries lanes for scheduled and spawned work:
The point of naming them is isolation: foreground chat lives in main, scheduled work lives in cron, spawned work lives in nested/subagent — and because each lane has its own concurrency budget, a burst of background activity can fill its own lanes without ever blocking the lane your live conversation is in. Background work can't starve foreground chat, by construction.
cron jobs their own lane separate from the main conversation lane?Here is the whole queue in motion on one conversation. An agent is running on session A with a progress bar. Press Inject message while it runs and watch the active mode decide what happens: steer folds it in at the next tool seam, followup queues it for after the run, collect waits out a debounce timer then merges, interrupt aborts and restarts on the newest. Flip to a tiny cap and flood it to see the drop policy shed messages. This one widget is everything from Chapters 1–7.
The bar is the running agent on session A; the small ticks are its tool seams. Pick a mode, press Inject message a few times, and watch the queue react. Lower the cap and inject faster than it drains to watch the chosen drop policy shed messages. The readout names every action as it happens.