fix(send_message): add 'platform:current' sentinel to target the active gateway session

Fixes #5472

When a skill running inside a Discord/Telegram/etc. session calls
send_message(target='discord'), the message is delivered to the
platform's home channel — not to the channel/thread where the
conversation is happening. This makes multi-message batch delivery
(e.g. a skill fetching 53 links in 5-link chunks) impossible.

Add a 'current' / '__session__' / 'session' sentinel to the target
resolution in _handle_send(). When specified, the tool reads
HERMES_SESSION_CHAT_ID and HERMES_SESSION_THREAD_ID via
gateway.session_context.get_session_env() (concurrency-safe
ContextVars with env var fallback) and routes the message there.

The resolver also rejects cross-platform sentinels (e.g. 'discord:current'
from a Telegram session) with a clear error instead of silently
falling through to the home channel — the LLM needs deterministic
behavior to rely on this primitive.

Tests
-----
Six new test cases under TestSendMessageCurrentSessionTarget covering:
- Session chat_id resolution with success note
- Thread_id preservation and mirror propagation
- 'current' / 'session' / '__session__' / case-insensitive aliases
- Missing HERMES_SESSION_CHAT_ID returns explicit error, no send attempt
- Platform mismatch returns clear error, no send attempt
- Sentinel path never calls config.get_home_channel (short-circuits the fallback)

All 61 tests in tests/tools/test_send_message_tool.py pass; the broader
80-test send_message suite is also green.

Schema
------
Updated SEND_MESSAGE_SCHEMA.target.description so the LLM discovers
the new 'platform:current' form and knows its constraint (only valid
from a messaging session).
This commit is contained in:
Jing-yilin 2026-04-17 17:43:16 +08:00 committed by Yilin Jing
parent 73bccc94c7
commit c559c66236
2 changed files with 412 additions and 4 deletions

View file

@ -18,6 +18,7 @@ from agent.redact import redact_sensitive_text
logger = logging.getLogger(__name__)
_CURRENT_SESSION_ALIASES = frozenset({"current", "__session__", "session"})
_TELEGRAM_TOPIC_TARGET_RE = re.compile(r"^\s*(-?\d+)(?::(\d+))?\s*$")
_FEISHU_TARGET_RE = re.compile(r"^\s*((?:oc|ou|on|chat|open)_[-A-Za-z0-9]+)(?::([-A-Za-z0-9_]+))?\s*$")
_WEIXIN_TARGET_RE = re.compile(r"^\s*((?:wxid|gh|v\d+|wm|wb)_[A-Za-z0-9_-]+|[A-Za-z0-9._-]+@chatroom|filehelper)\s*$")
@ -113,7 +114,7 @@ SEND_MESSAGE_SCHEMA = {
},
"target": {
"type": "string",
"description": "Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567', 'matrix:!roomid:server.org', 'matrix:@user:server.org'"
"description": "Delivery target. Format: 'platform' (uses home channel), 'platform:current' (current gateway session chat/thread — only valid when invoked from a messaging session; use for multi-message batch delivery into the same conversation), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'discord:current', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567', 'matrix:!roomid:server.org', 'matrix:@user:server.org'"
},
"message": {
"type": "string",
@ -156,11 +157,24 @@ def _handle_send(args):
target_ref = parts[1].strip() if len(parts) > 1 else None
chat_id = None
thread_id = None
is_explicit = False
used_current_session = False
if target_ref:
# Session-relative sentinel: resolve 'current' / '__session__' / 'session'
# to the active gateway session's chat_id/thread_id. This enables skills
# that need multi-message batch delivery into the same conversation.
if target_ref and target_ref.lower() in _CURRENT_SESSION_ALIASES:
session_err, session_chat_id, session_thread_id = _resolve_current_session_target(
platform_name
)
if session_err:
return json.dumps({"error": session_err})
chat_id = session_chat_id
thread_id = session_thread_id
is_explicit = True
used_current_session = True
elif target_ref:
chat_id, thread_id, is_explicit = _parse_target_ref(platform_name, target_ref)
else:
is_explicit = False
# Resolve human-friendly channel names to numeric IDs
if target_ref and not is_explicit:
@ -280,6 +294,11 @@ def _handle_send(args):
)
if used_home_channel and isinstance(result, dict) and result.get("success"):
result["note"] = f"Sent to {platform_name} home channel (chat_id: {chat_id})"
elif used_current_session and isinstance(result, dict) and result.get("success"):
thread_suffix = f", thread_id: {thread_id}" if thread_id else ""
result["note"] = (
f"Sent to current {platform_name} session (chat_id: {chat_id}{thread_suffix})"
)
# Mirror the sent message into the target's gateway session
if isinstance(result, dict) and result.get("success") and mirror_text:
@ -299,6 +318,46 @@ def _handle_send(args):
return json.dumps(_error(f"Send failed: {e}"))
def _resolve_current_session_target(platform_name: str):
"""Resolve a ``platform:current`` target to the active session's chat/thread.
Reads the concurrency-safe session context variables (with legacy env var
fallback) set by ``gateway.session_context.set_session_vars``.
Returns a tuple ``(error_message, chat_id, thread_id)``. On success the
first element is ``None``. Returns an error if there is no active
gateway session or if the session platform does not match
``platform_name`` (e.g. calling ``discord:current`` from a Telegram
session).
"""
from gateway.session_context import get_session_env
session_platform = (get_session_env("HERMES_SESSION_PLATFORM", "") or "").strip().lower()
session_chat_id = (get_session_env("HERMES_SESSION_CHAT_ID", "") or "").strip()
session_thread_id = (get_session_env("HERMES_SESSION_THREAD_ID", "") or "").strip() or None
if not session_chat_id:
return (
f"send_message target '{platform_name}:current' requires an active "
"gateway session but no HERMES_SESSION_CHAT_ID is set. Use an explicit "
f"channel target instead, e.g. '{platform_name}:<chat_id>' or '{platform_name}' "
"for the home channel.",
None,
None,
)
if session_platform and session_platform != platform_name:
return (
f"send_message target '{platform_name}:current' does not match the current "
f"session platform ('{session_platform}'). Use '{session_platform}:current' "
"to reply in the active conversation, or specify an explicit channel target.",
None,
None,
)
return None, session_chat_id, session_thread_id
def _parse_target_ref(platform_name: str, target_ref: str):
"""Parse a tool target into chat_id/thread_id and whether it is explicit."""
if platform_name == "telegram":