Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d9cc610
feat: Implement comprehensive test suite for queue commands
Aug 2, 2026
9d776d8
docs: Update documentation for prompt queue feature
Aug 2, 2026
73041ce
fix: Correct format_command_result calls in remove_queue
Aug 2, 2026
0a337de
fix: Correct queue command error handling and test assertions
Aug 2, 2026
eea4b81
fix: Update test assertions for remove-queue interactive mode
Aug 2, 2026
3c06656
fix: Correct format_command_result usage in queue commands
Aug 2, 2026
e44847a
cli33: merged
Sep 1, 2026
45d5e65
update reqs
Sep 3, 2026
7cc0d5e
feat: preserve prompt queue on model switch and isolate sub-agents
Sep 4, 2026
798bc1a
fix: improve prompt queue processing and fix source code corruption
Sep 5, 2026
1be85a7
feat: scope queue commands to the active agent
Sep 5, 2026
f1de8a0
fix: isolate sub-agent prompt queues by guarding inheritance
Sep 5, 2026
12e87a9
feat: implement /insert-queue command
Sep 6, 2026
61f913c
feat: implement /insert-queue command dispatch in TUI
Sep 6, 2026
9f76025
merged
Sep 6, 2026
c5e36a9
feat: implement prompt queue management system (CLI-33)
Sep 7, 2026
a347481
fix linting
Sep 7, 2026
34f6042
fix linting
Sep 7, 2026
db22778
fix linting
Sep 7, 2026
9adb066
fix: restore missing command methods and fix AttributeError in execute
Sep 7, 2026
2847ff5
fix: remove non-existent custom_commands assertion in test
Sep 7, 2026
5d26e5f
fix: remove unused variable 'commands' in test_command_paths_none_doe…
Sep 7, 2026
dd71975
docs: add `/insert-queue` to command documentation
Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,17 @@
* [Benchmark Results By Language](https://github.com/dwash96/aider-ce/pull/27)
* [Allow Benchmarks to Use Repo Map For Better Accuracy](https://github.com/dwash96/aider-ce/pull/25)
* [Read File Globbing](https://github.com/Aider-AI/aider/pull/3395)

### Prompt Queueing (CLI-33)
- Added `/queue <prompt>` to add prompts to a FIFO queue.
- Added `/list-queue` to view queued prompts.
- Added `/remove-queue [index|*]` to remove specific prompts or clear the queue.
- Queued prompts process automatically after the current command completes.
- Queue state persists across command executions within a session.
- Management commands do not trigger queue processing.
- Queue data structure implemented in `Commands` class with thread-safe operations using `asyncio.Lock`.
- Max queue size limit of 100 items.
* [Prompt Queueing](https://github.com/cecli-dev/cecli/issues/33)
* Added `/queue`, `/list-queue`, and `/remove-queue` commands for deferred prompt processing.
* Queued prompts process automatically in FIFO order when the system is idle.
* Queue state is in-memory and session-specific.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Use curl to download the script and execute it with sh:
curl -LsSf https://cecli.dev/install.sh | sh
```

If your system doesn't have curl, you can use wget:
If your system does not have curl, you can use wget:

```bash
wget -qO- https://cecli.dev/install.sh | sh
Expand Down Expand Up @@ -53,7 +53,7 @@ pip install cecli-dev
uv tool install --native-tls --python python3.12 cecli-dev
```

Use the tool installation so cecli doesn't interfere with your development environment
Use the tool installation so cecli doesn't interfere with your development environment.

## Features and Documentation:

Expand Down
219 changes: 141 additions & 78 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ async def create(
kwargs = use_kwargs
from_coder.ok_to_warm_cache = False

# Preserve the prompt queue across a model switch (same coder
# identity) but NOT when spawning a distinct sub-agent (which is
# assigned a fresh uuid in its kwargs). Model switches keep
# from_coder.uuid; sub-agents override it with a new uuid.
is_model_switch = from_coder.uuid == kwargs.get("uuid", "")
if is_model_switch:
kwargs.setdefault("prompt_queue", from_coder.prompt_queue)
kwargs.setdefault("_queue_counter", from_coder._queue_counter)

res = None
if (
getattr(main_model, "copy_paste_mode", False)
Expand All @@ -357,6 +366,17 @@ async def create(
if from_coder.tui:
res.tui = from_coder.tui

# Preserve prompt queue state across model switches (CLI-33).
# The queue lives on the coder (see command_queue.py) so the
# new coder instance must inherit the list and counter, while
# the lock and processing flag are intentionally fresh.
# Only model switches inherit the queue; sub-agents get their
# own isolated queue (each agent has its own context/queue).
if is_model_switch:
res.prompt_queue = from_coder.prompt_queue
res._queue_counter = from_coder._queue_counter
res.tui = from_coder.tui

# Sub-agents get a dedicated, independent MCP manager so they
# can rebuild a custom tool list (their own LocalServer tools /
# filters) and be disconnected independently from the parent.
Expand Down Expand Up @@ -457,6 +477,8 @@ def __init__(
root=None,
primary_root=None,
init_metadata={},
prompt_queue=None,
_queue_counter=None,
):
from cecli.helpers.agents.service import AgentService

Expand Down Expand Up @@ -611,12 +633,12 @@ def __init__(
self.commands = commands or Commands(self.io, self, args=args)
self.commands.coder = self

# Prompt queue for CLI-33: in-memory FIFO queue for deferred prompt
# processing. The queue lives on the coder so primary agents and
# sub-agents each have their own independent queue, managed by
# cecli.helpers.command_queue.
self.prompt_queue = []
self._queue_counter = 0
# Prompt queue for CLI-33: in-memory FIFO queue managed by
# cecli.helpers.command_queue. The queue lives on the Coder so it
# survives model switches (Coder.create preserves identity) while each
# sub-agent gets its own isolated queue.
self.prompt_queue = list(prompt_queue) if prompt_queue else []
self._queue_counter = _queue_counter if _queue_counter else 0
self._queue_lock = threading.Lock()
self._processing_queue = False

Expand Down Expand Up @@ -1962,97 +1984,138 @@ async def run_one(self, user_message, preproc):
else:
message = user_message

if self.commands.is_command(user_message) and not self.commands.is_test_command(
user_message
):
return
# /list-queue and /remove-queue must not trigger auto-processing of the
# queue (CLI-33). Every other message/command — including /queue, so a
# prompt queued while idle processes immediately — drains the queue once
# it has fully completed, so queued prompts are sent to the LLM after
# the current prompt finishes.
is_management_command = self._is_queue_management_command(user_message)

if not self.commands.is_command(user_message):
ConversationService.get_chunks(self).flush_removals()
self.last_user_message = user_message
self.error_code = None
self.num_tool_calls = 0
# Trim memory in the background so it doesn't delay the response
coroutines.fire_and_forget(asyncio.to_thread(trim_memory))
# Fire memorizer after each user request
# if self.auto_memory and self.edit_format not in ["subagent"]:
# from cecli.helpers.memory.utils import invoke_memorizer
#
# context = "If the user has stated any preferences, please remember them"
# asyncio.create_task(invoke_memorizer(self, additional_context=context))
try:
if self.commands.is_command(user_message) and not self.commands.is_test_command(
user_message
):
return

while True:
self.reflected_message = None
self.empty_response = False
self.tool_reflection = False
if not self.commands.is_command(user_message):
ConversationService.get_chunks(self).flush_removals()
self.last_user_message = user_message
self.error_code = None
self.num_tool_calls = 0
# Trim memory in the background so it doesn't delay the response
coroutines.fire_and_forget(asyncio.to_thread(trim_memory))
# Fire memorizer after each user request
# if self.auto_memory and self.edit_format not in ["subagent"]:
# from cecli.helpers.memory.utils import invoke_memorizer
#
# context = "If the user has stated any preferences, please remember them"
# asyncio.create_task(invoke_memorizer(self, additional_context=context))

if float(self.total_cost) > self.cost_multiplier * (
nested.getter(self.args, "cost_limit", float("inf")) or float("inf")
):
if await self.io.confirm_ask(
"You have reached your configured cost limit. Continue?",
group_response="Cost Limit",
explicit_yes_required=True,
while True:
self.reflected_message = None
self.empty_response = False
self.tool_reflection = False

if float(self.total_cost) > self.cost_multiplier * (
nested.getter(self.args, "cost_limit", float("inf")) or float("inf")
):
Coder.cost_multiplier += 1
else:
return
if await self.io.confirm_ask(
"You have reached your configured cost limit. Continue?",
group_response="Cost Limit",
explicit_yes_required=True,
):
Coder.cost_multiplier += 1
else:
return

async for _ in self.send_message(message):
pass
async for _ in self.send_message(message):
pass

await self.hot_reload()
await self.hot_reload()

if not self.empty_response:
if not self.reflected_message:
await self.auto_save_session(force=True)
break
if not self.empty_response:
if not self.reflected_message:
await self.auto_save_session(force=True)
break

if self.num_reflections >= self.max_reflections:
self.io.tool_warning(
f"Only {self.max_reflections} reflections allowed, stopping."
)
break
if self.num_reflections >= self.max_reflections:
self.io.tool_warning(
f"Only {self.max_reflections} reflections allowed, stopping."
)
break

self.num_reflections += 1
self.num_reflections += 1

if self.tool_reflection:
self.num_reflections -= 1
if self.tool_reflection:
self.num_reflections -= 1

if self.reflected_message is True:
message = None
else:
message = self.reflected_message
elif self.stop_on_empty:
await self.auto_save_session(force=True)
break
if self.reflected_message is True:
message = None
else:
message = self.reflected_message
elif self.stop_on_empty:
await self.auto_save_session(force=True)
break

if self.enable_context_compaction:
await self.compact_context_if_needed()
if self.enable_context_compaction:
await self.compact_context_if_needed()

if nested.getter(self, "agent_finished", False):
await self.auto_save_session(force=True)
break

if nested.getter(self, "agent_finished", False):
await self.auto_save_session(force=True)
break
finally:
# Drain the queue once the current message/command has fully
# completed, so queued prompts are sent to the LLM (CLI-33).
if not is_management_command:
await self._drain_prompt_queue(preproc)

await self.auto_save_session(force=True)
if not await HookIntegration.call_end_hooks(self):
self.io.tool_warning("Execution stopped by end hook")
return

# Move to the next queued prompt (CLI-33) only after the current message
# has fully completed, so the queue drains within run_one() instead of
# being watched by the generation loops.
if self.prompt_queue and not self._processing_queue:
self._processing_queue = True
try:
item = command_queue.dequeue_prompt(self)
finally:
self._processing_queue = False
def _is_queue_management_command(self, user_message):
"""Return True if user_message is a read-only queue-management command.

Only /list-queue and /remove-queue are excluded from auto-processing:
they inspect or mutate the queue without the user asking queued prompts
to run. /queue is deliberately NOT management here, so a prompt queued
while the coder is idle drains (and processes) immediately afterward.
"""
if not self.commands or not self.commands.is_command(user_message):
return False
words = user_message.strip().split()
if not words:
return False
cmd_name = words[0][1:]
return cmd_name in ("list-queue", "remove-queue")

if item is not None:
self.io.tool_output(f"Processing queued prompt (id: {item['id']})...")
await self.run_one(item["text"], preproc)
async def _drain_prompt_queue(self, preproc):
"""Process all queued prompts (FIFO) once the current message completes.

if not await HookIntegration.call_end_hooks(self):
self.io.tool_warning("Execution stopped by end hook")
Runs each queued prompt through ``run_one`` so it is sent to the LLM
exactly like a user-typed prompt. A single failing queued prompt is
logged and does not stop the remaining ones (``SwitchCoderSignal`` and
``ReloadProgramSignal`` are BaseExceptions and propagate unchanged).
"""
if self._processing_queue:
return
self._processing_queue = True
try:
while True:
item = command_queue.dequeue_prompt(self)
if item is None:
break
text = item["text"]
preview = text if len(text) <= 80 else text[:80] + "..."
self.io.tool_output(f"Processing queued prompt: {preview}")
try:
await self.run_one(text, preproc)
except Exception as e:
self.io.tool_error(f"Error processing queued prompt: {e}")
finally:
self._processing_queue = False

def _is_url_allowed(self, url):
allowed_domains = self.security_config.get("allowed-domains")
Expand Down
6 changes: 6 additions & 0 deletions cecli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .hooks import HooksCommand
from .hot_reload import HotReloadCommand
from .include_skill import IncludeSkillCommand
from .insert_queue import InsertQueueCommand
from .lint import LintCommand
from .list_mcp import ListMcpCommand
from .list_queue import ListQueueCommand
Expand Down Expand Up @@ -134,9 +135,11 @@
CommandRegistry.register(SpawnAgentCommand)
CommandRegistry.register(SwitchAgentCommand)
CommandRegistry.register(IncludeSkillCommand)
CommandRegistry.register(InsertQueueCommand)
CommandRegistry.register(LintCommand)
CommandRegistry.register(ListMcpCommand)
CommandRegistry.register(ListQueueCommand)
CommandRegistry.register(RemoveQueueCommand)
CommandRegistry.register(ListSessionsCommand)
CommandRegistry.register(ListSkillsCommand)
CommandRegistry.register(LoadCommand)
Expand Down Expand Up @@ -221,11 +224,13 @@
"HooksCommand",
"HotReloadCommand",
"IncludeSkillCommand",
"InsertQueueCommand",
"ReapAgentCommand",
"SpawnAgentCommand",
"SwitchAgentCommand",
"LintCommand",
"ListSessionsCommand",
"ListQueueCommand",
"ListSkillsCommand",
"LoadCommand",
"LoadHookCommand",
Expand All @@ -244,6 +249,7 @@
"PasteCommand",
"quote_filename",
"QueueCommand",
"RemoveQueueCommand",
"QuitCommand",
"ReadOnlyCommand",
"ReadOnlyStubCommand",
Expand Down
Loading
Loading