← Writing

OpenClaw

I Said “Don't Change It Yet.” Why Is OpenClaw Still Working?

Why does “don’t change it yet” fail to stop an assistant immediately? Follow session lanes, shared capacity, steering, and timeouts through OpenClaw’s handling of new input.

Let's continue the hypothetical scene from the previous articles. OpenClaw has read the meeting notes and saved the checklist. You ask it to break the second action item down further and update the file.

As it starts working, you remember something still needs confirmation. You quickly add:

Don't change the file yet. Just show me the proposal.

The chat shows that your message arrived. A moment later, the file changes anyway.

The natural reaction is: why didn't it listen? Before judging the model's understanding, however, we need to establish when the correction actually entered the execution flow.

A message reaching the service, entering the current run, being read by the next model call, and stopping a running tool are different events. This article follows that correction through OpenClaw's handling of incoming messages while it is busy.

We continue with OpenClaw v2026.5.22 and Pi 0.75.4, focusing on ordinary auto-replies and the embedded Pi path. The example is not a real incident report, and the behavior described here should not be generalized to every newer runtime. Queue documentation at this version

A new message need not start another assistant

Starting an independent agent run for every incoming message would quickly become complicated.

The first run could still be modifying the checklist under the old instructions while the second already knows about the correction. Each might read the same history, append its own messages, and invoke tools. The resulting order could reflect which network request returned first rather than which instruction you gave first.

Limiting the entire service to one request at a time creates the opposite problem: while your slow tool runs, an unrelated conversation has to wait too.

OpenClaw separates these concerns. Message policy decides how new input participates in the conversation; execution scheduling decides when a run may start. Sometimes the message enters the existing run, so no second independent run is needed. Auto-reply entry point

This distinction matters when debugging. The number of queued runs cannot explain every user experience. No second run in the queue does not mean the second message was lost. It may be waiting as steering input for the current run to consume at an appropriate boundary.

One conversation queues; different conversations can work together

First, suppose the new message does require an independent run.

In the selected default scheduling path of runEmbeddedPiAgent, execution enters a session lane such as session:<key>, then requests a place in a shared workload lane from inside it. Ordinary foreground work uses main by default; other workload types can use different lanes. Two-layer scheduling entry point

The session lane asks whether the previous run in this conversation has finished. The global lane asks whether this class of work has spare concurrency capacity.

Suppose the shared capacity is two and conversations A, B, and C all have work. A1 and B1 can execute together while C1 waits. If A2 arrives in conversation A, it waits behind A1 in A's lane. It does not immediately enter the global queue, consume a slot, and then sit there waiting for A1.

Separate lanes for sessions A, B, and C feed a shared lane with capacity two; A2 waits in its session while C1 waits for global capacity
This illustrates normal scheduling. Conversation order and shared execution capacity are separate constraints.

One benefit of this arrangement is that preventing ordinary runs from advancing the same history simultaneously is separate from serving multiple conversations at once. That is my interpretation of the structure, not an attributed statement of the authors' intent.

There is another qualification. “Global” here means shared capacity in a particular in-process workload lane, not one lock covering the entire deployment. The code also has names such as cron, cron-nested, and subagent. This version maps inner cron execution to cron-nested to avoid having an outer job occupy a cron slot while its inner execution waits for the same capacity. Lane names and mapping

You therefore cannot infer that all background activity together is bounded by the main limit. Normal serialization also needs to be understood alongside the timeout and recovery boundaries discussed below.

Should it listen now or handle this next time?

Return to “Don't change the file yet.” This version offers four main ways to handle new input in a busy session.

Mode How the message participates Important boundary
steer Enters the active run when it can accept input Does not directly interrupt a running tool
followup Becomes a later request after the active run ends The original task continues first
collect Combines compatible queued messages into a later turn after a quiet window Different reply channels or threads may need separate handling
interrupt Requests abortion of the active run, then handles the new message Cancellation does not roll back completed actions

These modes express different interaction intentions. A burst of additions such as “group by owner” and “also add deadlines” may suit collection. Changing the direction of ongoing reasoning is closer to steering. Replacing the current task calls for an interruption request. Mode definitions

The settings resolver in this version defaults to steer. Overrides are resolved from inline settings, saved session settings, channel configuration, global configuration, and finally the default. A default remembered from another release, or an old setting stored on this conversation, is not enough to predict the current behavior. Settings resolution

The names can also imply more than the mechanism guarantees. Selecting steer asks the program to deliver input to the current run; it does not mean the model has already understood and acted on it. If the run cannot accept input, the host must take a waiting or follow-up path. The auto-reply entry checks whether the run is streaming and awaits the injection result before returning from the accepted-steering path.

When does the correction actually become visible?

This is where user intuition and execution timing most easily diverge.

In the pinned Pi loop, when the model produces an assistant message containing tool calls, the executor processes that tool batch, collects its results, and emits the turn-end event. On the normal continuation path, it then drains pending steering messages for the next model call. Pi execution loop

If your correction arrives while the file tool is already running, it cannot pass through the active call stack and automatically reverse a write. It can affect later model decisions: for example, do not edit any more files, and explain what has already happened.

Pinned Pi execution processes the current tool batch and results before draining steering messages for the next model call; this does not interrupt or roll back tools
Receiving a correction and making it visible to the next decision may be separated by an unfinished batch of tool calls.

The boundary is not there to deliberately ignore the user. Tool calls need their corresponding results, and the run needs a defined place to incorporate new input. But the experience has a cost: a slow batch can make it look as though you called for a stop and the assistant carried on regardless.

A more accurate acknowledgement would say that the correction was received and will be considered at the next decision, rather than unconditionally saying that execution stopped. This is a product suggestion I would adopt, not a claim about OpenClaw's current interface.

Two different uses of “follow-up” also need separating. OpenClaw's followup mode schedules later requests after the active run finishes. Pi's loop has its own getFollowUpMessages() callback, consulted when that loop would otherwise stop. Similar names do not make them the same queue, and an internal loop continuation does not necessarily mean the host started a new run. Steering boundary documentation

Queued work is not always strictly first come, first served

Reading the queue documentation can leave the impression that every lane is FIFO: whoever arrived first always runs first.

The source snapshot also supports foreground, normal, and background priorities. Insertion compares priority first and enqueue sequence second. Equal-priority tasks preserve arrival order; higher-priority tasks can move ahead of lower-priority tasks that have not started. Queue insertion

The run entry maps user and manual triggers to foreground, and triggers such as cron and heartbeat to background. This explains why a later user request might start before queued background work. It does not mean it can preempt a running task. Trigger-to-priority mapping

I tested the original scheduler with controlled tasks. First, I set a test lane's concurrency to zero so it would accept entries without dispatching them. I submitted background, normal, foreground one, and foreground two, then restored capacity to one. The observed order was foreground one, foreground two, normal, background. In another check, a foreground request still waited when a task was already running.

This is a useful reason to inspect the implementation. FIFO describes ordering within one priority level, but not the entire scheduling policy. Whether sustained foreground traffic could leave background work waiting for too long deserves further load testing. This investigation produced no starvation-duration or throughput measurements.

A free queue slot does not mean the old work vanished

When an ordinary task succeeds or fails, the scheduler finishes its active-task bookkeeping and advances the lane. A thrown error should not block the entire lane forever.

Clearing the waiting queue is different. clearCommandLane() removes entries that have not started and rejects their callers with a specific clearing error. Active work remains active. Treating “queue cleared” as “everything stopped” gives the wrong state. Clear and recovery entry points

Timeout handling needs even closer attention. The configurable task timeout in the underlying scheduler uses Promise.race. When it expires, the waiting caller is rejected and the scheduler releases the slot so later work can proceed. The original task function may still be unwinding or return later. The implementation handles a late rejection; the race itself does not kill the function. Timeout implementation

I verified this with a Promise whose completion was controlled by the fixture. The timeout error had returned and the next task had started, while the old function could still finish afterward. This establishes the low-level queue's semantics. It is not a reproduced end-to-end OpenClaw failure in which two models corrupt a session. The host has cancellation signals, run registration, and write locks that must also be traced before reaching such a conclusion.

A session execution lane and a session file lock are not interchangeable either. The former coordinates in-process runs; the latter protects records around particular write phases. The selected implementation releases and reacquires locks and handles cleanup separately. It does not simply hold the same file lock throughout every model call. Session write-lock adapter

What I would specify first in my own assistant

This investigation completed 25 local checks against the original queue and lane modules, replacing only the diagnostic logging outlet. They cover same-session waiting, independent sessions, shared capacity, priority, failure release, clearing, timeout, and rejection of new work during gateway draining. The two-layer scheduling test uses a fixture with the same wrapper shape as the source; it does not start the full Gateway, a real model, or a messaging channel. Steering behavior was checked against pinned implementation and documentation, not exercised through an end-to-end injection test.

For a small assistant built from scratch, I would define message policy, execution scheduling, and cancellation as separate contracts. Message policy decides where additional input belongs. Scheduling controls order and capacity. Tool adapters specify whether an action can be cancelled and how its outcome is confirmed. A vague busy boolean should not carry all those responsibilities.

This scheduler stores its state in a process-global singleton so different bundled modules in that process share it. It is not a durable message broker and does not automatically coordinate multiple machines. Multiple execution instances require an additional ownership, persistence, and recovery design. In-process state

Return to “Don't change it yet.” A trustworthy assistant should not only accept the correction. It should be able to explain where the new instruction is waiting, whether the current action can stop, and which effects have already occurred. The next article explores the other half of that experience: is the text appearing while it works raw model output, or does channel adaptation shape it into the message you see?

继续阅读