Skip to content

fix(gateway): keep /model and /queue non-interrupting and execute queued /model post-turn#6252

Closed
HearthCore wants to merge 4 commits into
NousResearch:mainfrom
HearthCore:fix/gateway-slash-command-no-interrupt
Closed

fix(gateway): keep /model and /queue non-interrupting and execute queued /model post-turn#6252
HearthCore wants to merge 4 commits into
NousResearch:mainfrom
HearthCore:fix/gateway-slash-command-no-interrupt

Conversation

@HearthCore

@HearthCore HearthCore commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes gateway behavior for slash commands sent during an active session:

  • /model and /queue no longer trigger active-turn interrupts.
  • queued /model now executes after the current turn (via normal command pipeline) instead of being dropped.
  • fixes a related NameError in the first-response-before-queued-message send path.

Related Issue

Fixes #5057

Additional investigation/comment:
#5057 (comment)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • gateway/platforms/base.py
    • extended active-session bypass allowlist with model, queue, q
  • gateway/run.py
    • queue /model during active run (instead of hard reject)
    • in _dequeue_pending_text(), keep queued slash commands as MessageEvents by re-queueing recognized commands and returning None
    • fixed undefined event reference in post-turn send path
  • tests/gateway/test_command_bypass_active_session.py
    • added regression tests for /model and /queue bypass
    • added test ensuring queued slash command events are preserved for post-turn dispatch

How to Test

  1. Start gateway and trigger a long-running turn in Telegram.
  2. While running, send /model and /queue <prompt>.
  3. Verify no interrupt occurs.
  4. Verify queued /model executes after turn completion.
  5. Run tests:
    • ./venv/bin/pytest tests/gateway/test_command_bypass_active_session.py tests/gateway/test_queue_consumption.py -q

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • My PR contains only changes related to this fix
  • I've tested manually on my platform: Debian 13 / Telegram gateway
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes

Documentation & Housekeeping

  • N/A (no config keys, no docs/API schema changes)

Screenshots / Logs

Manual live test result in Telegram: /model and /queue no longer interrupt active turn; queued /model executes post-turn.

/model and /queue were not listed in the active-session guard bypass
in base.py, causing them to interrupt the running agent just like any
plain text message would.

The root cause: base.py line ~1168 has an explicit allowlist of commands
that skip the interrupt path. /model and /queue were missing from it,
so they fell through to the generic interrupt handler (pending_messages
+ active_sessions.set()), killing the running agent before run.py ever
got to handle them.

Fix: add 'model', 'queue', and 'q' to the bypass list so they are
dispatched inline (to run.py handlers) without triggering an interrupt.

- /model: returns a post-turn info message without killing the agent
- /queue: stores the prompt in pending_messages for next-turn pickup

Relates to: NousResearch#5057
Instead of rejecting /model while an agent is running, store the event in
adapter._pending_messages and return an acknowledgement.

After the current turn finishes, base adapter logic already dequeues pending
events and runs them through the normal command pipeline, so /model opens its
menu naturally without interrupting active work.

This matches /queue semantics and improves UX for model switching in gateway
chats.
…spatch

Root cause: queued slash commands were dropped by _run_agent recursion flow.
Pending events were converted to plain text via _dequeue_pending_text(), then
command-looking text was discarded by a safety filter to avoid leaking commands
into the agent as user input. This prevented queued /model from ever reaching
_handle_model_command after turn completion.

Fixes:
- In _dequeue_pending_text(), detect recognized slash-command events,
  re-queue the full MessageEvent back into adapter._pending_messages,
  and return None so command events stay in message-pipeline path.
- Keep /model and /queue bypass behavior in base.py so they don't interrupt.
- Fix NameError in first-response send path (use _progress_metadata instead of
  undefined event variable) that appeared after restart tests.

Result:
- /model during active turn is acknowledged, does not interrupt,
  and executes after current turn as intended.
- /queue remains non-interrupting and post-turn.
@HearthCore

Copy link
Copy Markdown
Contributor Author

Ran full suite per contributing checklist:

  • Command: ./venv/bin/pytest tests/ -q
  • Result: 109 failed, 9188 passed, 30 skipped, 1 xpassed (243s)

Notably, failures span many unrelated areas (CLI quick commands, API server, provider resolution, docker env, managed media gateways, terminal requirements), while this PR only changes:

  • gateway/platforms/base.py
  • gateway/run.py
  • tests/gateway/test_command_bypass_active_session.py

PR-specific regression tests are green:

  • ./venv/bin/pytest tests/gateway/test_command_bypass_active_session.py tests/gateway/test_queue_consumption.py -q
  • 26 passed

Also manually validated in Telegram DM after gateway restart:

  • /queue and /model no longer interrupt active turn
  • queued /model executes post-turn

teknium1 added a commit that referenced this pull request Apr 19, 2026
Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes #5057. Related: #6252, #10370, #4665.
teknium1 added a commit that referenced this pull request Apr 19, 2026
Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes #5057. Related: #6252, #10370, #4665.
elkimek added a commit to elkimek/hermes-agent that referenced this pull request Apr 19, 2026
Main upstream added a broader catch-all in _process_text_command() that
rejects any recognized slash command reaching the running-agent guard
with a "busy — wait or /stop first" message (fixes NousResearch#5057, NousResearch#6252, NousResearch#10370).

Keep the PR's specific handlers for /yolo, /verbose, /fast, /reasoning
BEFORE the catch-all — these four are state toggles that should actually
run (not be rejected) mid-agent. Resolution merges both blocks:

1. Toggle handlers (PR) — run yolo/verbose/fast/reasoning immediately
2. Dedicated info handlers (main) — help/commands/profile/update
3. Catch-all (main) — graceful "busy" reject for anything else

Dropped /reasoning from the catch-all's example list since it now has
an explicit handler above.

Verified: 151 related tests pass.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the thorough investigation and fix, @HearthCore! This was a real bug and your diagnosis in #5057 was accurate.

This automated hermes-sweeper review found that the core issue has since been resolved on main by a broader fix:

The narrower allowlist approach in this PR is superseded by that broader fix. Closing as implemented on main.

@teknium1 teknium1 closed this Apr 27, 2026
@alt-glitch alt-glitch added comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/bug Something isn't working labels Apr 30, 2026
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
…earch#12334)

Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes NousResearch#5057. Related: NousResearch#6252, NousResearch#10370, NousResearch#4665.
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
…earch#12334)

Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes NousResearch#5057. Related: NousResearch#6252, NousResearch#10370, NousResearch#4665.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…earch#12334)

Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes NousResearch#5057. Related: NousResearch#6252, NousResearch#10370, NousResearch#4665.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…earch#12334)

Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes NousResearch#5057. Related: NousResearch#6252, NousResearch#10370, NousResearch#4665.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…earch#12334)

Any recognized slash command now bypasses the Level-1 active-session
guard instead of queueing + interrupting. A mid-run /model (or
/reasoning, /voice, /insights, /title, /resume, /retry, /undo,
/compress, /usage, /provider, /reload-mcp, /sethome, /reset) used to
interrupt the agent AND get silently discarded by the slash-command
safety net — zero-char response, dropped tool calls.

Root cause:
- Discord registers 41 native slash commands via tree.command().
- Only 14 were in ACTIVE_SESSION_BYPASS_COMMANDS.
- The other ~15 user-facing ones fell through base.py:handle_message
  to the busy-session handler, which calls running_agent.interrupt()
  AND queues the text.
- After the aborted run, gateway/run.py:9912 correctly identifies the
  queued text as a slash command and discards it — but the damage
  (interrupt + zero-char response) already happened.

Fix:
- should_bypass_active_session() now returns True for any resolvable
  slash command. ACTIVE_SESSION_BYPASS_COMMANDS stays as the subset
  with dedicated Level-2 handlers (documentation + tests).
- gateway/run.py adds a catch-all after the dedicated handlers that
  returns a user-visible "agent busy — wait or /stop first" response
  for any other resolvable command.
- Unknown text / file-path-like messages are unchanged — they still
  queue.

Also:
- gateway/platforms/discord.py logs the invoker identity on every
  slash command (user id + name + channel + guild) so future
  ghost-command reports can be triaged without guessing.

Tests:
- 15 new parametrized cases in test_command_bypass_active_session.py
  cover every previously-broken Discord slash command.
- Existing tests for /stop, /new, /approve, /deny, /help, /status,
  /agents, /background, /steer, /update, /queue still pass.
- test_steer.py's ACTIVE_SESSION_BYPASS_COMMANDS check still passes.

Fixes NousResearch#5057. Related: NousResearch#6252, NousResearch#10370, NousResearch#4665.
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 P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Message handling behavior when multiple messages arrive during active agent execution

3 participants