2025-12-09 17:51:05 +00:00
---
summary: "Command queue design that serializes auto-reply command execution"
read_when:
- Changing auto-reply execution or concurrency
---
2026-01-03 04:26:36 +01:00
# Command Queue (2026-01-03)
2025-11-25 04:40:49 +01:00
2025-12-25 23:50:52 +01:00
We now serialize command-based auto-replies (WhatsApp Web listener) through a tiny in-process queue to prevent multiple commands from running at once, while allowing safe parallelism across sessions.
2025-11-25 04:40:49 +01:00
## Why
- Some auto-reply commands are expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
- Serializing avoids competing for terminal/stdin, keeps logs readable, and reduces the chance of rate limits from upstream tools.
## How it works
2026-01-06 20:25:08 +00:00
- [`src/process/command-queue.ts` ](https://github.com/clawdbot/clawdbot/blob/main/src/process/command-queue.ts ) holds a lane-aware FIFO queue and drains each lane synchronously.
2025-12-25 23:50:52 +01:00
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>` ) to guarantee only one active run per session.
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agent.maxConcurrent` .
2025-11-25 04:40:49 +01:00
- When verbose logging is enabled, queued commands emit a short notice if they waited more than ~2s before starting.
- Typing indicators (`onReplyStart` ) still fire immediately on enqueue so user experience is unchanged while we wait our turn.
2026-01-06 18:25:52 +00:00
## Queue modes (per provider)
2026-01-03 04:26:36 +01:00
Inbound messages can steer the current run, wait for a followup turn, or do both:
- `steer` : inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.
- `followup` : enqueue for the next agent turn after the current run ends.
- `collect` : coalesce all queued messages into a **single** followup turn (default).
- `steer-backlog` (aka `steer+backlog` ): steer now **and** preserve the message for a followup turn.
- `interrupt` (legacy): abort the active run for that session, then run the newest message.
- `queue` (legacy alias): same as `steer` .
2025-12-26 13:35:44 +01:00
2026-01-03 18:44:07 +01:00
Steer-backlog means you can get a followup response after the steered run, so
streaming surfaces can look like duplicates. Prefer `collect` /`steer` if you want
one response per inbound message.
2026-01-06 14:17:56 -06:00
Send `/queue collect` as a standalone command (per-session) or set `routing.queue.byProvider.discord: "collect"` .
2026-01-03 18:44:07 +01:00
2025-12-26 13:35:44 +01:00
Defaults (when unset in config):
2026-01-03 04:26:36 +01:00
- All surfaces → `collect`
2025-12-26 13:35:44 +01:00
2026-01-06 18:25:52 +00:00
Configure globally or per provider via `routing.queue` :
2025-12-26 13:35:44 +01:00
```json5
{
routing: {
queue: {
2026-01-03 04:26:36 +01:00
mode: "collect",
debounceMs: 1000,
cap: 20,
drop: "summarize",
2026-01-06 18:25:52 +00:00
byProvider: { discord: "collect" }
2025-12-26 13:35:44 +01:00
}
}
}
```
2026-01-03 04:26:36 +01:00
## Queue options
Options apply to `followup` , `collect` , and `steer-backlog` (and to `steer` when it falls back to followup):
- `debounceMs` : wait for quiet before starting a followup turn (prevents “continue, continue”).
- `cap` : max queued messages per session.
- `drop` : overflow policy (`old` , `new` , `summarize` ).
Summarize keeps a short bullet list of dropped messages and injects it as a synthetic followup prompt.
Defaults: `debounceMs: 1000` , `cap: 20` , `drop: summarize` .
2025-12-26 13:35:44 +01:00
## Per-session overrides
2026-01-06 14:17:56 -06:00
- Send `/queue <mode>` as a standalone command to store the mode for the current session.
2026-01-03 04:26:36 +01:00
- Options can be combined: `/queue collect debounce:2s cap:25 drop:summarize`
2025-12-26 14:24:53 +01:00
- `/queue default` or `/queue reset` clears the session override.
2025-12-26 13:35:44 +01:00
2025-11-25 04:40:49 +01:00
## Scope and guarantees
- Applies only to config-driven command replies; plain text replies are unaffected.
2025-12-25 23:50:52 +01:00
- Default lane (`main` ) is process-wide for inbound + main heartbeats; set `agent.maxConcurrent` to allow multiple sessions in parallel.
2025-12-13 02:34:11 +00:00
- Additional lanes may exist (e.g. `cron` ) so background jobs can run in parallel without blocking inbound replies.
2025-12-25 23:50:52 +01:00
- Per-session lanes guarantee that only one agent run touches a given session at a time.
2025-11-25 04:40:49 +01:00
- No external dependencies or background worker threads; pure TypeScript + promises.
## Troubleshooting
- If commands seem stuck, enable verbose logs and look for “queued for …ms” lines to confirm the queue is draining.
- `enqueueCommand` exposes a lightweight `getQueueSize()` helper if you need to surface queue depth in future diagnostics.