{"repo":"NousResearch/hermes-agent","public_id":"34740","display_label":"send_message email target recognition, plus unrelated Windows update fix","summary":"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.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35258,"merged_at":"2026-05-30T09:39:09+00:00"},"resolution":{"state":"resolved","by":"pr-35257"},"freshness":{"state":"verified","last_verified_at":"2026-05-30T09:39:09+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":29341,"pr_number":35257,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-30T09:38:40+00:00"},{"issue_number":34740,"pr_number":35258,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-30T09:39:09+00:00"}],"members":[{"kind":"issue","number":34740,"title":"[Bug]: send_message does not recognize explicit email targets and incorrectly falls back to home target resolution","url":null,"state":"closed"},{"kind":"pr","number":34748,"title":"fix(tools): recognize email addresses as explicit targets in send_message","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d3724c0be","number":0},{"kind":"pr","value":"#35258","number":35258}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":35257,"title":"fix(update/windows): robustly exclude launcher-shim ancestors from concurrent check","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","tests/hermes_cli/test_update_concurrent_quarantine.py"]},{"kind":"pr","number":35258,"title":"fix(tools): recognize email addresses as explicit send_message targets","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_send_message_tool.py","tools/send_message_tool.py"],"fix_hunks":[{"file":"tests/tools/test_send_message_tool.py","hunks":["@@ -1220,6 +1220,90 @@ def test_slack_id_not_explicit_for_other_platforms(self):\n         assert _parse_target_ref(\"telegram\", \"C0B0QV5434G\")[2] is False\n \n \n+class TestParseTargetRefEmail:\n+    \"\"\"_parse_target_ref recognizes email addresses as explicit for the email platform.\"\"\"\n+\n+    def test_standard_email_is_explicit(self):\n+        chat_id, thread_id, is_explicit = _parse_target_ref(\"email\", \"[redacted:email]\")\n+        assert chat_id == \"[redacted:email]\"\n+        assert thread_id is None\n+        assert is_explicit is True\n+\n+    def test_email_with_dots_in_local_part(self):\n+        chat_id, _, is_explicit = _parse_target_ref(\"email\", \"[redacted:email]\")\n+        assert chat_id == \"[redacted:email]\"\n+        assert is_explicit is True\n+\n+    def test_email_with_plus_tag(self):\n+        chat_id, _, is_explicit = _parse_target_ref(\"email\", \"[redacted:email]\")\n+        assert chat_id == \"[redacted:email]\"\n+        assert is_explicit is True\n+\n+    def test_email_strips_whitespace(self):\n+        chat_id, _, is_explicit = _parse_target_ref(\"email\", \"  [redacted:email]  \")\n+        assert chat_id == \"[redacted:email]\"\n+        assert is_explicit is True\n+\n+    def test_invalid_email_not_explicit(self):\n+        assert _parse_target_ref(\"email\", \"not-an-email\")[2] is False\n+        assert _parse_target_ref(\"email\", \"@example.com\")[2] is False\n+        assert _parse_target_ref(\"email\", \"user@\")[2] is False\n+        assert _parse_target_ref(\"email\", \"user@.com\")[2] is False\n+\n+    def test_email_not_explicit_for_other_platforms(self):\n+        assert _parse_target_ref(\"telegram\", \"[redacted:email]\")[2] is False\n+        assert _parse_target_ref(\"discord\", \"[redacted:email]\")[2] is False\n+        assert _parse_target_ref(\"slack\", \"[redacted:email]\")[2] is False\n+\n+\n+class TestEmailHomeChannelErrorHint:\n+    \"\"\"The no-home-channel error for email points at the real env var.\n+\n+    Email reads its home channel from EMAIL_HOME_ADDRESS (gateway/config.py),\n+    not the generic EMAIL_HOME_CHANNEL. The error guidance must name the\n+    variable that is actually consulted so users who follow it succeed.\n+    \"\"\"\n+\n+    def test_email_error_names_email_home_address(self):\n+        email_cfg = SimpleNamespace(enabled=True, token=\"\", extra={})\n+        config = SimpleNamespace(\n+            platforms={Platform.EMAIL: email_cfg},\n+            get_home_channel=lambda _platform: None,\n+        )\n+        with patch(\"gateway.config.load_gateway_config\", return_value=config), \\\n+             patch(\"tools.interrupt.is_interrupted\", return_value=False):\n+            result = json.loads(\n+                send_message_tool(\n+                    {\n+                        \"action\": \"send\",\n+                        \"target\": \"email\",\n+                        \"message\": \"hi\",\n+                    }\n+                )\n+            )\n+        assert \"EMAIL_HOME_ADDRESS\" in result[\"error\"]\n+        assert \"EMAIL_HOME_CHANNEL\" not in result[\"error\"]\n+\n+    def test_non_email_platform_keeps_generic_home_channel_hint(self):\n+        telegram_cfg = SimpleNamespace(enabled=True, token=\"***\", extra={})\n+        config = SimpleNamespace(\n+            platforms={Platform.TELEGRAM: telegram_cfg},\n+            get_home_channel=lambda _platform: None,\n+        )\n+        with patch(\"gateway.config.load_gateway_config\", return_value=config), \\\n+             patch(\"tools.interrupt.is_interrupted\", return_value=False):\n+            result = json.loads(\n+                send_message_tool(\n+                    {\n+                        \"action\": \"send\",\n+                        \"target\": \"telegram\",\n+                        \"message\": \"hi\",\n+                    }\n+                )\n+            )\n+        assert \"TELEGRAM_HOME_CHANNEL\" in result[\"error\"]\n+\n+\n class TestSendDiscordThreadId:\n     \"\"\"_send_discord uses thread_id when provided.\"\"\"\n "]},{"file":"tools/send_message_tool.py","hunks":["@@ -40,6 +40,15 @@\n # downstream adapters (signal, etc.) expect.\n _PHONE_PLATFORMS = frozenset({\"signal\", \"sms\", \"whatsapp\"})\n _E164_TARGET_RE = re.compile(r\"^\\s*\\+(\\d{7,15})\\s*$\")\n+# Email addresses — a valid email like \"[redacted:email]\" should be treated as\n+# an explicit target for the email platform, not fall through to channel-name\n+# resolution which has no way to resolve a raw address.\n+_EMAIL_TARGET_RE = re.compile(r\"^\\s*[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\s*$\")\n+# Most platforms read their home channel from \"<PLATFORM>_HOME_CHANNEL\", but a\n+# few diverge. Email reads EMAIL_HOME_ADDRESS (see gateway/config.py), so the\n+# generic \"<PLATFORM>_HOME_CHANNEL\" hint would point users at a variable that is\n+# never read. Map the exceptions so the error guidance is actually actionable.\n+_HOME_CHANNEL_ENV_OVERRIDES = {\"email\": \"EMAIL_HOME_ADDRESS\"}\n _IMAGE_EXTS = {\".jpg\", \".jpeg\", \".png\", \".webp\", \".gif\"}\n _VIDEO_EXTS = {\".mp4\", \".mov\", \".avi\", \".mkv\", \".3gp\"}\n _AUDIO_EXTS = {\".ogg\", \".opus\", \".mp3\", \".wav\", \".m4a\", \".flac\"}","@@ -265,10 +274,13 @@ def _handle_send(args):\n             chat_id = home.chat_id\n             used_home_channel = True\n         else:\n+            home_env = _HOME_CHANNEL_ENV_OVERRIDES.get(\n+                platform_name, f\"{platform_name.upper()}_HOME_CHANNEL\"\n+            )\n             return json.dumps({\n                 \"error\": f\"No home channel set for {platform_name} to determine where to send the message. \"\n                 f\"Either specify a channel directly with '{platform_name}:CHANNEL_NAME', \"\n-                f\"or set a home channel via: hermes config set {platform_name.upper()}_HOME_CHANNEL <channel_id>\"\n+                f\"or set a home channel via: hermes config set {home_env} <channel_id>\"\n             })\n \n     duplicate_skip = _maybe_skip_cron_duplicate_send(platform_name, chat_id, thread_id)","@@ -383,6 +395,10 @@ def _parse_target_ref(platform_name: str, target_ref: str):\n         if target_ref.strip().isdigit():\n             return f\"group:{target_ref.strip()}\", None, True\n         return None, None, False\n+    if platform_name == \"email\":\n+        match = _EMAIL_TARGET_RE.fullmatch(target_ref)\n+        if match:\n+            return target_ref.strip(), None, True\n     if platform_name in _PHONE_PLATFORMS:\n         match = _E164_TARGET_RE.fullmatch(target_ref)\n         if match:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}