From 7105657f2cf5cf9a2a246f3a3c46af09e32edfef Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:12:38 -0700 Subject: [PATCH] fix: scope /approve-/deny direct-send confirmations to native-streaming adapters; drop leftover debug log; refresh WeCom docs - The direct adapter.send() confirmation path in /approve and /deny is only needed on native-streaming platforms (WeCom) where the reply stream is already finalized; other platforms keep the return-text contract (fixes 4 approve/deny regression tests, guarded with 'is not True' against MagicMock auto-attributes). - Remove a stray [DEBUG] logger.info left in _deliver_media_from_response. - website/docs wecom.md: replace the 'does not stream' notes with the native msgtype:stream behavior and document the stream keepalive extra keys. --- gateway/run.py | 1 - gateway/slash_commands.py | 14 ++++++++++++++ hermes_cli/config.py | 2 -- website/docs/user-guide/messaging/wecom.md | 15 ++++++++++----- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 07b8f37f3f..e403d73e1f 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -22886,7 +22886,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew asked to deliver (#20834). Only ``MEDIA:`` directives — the explicit attachment contract — trigger post-stream uploads. """ - logger.info("[DEBUG] _deliver_media_from_response called: response=%r chat=%s", response[:100], event.source.chat_id) from pathlib import Path from urllib.parse import quote as _quote diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 99a887f6c7..bb7d451cd4 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -5886,6 +5886,14 @@ class GatewaySlashCommandsMixin: logger.info("User approved %d dangerous command(s) via /approve (%s)", count, choice) plural = "plural" if count > 1 else "singular" confirmation_text = t(f"gateway.approve.{choice}_{plural}", count=count) + # Native-streaming adapters (WeCom msgtype:"stream") need the + # confirmation sent directly with control-lane metadata so it lands + # via a reliable proactive send instead of the (already-finalized) + # reply stream. Every other platform keeps the normal contract: + # else: return the text and let the gateway deliver it. + # (`is not True` — mock adapters auto-create truthy attributes.) + if getattr(_adapter, "SUPPORTS_NATIVE_STREAMING", False) is not True: + return confirmation_text if _adapter: try: await _adapter.send( @@ -5971,6 +5979,12 @@ class GatewaySlashCommandsMixin: else: confirmation_text = t("gateway.deny.denied_singular") + # Same native-streaming carve-out as /approve above: only WeCom-style + # native-stream adapters take the direct control-lane send; everyone + # else returns the text for normal gateway delivery. + # (`is not True` — mock adapters auto-create truthy attributes.) + if getattr(_adapter, "SUPPORTS_NATIVE_STREAMING", False) is not True: + return confirmation_text if _adapter: try: await _adapter.send( diff --git a/hermes_cli/config.py b/hermes_cli/config.py index a28c056d18..ef939e154b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -1014,8 +1014,6 @@ def _ensure_hermes_home_managed(home: Path): from hermes_cli.config_defaults import DEFAULT_CONFIG, OPTIONAL_ENV_VARS # noqa: F401 - - # ============================================================================= # Config Migration System # ============================================================================= diff --git a/website/docs/user-guide/messaging/wecom.md b/website/docs/user-guide/messaging/wecom.md index 2ac5bddb14..3dec3bca94 100644 --- a/website/docs/user-guide/messaging/wecom.md +++ b/website/docs/user-guide/messaging/wecom.md @@ -96,10 +96,12 @@ hermes gateway - **Auto-reconnect** — exponential backoff on connection drops :::note Streaming and typing indicators -The WeCom adapter delivers each response as a single complete message — it does -**not** stream responses token-by-token, and it does **not** show a typing -indicator. "Reply correlation" (below) only threads a response to its inbound -request; it is not live streaming. +The WeCom adapter streams responses natively over WeCom's `msgtype: "stream"` +protocol: the client shows a thinking/typing bubble as soon as a turn starts, +and the reply renders token-by-token in a single bubble as the model +generates it. Tool-call progress is folded into the same bubble. Native +streaming is enabled by default (`display.platforms.wecom.streaming: true` in +`config.yaml`); set it to `false` to restore single-shot delivery. ::: ## Configuration Options @@ -116,6 +118,9 @@ Set these in `config.yaml` under `platforms.wecom.extra`: | `allow_from` | `[]` | User IDs allowed for DMs (when dm_policy=allowlist) | | `group_allow_from` | `[]` | Group IDs allowed (when group_policy=allowlist) | | `groups` | `{}` | Per-group configuration (see below) | +| `stream_keepalive_enabled` | `false` | Send periodic keepalive frames to refresh WeCom's ~6-minute reply-stream window on long turns | +| `stream_keepalive_interval_seconds` | `120` | Keepalive frame cadence when enabled | +| `stream_safe_duration_seconds` | `330` | Stream age after which finalize prefers the reliable proactive send | ## Access Policies @@ -237,7 +242,7 @@ Files exceeding the absolute 20 MB limit are rejected with an informational mess When the bot receives a message via the WeCom callback, the adapter remembers the inbound request ID. If a response is sent while the request context is still active, the adapter uses WeCom's reply-mode (`aibot_respond_msg`) to correlate the response directly to the inbound message. This provides a more natural conversation experience in the WeCom client. -The full response is delivered as a single message — the adapter does not stream tokens incrementally. If the inbound request context has expired or is unavailable, the adapter falls back to proactive message sending via `aibot_send_msg`. +When a native reply stream is active, the response streams incrementally through reply-mode `msgtype: "stream"` frames. If the inbound request context has expired or is unavailable (or a stream frame fails), the adapter falls back to proactive message sending via `aibot_send_msg`. Reply-mode also works for media: uploaded media can be sent as a reply to the originating message.