# NousResearch/hermes-agent · send_message email target recognition, plus unrelated Windows update fix

## Open now

- Nothing open on GitHub.

Three of four members concern send_message not recognizing email addresses as explicit targets (one bug report and two fix PRs). The fourth, about excluding launcher-shim ancestors in a Windows update concurrency check, appears unrelated to that theme.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 2 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #35258. Verify it addresses your case.
- **Recommended:** read PR #35258 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-35257`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #34740: \[Bug\]: send_message does not recognize explicit email targets and incorrectly falls back to home target resolution

## Prior & competing PRs

- **#35257: fix(update/windows): robustly exclude launcher-shim ancestors from concurrent check**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/main.py`, `tests/hermes_cli/test_update_concurrent_quarantine.py`
- **#35258: fix(tools): recognize email addresses as explicit send_message targets**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `tests/tools/test_send_message_tool.py`, `tools/send_message_tool.py`
  - fix hunks:
  - `tests/tools/test_send_message_tool.py`

    ```diff
    @@ -1220,6 +1220,90 @@ def test_slack_id_not_explicit_for_other_platforms(self):
             assert _parse_target_ref("telegram", "C0B0QV5434G")[2] is False
     
     
    +class TestParseTargetRefEmail:
    +    """_parse_target_ref recognizes email addresses as explicit for the email platform."""
    +
    +    def test_standard_email_is_explicit(self):
    +        chat_id, thread_id, is_explicit = _parse_target_ref("email", "[redacted:email]")
    +        assert chat_id == "[redacted:email]"
    +        assert thread_id is None
    +        assert is_explicit is True
    +
    +    def test_email_with_dots_in_local_part(self):
    +        chat_id, _, is_explicit = _parse_target_ref("email", "[redacted:email]")
    +        assert chat_id == "[redacted:email]"
    +        assert is_explicit is True
    +
    +    def test_email_with_plus_tag(self):
    +        chat_id, _, is_explicit = _parse_target_ref("email", "[redacted:email]")
    +        assert chat_id == "[redacted:email]"
    +        assert is_explicit is True
    +
    +    def test_email_strips_whitespace(self):
    +        chat_id, _, is_explicit = _parse_target_ref("email", "  [redacted:email]  ")
    +        assert chat_id == "[redacted:email]"
    +        assert is_explicit is True
    +
    +    def test_invalid_email_not_explicit(self):
    +        assert _parse_target_ref("email", "not-an-email")[2] is False
    +        assert _parse_target_ref("email", "@example.com")[2] is False
    +        assert _parse_target_ref("email", "user@")[2] is False
    +        assert _parse_target_ref("email", "user@.com")[2] is False
    +
    +    def test_email_not_explicit_for_other_platforms(self):
    +        assert _parse_target_ref("telegram", "[redacted:email]")[2] is False
    +        assert _parse_target_ref("discord", "[redacted:email]")[2] is False
    +        assert _parse_target_ref("slack", "[redacted:email]")[2] is False
    +
    +
    +class TestEmailHomeChannelErrorHint:
    +    """The no-home-channel error for email points at the real env var.
    +
    +    Email reads its home channel from EMAIL_HOME_ADDRESS (gateway/config.py),
    +    not the generic EMAIL_HOME_CHANNEL. The error guidance must name the
    +    variable that is actually consulted so users who follow it succeed.
    +    """
    +
    +    def test_email_error_names_email_home_address(self):
    +        email_cfg = SimpleNamespace(enabled=True, token="", extra={})
    +        config = SimpleNamespace(
    +            platforms={Platform.EMAIL: email_cfg},
    +            get_home_channel=lambda _platform: None,
    +        )
    +        with patch("gateway.config.load_gateway_config", return_value=config), \
    +             patch("tools.interrupt.is_interrupted", return_value=False):
    +            result = json.loads(
    +                send_message_tool(
    +                    {
    +                        "action": "send",
    +                        "target": "email",
    +                        "message": "hi",
    +                    }
    +                )
    +            )
    +        assert "EMAIL_HOME_ADDRESS" in result["error"]
    +        assert "EMAIL_HOME_CHANNEL" not in result["error"]
    +
    +    def test_non_email_platform_keeps_generic_home_channel_hint(self):
    +        telegram_cfg = SimpleNamespace(enabled=True, token="***", extra={})
    +        config = SimpleNamespace(
    +            platforms={Platform.TELEGRAM: telegram_cfg},
    +            get_home_channel=lambda _platform: None,
    +        )
    +        with patch("gateway.config.load_gateway_config", return_value=config), \
    +             patch("tools.interrupt.is_interrupted", return_value=False):
    +            result = json.loads(
    +                send_message_tool(
    +                    {
    +                        "action": "send",
    +                        "target": "telegram",
    +                        "message": "hi",
    +                    }
    +                )
    +            )
    +        assert "TELEGRAM_HOME_CHANNEL" in result["error"]
    +
    +
     class TestSendDiscordThreadId:
         """_send_discord uses thread_id when provided."""
     
    ```
  - `tools/send_message_tool.py`

    ```diff
    @@ -40,6 +40,15 @@
     # downstream adapters (signal, etc.) expect.
     _PHONE_PLATFORMS = frozenset({"signal", "sms", "whatsapp"})
     _E164_TARGET_RE = re.compile(r"^\s*\+(\d{7,15})\s*$")
    +# Email addresses — a valid email like "[redacted:email]" should be treated as
    +# an explicit target for the email platform, not fall through to channel-name
    +# resolution which has no way to resolve a raw address.
    +_EMAIL_TARGET_RE = re.compile(r"^\s*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\s*$")
    +# Most platforms read their home channel from "<PLATFORM>_HOME_CHANNEL", but a
    +# few diverge. Email reads EMAIL_HOME_ADDRESS (see gateway/config.py), so the
    +# generic "<PLATFORM>_HOME_CHANNEL" hint would point users at a variable that is
    +# never read. Map the exceptions so the error guidance is actually actionable.
    +_HOME_CHANNEL_ENV_OVERRIDES = {"email": "EMAIL_HOME_ADDRESS"}
     _IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
     _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".3gp"}
     _AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".m4a", ".flac"}
    ```

    ```diff
    @@ -265,10 +274,13 @@ def _handle_send(args):
                 chat_id = home.chat_id
                 used_home_channel = True
             else:
    +            home_env = _HOME_CHANNEL_ENV_OVERRIDES.get(
    +                platform_name, f"{platform_name.upper()}_HOME_CHANNEL"
    +            )
                 return json.dumps({
                     "error": f"No home channel set for {platform_name} to determine where to send the message. "
                     f"Either specify a channel directly with '{platform_name}:CHANNEL_NAME', "
    -                f"or set a home channel via: hermes config set {platform_name.upper()}_HOME_CHANNEL <channel_id>"
    +                f"or set a home channel via: hermes config set {home_env} <channel_id>"
                 })
     
         duplicate_skip = _maybe_skip_cron_duplicate_send(platform_name, chat_id, thread_id)
    ```

    ```diff
    @@ -383,6 +395,10 @@ def _parse_target_ref(platform_name: str, target_ref: str):
             if target_ref.strip().isdigit():
                 return f"group:{target_ref.strip()}", None, True
             return None, None, False
    +    if platform_name == "email":
    +        match = _EMAIL_TARGET_RE.fullmatch(target_ref)
    +        if match:
    +            return target_ref.strip(), None, True
         if platform_name in _PHONE_PLATFORMS:
             match = _E164_TARGET_RE.fullmatch(target_ref)
             if match:
    ```
- **#34748: fix(tools): recognize email addresses as explicit targets in send_message**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `d3724c0be`, `#35258`

## Fix status

- Merged to main in PR #35258 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-30`.
