Skip to content

fix(gateway): /queue is now a true FIFO — each invocation gets its own turn - #16175

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-548c23dd
Apr 26, 2026
Merged

fix(gateway): /queue is now a true FIFO — each invocation gets its own turn#16175
teknium1 merged 1 commit into
mainfrom
hermes/hermes-548c23dd

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Repeated /queue commands now each produce a full agent turn, in FIFO order, with no merging. Previously the second /queue silently overwrote the first because the handler wrote directly into the adapter's single-slot _pending_messages dict — CLI users got stacking (real queue.Queue) but every gateway platform dropped everything but the last item.

Changes

  • gateway/run.py: GatewayRunner grows a _queued_events: Dict[str, List[MessageEvent]] overflow buffer. /queue uses the adapter's next-up slot when free, otherwise appends to overflow; drain promotes the overflow head into the slot after each consumed turn so the recursion sees it.
  • _handle_reset_command (/new, /reset) clears the overflow for that session. /model and other mid-session operations preserve the queue.
  • _handle_status_command (/status) reports Queued follow-ups: N when non-zero.
  • /queue ack now surfaces depth once it exceeds 1 (Queued for the next turn. (3 queued)).
  • Extracted _enqueue_fifo, _promote_queued_event, _queue_depth helpers. All use getattr(self, '_queued_events', None) fallback so existing tests using object.__new__(GatewayRunner) (pitfall Add support for Atropos Agentic RL environments (requires branch tool… #17) keep working.

Validation

Before After
/queue A; /queue B; /queue C on gateway only C runs A, B, C each get own turn
Text merging across /queue items possible via merge_pending_message_event impossible — separate dict slots
/new after queueing overflow leaked into new conversation cleared
/status output no queue visibility shows depth when non-zero
Targeted tests n/a 50 pass (queue, drain race, hygiene, status, model reset, steer)

Added 5 new tests to tests/gateway/test_queue_consumption.py covering FIFO enqueue, drain-site promotion across 3 consecutive turns, interrupt-follow-up staging, depth counting, and no-merge invariant. Removed the stale test_multiple_queues_last_one_wins test which documented the old broken behavior.

CLI behavior is unchanged — self._pending_input = queue.Queue() already did this correctly.

…n turn

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
@teknium1
teknium1 merged commit 1dfcc2f into main Apr 26, 2026
11 of 12 checks passed
@teknium1
teknium1 deleted the hermes/hermes-548c23dd branch April 26, 2026 18:55
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery labels Apr 26, 2026
@ether-btc

Copy link
Copy Markdown
Contributor

Excellence Contribution — /queue True FIFO

What it does
Each /queue invocation now gets its own distinct agent turn, processed in strict FIFO order. Previously, the gateway wrote queued events directly into the adapter's single-slot _pending_messages dict — subsequent /queue commands overwrote prior ones. The CLI already used queue.Queue() and handled this correctly; all gateway platforms did not.

Why it matters
Queue starvation was a silent correctness bug. /queue A; /queue B; /queue C on any gateway platform (Discord, Slack, Telegram, etc.) would execute only C. A and B were silently discarded with no error, no warning. Any workflow using /queue to stage sequential operations — batch document processing, multi-step analysis — had operations silently disappear.

Technical approach
GatewayRunner gains a _queued_events: Dict[str, List[MessageEvent]] overflow buffer. When /queue fires and the adapter slot is occupied, the event appends to overflow. On each drain completion, the overflow head is promoted into the slot in order. /new clears the overflow; /status shows Queued follow-ups: N when N > 0.

Behavior Before After
/queue A; /queue B; /queue C on gateway Only C runs A, B, C each get their own turn
/new after queueing Overflow leaked into new conversation Overflow cleared
/status No queue visibility Shows Queued follow-ups: N
CLI behavior Correct Unchanged

Implications

  • Sequential batch workflows on all gateway platforms now work correctly
  • /queue graduates from "unreliable on gateway" to a first-class workflow primitive
  • The Queued follow-ups visibility in /status makes queue state transparent — eliminating the silent-drop failure mode

donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
…n turn (NousResearch#16175)

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…n turn (NousResearch#16175)

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…n turn (NousResearch#16175)

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…n turn (NousResearch#16175)

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…n turn (NousResearch#16175)

Repeated /queue commands now each produce a full agent turn, in order,
with no merging.  Previously the second /queue overwrote the first
because the handler wrote directly into the adapter's single-slot
_pending_messages dict.

- GatewayRunner grows a _queued_events overflow buffer (dict of list).
- /queue puts new items in the adapter's next-up slot when free,
  otherwise appends to the overflow.  After each run's drain consumes
  the slot, the next overflow item is promoted so the recursive run
  picks it up.
- /new and /reset clear the overflow.
- /status now reports queue depth when non-zero.
- Ack message shows the depth once it exceeds 1.

Helpers (_enqueue_fifo, _promote_queued_event, _queue_depth) use the
getattr default-fallback pattern so existing tests that build bare
GatewayRunner instances via object.__new__ keep working.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants