Skip to content

fix: drain queue-mode steers at the idle prompt (Discord/queue messages stuck 'pending') - #716

Open
kooricold wants to merge 1 commit into
mpfaffenberger:mainfrom
kooricold:fix/idle-queued-steer-wake
Open

fix: drain queue-mode steers at the idle prompt (Discord/queue messages stuck 'pending')#716
kooricold wants to merge 1 commit into
mpfaffenberger:mainfrom
kooricold:fix/idle-queued-steer-wake

Conversation

@kooricold

Copy link
Copy Markdown

Problem

A queue-mode steer that arrives while the REPL is parked at the idle prompt is not processed until the user types something at the terminal.

This is most visible with the cp_discord plugin: send a message from Discord while the session sits idle, and it shows as "pending" but never runs until you press a key in the terminal. It also affects /queue entries and steers left over from a cancelled run.

Root cause

PauseController.pop_next_steer_queued() is documented as "Owned by the idle REPL loop: consume queued prompts one at a time as fresh turns when no run is in flight." — but the idle loop only consulted it once, at the top of the persistent-prompt branch, and then blocked on wait_for_idle_submission(). A steer arriving while blocked had nothing to wake it. The classic prompt_toolkit branch never consulted it at all.

cp_discord's inbound router is doing the right thing: when run_depth == 0 (idle) it correctly picks MODE_QUEUE. The gap was entirely on the core side — the idle loop never drained the queue until the next keystroke.

Fix

  • messaging/run_ui.py
    • Add a _WAKE idle-queue sentinel (public alias IDLE_WAKE) and wake_idle_for_queued_steer(), which reuses the existing thread-safe _push_idleloop.call_soon_threadsafe hand-off (no new threading machinery).
    • Register a pause-queue listener _on_steer_queue_change in start_persistent_ui (removed in stop_persistent_ui) that nudges the idle prompt only when a queue-mode steer is actually pending and no run is in flight. now-mode steers (drained by the run's history processor) and drains-to-zero are ignored.
    • wait_for_idle_submission() returns IDLE_WAKE when it dequeues the sentinel so the REPL can re-check the queue.
  • cli_runner.py
    • Hoist the pop_next_steer_queued() drain above the persistent/classic split so both prompt modes drain queued steers, and treat IDLE_WAKE as "loop back and drain".

The instant-wake works in the persistent-prompt mode (the default). Classic prompt_toolkit mode now at least drains queued steers at the top of each loop iteration (previously it never did).

Tests

Adds tests/messaging/test_run_ui_idle_wake.py:

  • queued steer at idle → wakes
  • now-mode-only steer → does not wake
  • run in flight → does not wake (the between-turns loop owns it)
  • listener swallows errors (it runs on the steer-producer thread)
  • wait_for_idle_submission() returns IDLE_WAKE for the sentinel and plain text otherwise

All 6 new tests pass, along with the existing test_run_ui*, test_pause_controller*, and test_steer_queue suites. ruff check and ruff format --check are clean on the changed files.

(Three unrelated pre-existing failures — test_tty_without_flags_selects_new_path and two QueueMenuApp tests — reproduce on a pristine checkout in a headless environment because prompt_toolkit needs a real Windows console; they are not touched by this change.)

A queue-mode steer (e.g. a message routed in by the cp_discord plugin, a
/queue entry, or a cancelled run's leftovers) that arrived while the REPL
was parked at the idle prompt sat unseen until the user typed something at
the terminal.

Root cause: PauseController.pop_next_steer_queued() -- documented as
"owned by the idle REPL loop" -- was only consulted once at the top of the
persistent-prompt branch, after which the loop blocked on
wait_for_idle_submission(). A steer arriving while blocked had nothing to
wake it. The classic prompt_toolkit branch never consulted it at all.

Fix:
- run_ui: add a _WAKE idle-queue sentinel (public alias IDLE_WAKE) and
  wake_idle_for_queued_steer(), which reuses the existing thread-safe
  _push_idle -> loop.call_soon_threadsafe hand-off. A pause-queue listener
  (_on_steer_queue_change) nudges the idle prompt only when a queue-mode
  steer is actually pending AND no run is in flight; it is registered in
  start_persistent_ui and removed in stop_persistent_ui.
- wait_for_idle_submission() returns IDLE_WAKE when it dequeues the wake
  sentinel so the REPL can re-check the queue.
- cli_runner: hoist the pop_next_steer_queued() drain above the
  persistent/classic split so BOTH modes drain queued steers, and treat
  IDLE_WAKE as "loop back and drain".

Adds tests/messaging/test_run_ui_idle_wake.py covering the wake decision
(queued vs now vs run-active), listener error isolation, and the sentinel
round-trip through wait_for_idle_submission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant