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.
This commit is contained in:
Teknium
2026-08-27 07:12:38 -07:00
parent 9faa953cd1
commit 7105657f2c
4 changed files with 24 additions and 8 deletions
-1
View File
@@ -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
+14
View File
@@ -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(
-2
View File
@@ -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
# =============================================================================
+10 -5
View File
@@ -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.