feat(gateway): per-message busy-session controls — inline buttons + multilingual halt heuristic#21080
feat(gateway): per-message busy-session controls — inline buttons + multilingual halt heuristic#21080tymrtn wants to merge 10 commits into
Conversation
…ultilingual halt heuristic Resolves NousResearch#11639. Resolves NousResearch#11118. Adds per-message busy-session controls on top of the existing ``display.busy_input_mode`` infrastructure. Lets users pick **steer / interrupt / stop** on a specific follow-up message via inline keyboard buttons rather than only changing the global mode. Three additive pieces: 1. **Inline keyboard ``[/steer] [/interrupt] [/stop]``** anchored to the running tool-progress bubble (Telegram + Discord + Slack). Reuses the existing busy-ack message as the keyboard anchor when no tool bubble is live yet — no extra messages, no duplication. After a tap, the ack body rewrites to reflect what actually happened (``⏩ Steered…`` / ``⚡ Interrupted…`` / ``🛑 Stopped.``) so chat history stays accurate. A single tap acts on EVERY follow-up that arrived since the agent went busy — texts joined, primitive applied once, reaction emitted on each individual user message. 2. **Multilingual halt-phrase heuristic** — 16-language exact-word matcher (en/es/fr/de/pt/it/nl/ja/ko/zh-Hans/zh-Hant/ru/ar/hi/tr/pl) with conservative function-word exclusions ("para" / "basta" / "잠깐" deliberately not included). Runs as a pre-flight inside ``_handle_active_session_busy_message``; matches like ``stop``, ``alto``, ``止まれ``, ``/``, or empty msg trigger the full ``_interrupt_and_clear_session`` path so the chat unlocks even if the agent is wedged inside a tool. 3. **Reaction lifecycle on user follow-ups** — 👍 (steer), ⚡ (interrupt), 🙊 (halt). Wraps emoji in ``ReactionTypeEmoji`` to dodge python-telegram-bot's variation-selector ``custom_emoji`` serialization bug. **Default change:** ``display.busy_input_mode`` defaults to ``queue`` instead of ``interrupt``. Interrupt-by-default destroys partial work; queue buffers the follow-up for the next turn. The new buttons give users an explicit per-message override when they want something different. New ``display.busy_buttons: true`` toggle (env: ``HERMES_GATEWAY_BUSY_BUTTONS=false``) for per-bot disable. **Why deterministic instead of the LLM-router proposed in NousResearch#11639?** No model latency, no false positives on ambiguous text, no per-message token cost. The buttons are language-neutral and always available; the halt heuristic gives a fast path for natural-language stops without needing ``/stop``. **Authorization:** - Telegram: ``_is_callback_user_authorized`` (PR NousResearch#17775). - Discord: ``_component_check_auth`` (mirrors ``ExecApprovalView`` etc.) PLUS runner-level ``_is_user_authorized`` for deployments using ``GATEWAY_ALLOWED_USERS`` or pairing. - Slack: routes through ``GatewayRunner._is_user_authorized`` for full org-level checks; falls back to ``SLACK_ALLOWED_USERS`` only when no runner is bound, and FAILS CLOSED when neither is configured. **Cross-user ownership gate:** in shared chats with per-user session keys, user A's busy-session buttons are visible to user B. The runner compares the target ``session_key`` against the key the tapping user's source produces (via ``_session_key_for_source``, the resolver that honors ``group_sessions_per_user`` / ``thread_sessions_per_user`` from the session store) and rejects mismatches. Mirrors the implicit isolation ``/stop`` already enjoys. **Telegram callback_data 64-byte cap:** Long group/forum session keys (e.g. ``agent:main:telegram:supergroup:-1001234567890:thread:42:user:9876543210``) overflow Telegram's 64-byte cap. ``build_buttons_with_handles`` switches to a stable short hash (``bs:steer:#<12-char-sha256>``) and surfaces the handle map; the platform registers the mapping at attach time and resolves it on tap. **Tests:** - 21 button-module tests (wire format, primitive set, hash-handle invariants, callback parsing). - 33 stop-phrase matcher tests (per-language matching, false-positive defenses, length cap, function-word exclusions). - 19 runner-integration tests (halt pre-flight, button-tap dispatch, multi-followup concatenation, ack-text rewrite, control-bubble fallback, cross-user rejection, cleanup). - All existing busy-session/auth/steer/queue/reaction upstream tests still pass (271 focused tests green). - ``codex review --base origin/main`` — 7 passes, 25 findings resolved (Slack auth bypass, Discord adapter binding, halt-phrase full-clear, multi-anchor cleanup, callback-data cap, cross-user ownership, Slack 3000-char Block Kit cap, threaded-reply text fetch, forum-topic session-key normalization, …). **Backwards-compatibility:** - ``display.busy_buttons: false`` keeps current behavior (no buttons). - ``display.busy_input_mode: interrupt`` (legacy) still works; the change is to the default only. - All new platform methods on ``BasePlatformAdapter`` have no-op defaults; platforms that don't override (Matrix, Feishu, DingTalk, WhatsApp) are unaffected and inherit silently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5bf1eff to
5eb97f4
Compare
…ons-refresh # Conflicts: # gateway/platforms/discord.py
|
👍 from me -- as I created one just like this for Telegram, but simpler and not as broadly suitable. Would love to see this merged in! |
|
Yeah, it's been a bother to have to maintain this outside of upstream bc it's functionality I heavily depend on |
Preserve busy-session controls and local gateway UX behavior while resolving upstream conflicts.
|
Production-use update: this has become one of the most useful gateway UX features in my Telegram/mobile workflow. The ability to choose steer / interrupt / stop while a session is busy is materially better than the old all-or-nothing behavior. It preserves long-running work by default, while still providing a clear emergency brake when a tool call or model turn is stuck. I’ve been using this regularly with long-running Telegram sessions, and it is solving a real reliability problem for chat-based operation. Another contributor also noted above that this implementation is broader than their Telegram-only version. The PR has drifted and is currently conflicting with @teknium1 flagging for visibility because this is a small-looking UX feature with a large practical impact for mobile/chat users. |
Summary
Adds per-message busy-session controls on top of the existing
display.busy_input_modeinfrastructure. Users can choose steer / interrupt / stop on a specific follow-up message via inline keyboard buttons instead of only changing a global mode.Resolves #11639. Resolves #11118.
What changed
callback_datalimit.stop/alto/止まれstyle messages can unlock a busy chat without slash-command exactness.queueand keeps explicit per-message override controls visible while the turn runs.edit_message_textcalls.Dogfood evidence
This has been running in Tyler's live Telegram gateway. The interrupt buttons are working well in practice, including long tool-running sessions where follow-up messages need to be queued, steered, interrupted, or stopped without losing the current transcript.
Test plan
python -m py_compile gateway/run.py gateway/platforms/telegram.py gateway/platforms/discord.py gateway/platforms/slack.py gateway/busy_session_buttons.py gateway/stop_phrases.pypython -m pytest -o addopts='' tests/gateway/test_busy_session_buttons.py tests/gateway/test_busy_session_runner.py tests/gateway/test_stop_phrase_matcher.py tests/gateway/test_telegram_busy_controls.py tests/gateway/test_telegram_clarify_buttons.py tests/gateway/test_restart_drain.py -q105 passed in 1.26sCompatibility
display.busy_buttons: falsedisables the new inline controls.display.busy_input_modesettings still work.