Skip to content

fix(gateway): allow settings slash commands during active sessions - #12387

Closed
cometjc wants to merge 14 commits into
NousResearch:mainfrom
cometjc:fix/slash-commands-in-active-session
Closed

fix(gateway): allow settings slash commands during active sessions#12387
cometjc wants to merge 14 commits into
NousResearch:mainfrom
cometjc:fix/slash-commands-in-active-session

Conversation

@cometjc

@cometjc cometjc commented Apr 19, 2026

Copy link
Copy Markdown

Summary

Slash commands like /model, /verbose, /fast, /reasoning, /yolo, /personality, /provider, /voice are silently eaten when sent on Telegram while an agent is actively running. Instead of invoking the command handler, the user gets the ⚡ Interrupting current task... ack and the command text is fed into the running agent as interrupt input.

Root Cause

gateway/platforms/base.py:1614-1634 checks should_bypass_active_session(cmd) before any per-command logic in gateway/run.py runs. Commands not in the bypass allowlist fall through to _busy_session_handler (gateway/run.py:1391), which calls running_agent.interrupt(event.text) and replies with the interrupt ack. The command handler is never reached.

The bypass allowlist ACTIVE_SESSION_BYPASS_COMMANDS in hermes_cli/commands.py previously only covered control/info commands (agents, approve, background, commands, deny, help, new, profile, queue, restart, status, stop, update). Settings commands were missing, so /model (and the other settings toggles) fell through to the busy handler.

Side effect: the /model guard at gateway/run.py:3022-3024 (return "Agent is running — wait or /stop first, then switch models.") became dead code, since the adapter short-circuits before _handle_message is ever called.

Fix

  1. Extend ACTIVE_SESSION_BYPASS_COMMANDS with 8 settings commands: model, provider, verbose, fast, reasoning, yolo, personality, voice.
  2. Replace the dead /model string-return in gateway/run.py:3022-3028 with an actual dispatch to _handle_model_command, kept as a defensive fallback for adapters that don't call the bypass check or if the allowlist is later narrowed.

Handler safety audit

Each of the 8 added handlers was audited — none call running_agent.interrupt/cancel or mutate _running_agents. They only read/write config:

Command Handler What it touches
/model _handle_model_command _session_model_overrides, _agent_cache eviction, cached entry switch_model() (public API)
/provider _handle_provider_command read-only
/verbose _handle_verbose_command display.platforms.<p>.tool_progress YAML
/fast _handle_fast_command _service_tier + YAML
/reasoning _handle_reasoning_command _reasoning_config, _show_reasoning + YAML
/yolo _handle_yolo_command session-yolo helpers in tools/approval
/personality _handle_personality_command _ephemeral_system_prompt + YAML
/voice _handle_voice_command _voice_mode, adapter voice state

All 8 effects apply to the next turn (or are read-only), so dispatching them mid-run is safe.

Tests

New: tests/gateway/test_active_session_bypass_commands.py (TDD — failed before fix, 23 green after):

  • 8 parametrized tests for the added commands
  • 13 parametrized regression guards for pre-existing bypass commands
  • 2 sanity tests: known-but-excluded command (/skills) and unknown command both return False

Verified green locally:

tests/gateway/test_active_session_bypass_commands.py .... 23 passed
tests/hermes_cli/test_commands.py ........................ passed
tests/gateway/test_busy_session_ack.py ................... passed
tests/gateway/test_verbose_command.py .................... passed
145 passed in 2.12s

Test Plan

  • Unit tests passing locally.
  • Manual: send /model on a Telegram thread with an agent mid-run — should now open the model picker, not produce the interrupt ack.
  • Manual: confirm /stop and /new still interrupt as expected (regression check).

Scope

Touches 3 files (hermes_cli/commands.py, gateway/run.py, new test). No behavioral changes outside the allowlist extension.

teknium1 and others added 14 commits April 18, 2026 16:19
Introduces a dedicated `telegram_topic` tool for forum topic lifecycle ops
(create/close/reopen/delete/rename/list) alongside the existing
`send_message` tool. Targets use the same `telegram:<chat_id>[:<thread_id>]`
format; `delete` is gated behind `confirm=true`; Telegram API errors are
mapped to stable codes (`no_rights`, `topic_not_found`, `chat_not_found`,
`topic_closed`, `unknown`). `action='list'` enumerates topics observed via
incoming messages (source='observed_sessions'), since the Bot API does not
expose a topic enumeration endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Short orientation file for Claude Code instances: points at AGENTS.md as the
canonical dev guide, then extracts the must-know-before-editing surface --
`scripts/run_tests.sh` as the only correct test entrypoint, the
auto-discovery registration pattern in `tools/registry.py`, the single
`COMMAND_REGISTRY` as source of truth for slash commands, profile-safe path
rules (`get_hermes_home()` / `display_hermes_home()`), the Ink+Python TUI
split, and the prompt-caching invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without this entry, the gateway's platform→toolset reverse-map in
_get_platform_tools (hermes_cli/tools_config.py) only surfaced tools
whose names are subset members of a CONFIGURABLE_TOOLSETS key. Since
telegram_topic was added to _HERMES_CORE_TOOLS but not to the
"messaging" toolset definition, it was silently dropped before
reaching get_tool_definitions — the bot registered the handler but
never saw the schema, so the model couldn't call it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md was a condensed pointer-summary of AGENTS.md; every invariant
and architecture note it carried was already spelled out in AGENTS.md at
greater depth. Replace it with a symlink so Claude Code and other agents
that probe CLAUDE.md resolve to the same canonical guide — keeping one
source of truth and removing the risk of the summary drifting out of
sync with the authoritative doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adding a tool to _HERMES_CORE_TOOLS alone silently drops it from
messaging-platform bots when the user has no platform_toolsets override,
because _get_platform_tools reverse-maps to CONFIGURABLE_TOOLSETS keys —
anything not listed under one of those keys never reaches the model.
Document the two-place registration requirement, the _check_send_message
gating pattern for bot-only tools, and reference the recent telegram_topic
regression so this trap stops catching people.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The watch_file list referenced nix/package.nix, which does not exist in
the tree — the real file is nix/packages.nix. `direnv status` showed it
as a 1970-01-01 epoch entry, meaning direnv was tracking a non-existent
path and changes to the actual packages.nix never triggered a reload.
Correct the name so edits to the package derivation invalidate the dev
shell as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user opens a new forum topic by forwarding a message into a group,
the kickoff message arrives with forum_topic_created (and/or
forward_origin/forward_from/forward_from_chat). Under require_mention=true
none of these match the existing trigger gates, so the bot would stay
silent until a second message landed in the topic.

Treat both signals as valid triggers in _should_process_message so
forwarded topic kickoffs get picked up immediately. ignored_threads still
wins, so muted threads are unaffected.

Tests:
- test_forum_topic_created_message_bypasses_mention_requirement
- test_forwarded_message_bypasses_mention_requirement (forward_origin /
  forward_from / forward_from_chat)
- test_ignored_threads_still_drop_forum_topic_created
- Design doc: architecture, data flow, failure modes, rollout.
- Implementation plan: 10 phases, bite-sized TDD tasks.
- Phase 0 pre-flight checks done (real state.db, 12/12 recall).
- Zero-change to Hermes core; standalone mempalace-sync daemon repo.
…c w/ 24h TTL

When a parent session on Telegram dispatches delegate_task, the gateway now
lazily creates a dedicated forum topic named from the task goal (e.g. "SA
讀 README…") and forwards every subagent.* event into that topic instead
of interleaving it with the main conversation.  Benefits:

  * Parent thread stays clean — only a one-shot pointer message (t.me deep
    link to the new topic) on lazy create.
  * Each parent session maps to exactly one topic; user replies inside the
    new topic naturally become a fresh top-level agent session (depth=0)
    because gateway's session key already includes thread_id.
  * State persisted at ~/.hermes/state/subagent_topics.json (atomic writes,
    chmod 0o600).
  * A cron reaper (`cron/subagent_topic_reaper.py`, scheduled every 15m
    via mcp_cronjob) deletes topics whose last_message_ts is older than
    24h, keeping the forum tidy.

Highlights:
  * gateway/subagent_topic_router.py — per-session lazy create, linear-
    history forwarding, soft-fail everywhere, runtime blocklist for
    non-forum chats, per-session backpressure.
  * gateway/run.py — progress_callback now routes subagent.* events via
    the router and short-circuits before the existing tool.started path,
    so subagent chatter never leaks into the parent's progress message.
  * cron/subagent_topic_reaper.py — standalone sweep (`python -m` + CLI),
    dry-run mode, re-load-before-prune to avoid clobbering concurrent
    router writes, classified Telegram errors → skipped_gone vs failed.
  * 103 new unit tests (router 74 + reaper 29) — all pass in <1s.

Design doc: docs/plans/2026-04-19-subagent-progress-ttl-topics.md
…space)

The /plan skill writes timestamped markdown into the workspace-local
.hermes/plans/ directory. Published plans are copied to docs/plans/ with
the agreed repo naming convention; the .hermes/ root is pure scratch and
should not pollute git status.
The gateway progress callback truncates terminal previews to 40 chars via
tool_preview_length, so `cd /home/long/path && real_cmd` used to render as
"cd /home/long/path..." in Telegram/Slack progress messages — completely
hiding what the agent actually ran.

build_tool_preview() now strips leading `cd <path> && ` prefixes (including
chained `cd a && cd b && cmd`) before truncation, so the preview shows the
real command.  Quoted paths with spaces work; malformed inputs are left
alone.

Preferred solution is still the tool's `workdir` parameter (no cd at all),
but this fallback catches the common agent habit when we forget.

6 new unit tests in tests/agent/test_display.py cover the happy paths,
chained cds, quoted paths, truncation interaction, and the malformed-input
safe fallback.
Root cause: the adapter-layer dispatch in gateway/platforms/base.py:1617
checks should_bypass_active_session(cmd) BEFORE any per-command logic in
gateway/run.py runs. Commands not in the allowlist fall through to
_busy_session_handler, which feeds the command text to the running agent
as interrupt input and replies with '⚡ Interrupting current task...' —
the command handler is never called.

Symptom: on Telegram, sending /model (or /verbose, /fast, /reasoning,
/yolo, /personality, /provider, /voice) while the agent is running
produces the interrupt ack instead of executing the command.

Fix: extend ACTIVE_SESSION_BYPASS_COMMANDS with the settings/info
commands. Each handler was audited and confirmed NOT to touch
running_agent.* or mutate _running_agents — they only read/write config.

Commands added:
  - model, provider  (model selection / provider listing)
  - verbose, fast, reasoning, yolo  (runtime config toggles)
  - personality, voice  (persona / TTS toggles)

Also replaced the dead-code 'Agent is running — wait or /stop first'
guard for /model in gateway/run.py:3022 with an actual dispatch to the
model handler, kept as a defensive fallback in case the allowlist is
later narrowed.

TDD: tests/gateway/test_active_session_bypass_commands.py added first
(failed before the fix, passes after).

Tests run:
  tests/gateway/test_active_session_bypass_commands.py  -> 23 passed
  tests/hermes_cli/test_commands.py                     -> passed
  tests/gateway/test_busy_session_ack.py                -> passed
  tests/gateway/test_verbose_command.py                 -> passed
@cometjc

cometjc commented Apr 19, 2026

Copy link
Copy Markdown
Author

Closing as duplicate — #12334 already landed the more comprehensive fix on main yesterday (commit 632a807). That PR takes the broader approach of treating any resolvable slash command as a bypass, covering /model and also /reasoning, /voice, /insights, /title, /resume, /retry, /undo, /compress, /usage, /provider, /reload-mcp, /sethome, /reset. My allowlist-extension approach here is strictly narrower and now redundant.

Thanks for the clean upstream fix.

@cometjc cometjc closed this Apr 19, 2026
@cometjc
cometjc deleted the fix/slash-commands-in-active-session branch April 19, 2026 03:34
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.

3 participants