# NousResearch/hermes-agent · WhatsApp send_message target routing and JID handling

## Open now

- 2 issues, 2 PRs open on GitHub.
- #33547: send_message should trust channel-directory resolution for plugin platform targets
- #37906: send_message tool fails for WhatsApp: @lid JIDs not recognized, home channel fallback, and raw phone numbers cause jidDecode error
- PR #27444: fix(tools): trust resolve_channel_name for plugin platform targets
- PR #68322: Fix/plugin target verbatim fallback

These issues and fixes concern failures in send_message target resolution for WhatsApp, including unrecognized JID formats (groups, DMs, LIDs), bare phone numbers missing the @s.whatsapp.net suffix, and incorrect fallback to home channel instead of the intended target. Some adjacent items touch email and Slack target recognition in the same routing logic.

## TL;DR

- 41 member(s): 2 open issue(s), 13 closed issue(s), 2 open PR(s), 6 merged PR(s), 18 PR(s) closed without merging.
- A fix was merged to main in PR #3860. Verify it addresses your case.
- **Recommended:** read PR #3860 (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: `partially_resolved` · deciding PR(s): `pr-3860,pr-35257,pr-50379,pr-53588`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.05`: 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.

## Open related issues

- #33547: send_message should trust channel-directory resolution for plugin platform targets
- #37906: send_message tool fails for WhatsApp: @lid JIDs not recognized, home channel fallback, and raw phone numbers cause jidDecode error

## Closed / duplicate issues

- #1945: \[Bug\]: Cron jobs to WhatsApp fail with Baileys jidDecode error when deliver uses human-readable contact label
- #2899: \[Bug\]: Slack send_message can fall back to home DM instead of explicit channel target
- #8637: WhatsApp bridge crashes on bare phone numbers — missing JID normalization
- #14486: \[Bug\]: WhatsApp bridge fails to resolve contacts from allowlist when using phone numbers
- #17295: \[WhatsApp\] send_message tool: "No home channel set" even when WHATSAPP_HOME_CHANNEL is configured
- #18646: \[Bug\]: send_message ignores WhatsApp group target, routes to home channel instead
- #31396: \[Bug\]: send_message silently delivers to home channel and reports success when target resolution fails (WhatsApp LID format)
- #32538: \[Bug\]: WhatsApp Integration Failure
- #34740: \[Bug\]: send_message does not recognize explicit email targets and incorrectly falls back to home target resolution
- #41407: \[Bug\] send_message WhatsApp targets: group/LID JIDs silently fall back to home channel
- #41660: WhatsApp send fails with bare phone number — missing @s.whatsapp.net JID suffix
- #59136: WhatsApp LID format not recognized in send_message tool
- #67941: Plugin platforms cannot define explicit target formats for \`hermes send\`

## Prior & competing PRs

- **#3860: fix(cron): resolve human-friendly delivery labels via channel directory**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cron/scheduler.py`, `tests/cron/test_scheduler.py`
- **#3862: fix(auth): stop silently falling back to OpenRouter when no provider is configured**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli-config.yaml.example`, `cli.py`, `cron/scheduler.py`, `gateway/run.py`, `hermes_cli/auth.py`, `tests/test_api_key_providers.py`
- **#8639: fix(whatsapp): normalize bare phone targets to JIDs**. closed without merge · fix tier `suggested`
  - maintainer direction: category `merged_via`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#14738: fix(tools): resolve whatsapp phone numbers to lid format**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8639`, `#8639`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#35257: fix(update/windows): robustly exclude launcher-shim ancestors from concurrent check**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`; implication `superseded`
  - 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:
    ```
- **#41415: fix(#41407): recognize WhatsApp JID formats (groups, DMs, LIDs)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `merged_via`; implication `superseded`; refs `ea49a79633d93202d8e495648b2586ee5a1fbecc`, `#46622`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#41581: fix(whatsapp): recognize JID targets and reject bare digits (#41407)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#41866: fix(whatsapp): normalize bare phone numbers to full JID for outgoing sends (#41660)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8639`, `#8639`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#50379: fix(whatsapp): normalize bare phone targets to JIDs (salvage #8639)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/whatsapp_identity.py`, `plugins/platforms/whatsapp/adapter.py`, `tests/gateway/test_whatsapp_connect.py`, `tests/gateway/test_whatsapp_to_jid.py`
  - fix hunks:
  - `gateway/whatsapp_identity.py`

    ```diff
    @@ -67,6 +67,57 @@ def normalize_whatsapp_identifier(value: str) -> str:
         )
     
     
    +# A target that is "just a phone number" — optional leading ``+`` then digits
    +# and the usual human separators (spaces, dots, dashes, parens). Anything that
    +# already carries an ``@`` is a fully-qualified JID and must pass through
    +# untouched (group ``@g.us``, LID ``@lid``, ``status@broadcast`` etc.).
    +_BARE_PHONE_RE = re.compile(r"^\+?[\d\s().\-]+$")
    +
    +
    +def to_whatsapp_jid(value: str) -> str:
    +    """Normalize an *outbound* WhatsApp target to a bridge-safe JID.
    +
    +    Baileys' ``jidDecode`` crashes on a bare phone number — it expects a
    +    fully-qualified JID such as ``[redacted:email]``. This helper
    +    is the inverse of :func:`normalize_whatsapp_identifier`: instead of
    +    stripping a JID down to its numeric core for comparison, it *builds* the
    +    JID a send must use.
    +
    +    Behaviour:
    +
    +    - ``"+50766715226"`` / ``"50766715226"`` → ``"[redacted:email]"``
    +    - ``"[redacted:email]"`` → unchanged
    +    - ``"[redacted:email]"`` / ``"130631430344750@lid"`` → unchanged
    +    - ``"user:[redacted:email]"`` style colon-before-``@`` → ``@`` form
    +    - anything that isn't a recognizable bare phone → returned unchanged so
    +      the bridge can surface a meaningful error rather than us mangling it.
    +
    +    Returns ``""`` for an empty/whitespace input.
    +    """
    +    if not value:
    +        return ""
    +
    +    normalized = str(value).strip()
    +    # Drop a device suffix before the domain: ``user:device@domain`` is a
    +    # legacy Baileys shape whose ``:device`` part is not addressable — collapse
    +    # it to ``user@domain``. (Mirrors normalize_whatsapp_identifier, which
    +    # splits the bare id on ``:`` for the same reason.)
    +    if ":" in normalized and "@" in normalized:
    +        prefix, _, domain = normalized.partition("@")
    +        normalized = f"{prefix.split(':', 1)[0]}@{domain}"
    +
    +    # Already a fully-qualified JID — leave it alone.
    +    if "@" in normalized:
    +        return normalized
    +
    +    if _BARE_PHONE_RE.fullmatch(normalized):
    +        digits = re.sub(r"\D+", "", normalized)
    +        if digits:
    +            return f"{digits}@s.whatsapp.net"
    +
    +    return normalized
    +
    +
     def expand_whatsapp_aliases(identifier: str) -> Set[str]:
         """Resolve WhatsApp phone/LID aliases via bridge session mapping files.
     
    ```
  - `plugins/platforms/whatsapp/adapter.py`

    ```diff
    @@ -182,6 +182,7 @@ def _terminate_bridge_process(proc, *, force: bool = False) -> None:
     
     from gateway.config import Platform, PlatformConfig
     from gateway.platforms.whatsapp_common import WhatsAppBehaviorMixin
    +from gateway.whatsapp_identity import to_whatsapp_jid
     from gateway.platforms.base import (
         BasePlatformAdapter,
         MessageEvent,
    ```

    ```diff
    @@ -726,6 +727,8 @@ async def send(
             if not content or not content.strip():
                 return SendResult(success=True, message_id=None)
     
    +        chat_id = to_whatsapp_jid(chat_id)
    +
             try:
                 import aiohttp
     
    ```

    ```diff
    @@ -785,7 +788,7 @@ async def edit_message(
                 async with self._http_session.post(
                     f"http://127.0.0.1:{self._bridge_port}/edit",
                     json={
    -                    "chatId": chat_id,
    +                    "chatId": to_whatsapp_jid(chat_id),
                         "messageId": message_id,
                         "message": content,
                     },
    ```

    ```diff
    @@ -820,7 +823,7 @@ async def _send_media_to_bridge(
                     return SendResult(success=False, error=f"File not found: {file_path}")
     
                 payload: Dict[str, Any] = {
    -                "chatId": chat_id,
    +                "chatId": to_whatsapp_jid(chat_id),
                     "filePath": file_path,
                     "mediaType": media_type,
                 }
    ```

    ```diff
    @@ -932,7 +935,7 @@ async def send_typing(self, chat_id: str, metadata=None) -> None:
                 # socket in CLOSE_WAIT. See #18451.
                 async with self._http_session.post(
                     f"http://127.0.0.1:{self._bridge_port}/typing",
    -                json={"chatId": chat_id},
    +                json={"chatId": to_whatsapp_jid(chat_id)},
                     timeout=aiohttp.ClientTimeout(total=5)
                 ):
                     pass
    ```

    ```diff
    @@ -950,7 +953,7 @@ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
                 import aiohttp
     
                 async with self._http_session.get(
    -                f"http://127.0.0.1:{self._bridge_port}/chat/{chat_id}",
    +                f"http://127.0.0.1:{self._bridge_port}/chat/{to_whatsapp_jid(chat_id)}",
                     timeout=aiohttp.ClientTimeout(total=10)
                 ) as resp:
                     if resp.status == 200:
    ```

    ```diff
    @@ -1238,10 +1241,11 @@ async def _standalone_send(
             return {"error": "aiohttp not installed. Run: pip install aiohttp"}
         try:
             bridge_port = extra.get("bridge_port", 3000)
    +        normalized_chat_id = to_whatsapp_jid(chat_id)
             async with aiohttp.ClientSession() as session:
                 async with session.post(
                     f"http://localhost:{bridge_port}/send",
    -                json={"chatId": chat_id, "message": message},
    +                json={"chatId": normalized_chat_id, "message": message},
                     timeout=aiohttp.ClientTimeout(total=30),
                 ) as resp:
                     if resp.status == 200:
    ```

    ```diff
    @@ -1249,7 +1253,7 @@ async def _standalone_send(
                         return {
                             "success": True,
                             "platform": "whatsapp",
    -                        "chat_id": chat_id,
    +                        "chat_id": normalized_chat_id,
                             "message_id": data.get("messageId"),
                         }
                     body = await resp.text()
    ```
  - `tests/gateway/test_whatsapp_connect.py`

    ```diff
    @@ -262,6 +262,51 @@ async def test_send_marks_retryable_fatal_when_managed_bridge_exits(self):
             mock_fh.close.assert_called_once()
             assert adapter._bridge_log_fh is None
     
    +    @pytest.mark.asyncio
    +    async def test_send_normalizes_bare_phone_numbers_to_jid(self):
    +        """A bare phone target (with or without +) becomes a full JID.
    +
    +        Baileys' jidDecode crashes on a bare number (#8637); the adapter
    +        must rewrite it to ``<digits>@s.whatsapp.net`` before the bridge
    +        call. Regression guard for that crash.
    +        """
    +        adapter = _make_adapter()
    +        adapter._running = True
    +        adapter._bridge_process = None  # unmanaged bridge — skip exit check
    +
    +        mock_resp = MagicMock()
    +        mock_resp.status = 200
    +        mock_resp.json = AsyncMock(return_value={"messageId": "msg-1"})
    +        mock_session = MagicMock()
    +        mock_session.post = MagicMock(return_value=_AsyncCM(mock_resp))
    +        adapter._http_session = mock_session
    +
    +        result = await adapter.send("+50766715226", "hello")
    +
    +        assert result.success is True
    +        payload = mock_session.post.call_args.kwargs["json"]
    +        assert payload["chatId"] == "[redacted:email]"
    +
    +    @pytest.mark.asyncio
    +    async def test_send_leaves_group_jid_untouched(self):
    +        """A fully-qualified group JID must pass through unchanged."""
    +        adapter = _make_adapter()
    +        adapter._running = True
    +        adapter._bridge_process = None
    +
    +        mock_resp = MagicMock()
    +        mock_resp.status = 200
    +        mock_resp.json = AsyncMock(return_value={"messageId": "msg-2"})
    +        mock_session = MagicMock()
    +        mock_session.post = MagicMock(return_value=_AsyncCM(mock_resp))
    +        adapter._http_session = mock_session
    +
    +        result = await adapter.send("[redacted:email]", "hello")
    +
    +        assert result.success is True
    +        payload = mock_session.post.call_args.kwargs["json"]
    +        assert payload["chatId"] == "[redacted:email]"
    +
         @pytest.mark.asyncio
         async def test_poll_messages_marks_retryable_fatal_when_managed_bridge_exits(self):
             adapter = _make_adapter()
    ```
  - `tests/gateway/test_whatsapp_to_jid.py`

    ```diff
    @@ -0,0 +1,56 @@
    +"""Unit tests for gateway.whatsapp_identity.to_whatsapp_jid.
    +
    +``to_whatsapp_jid`` is the outbound inverse of
    +``normalize_whatsapp_identifier``: it builds the bridge-safe JID a send
    +must use. Baileys' ``jidDecode`` crashes on a bare phone number (#8637),
    +so every outbound target must be rewritten to ``<digits>@s.whatsapp.net``
    +before it reaches the bridge.
    +"""
    +
    +import pytest
    +
    +from gateway.whatsapp_identity import to_whatsapp_jid
    +
    +
    +class TestToWhatsappJid:
    +    @pytest.mark.parametrize(
    +        "raw,expected",
    +        [
    +            # bare phone numbers → user JID
    +            ("+50766715226", "[redacted:email]"),
    +            ("50766715226", "[redacted:email]"),
    +            # human-formatted phone numbers get stripped to digits
    +            ("+1 (555) 123-4567", "[redacted:email]"),
    +            ("+1.555.123.4567", "[redacted:email]"),
    +        ],
    +    )
    +    def test_bare_phone_becomes_user_jid(self, raw, expected):
    +        assert to_whatsapp_jid(raw) == expected
    +
    +    @pytest.mark.parametrize(
    +        "jid",
    +        [
    +            "[redacted:email]",  # already a user JID
    +            "[redacted:email]",    # group JID
    +            "130631430344750@lid",         # linked identity
    +            "status@broadcast",            # broadcast pseudo-chat
    +            "123@newsletter",              # channel/newsletter
    +        ],
    +    )
    +    def test_fully_qualified_jid_passes_through(self, jid):
    +        assert to_whatsapp_jid(jid) == jid
    +
    +    def test_device_suffixed_colon_form_collapses_to_at(self):
    +        # ``user:device@domain`` (legacy) → ``user@domain``
    +        assert to_whatsapp_jid("60123456789:[redacted:email]") == (
    +            "[redacted:email]"
    +        )
    +
    +    @pytest.mark.parametrize("empty", ["", "   ", None])
    +    def test_empty_input_returns_empty(self, empty):
    +        assert to_whatsapp_jid(empty) == ""
    +
    +    def test_unrecognized_target_passes_through_unchanged(self):
    +        # Not a phone, no ``@`` — leave it for the bridge to reject with a
    +        # meaningful error rather than mangling it into a bogus JID.
    +        assert to_whatsapp_jid("not-a-number") == "not-a-number"
    ```
- **#53588: fix(whatsapp): resolve phone↔LID aliases in adapter DM/group allowlist**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/whatsapp_common.py`, `tests/gateway/test_whatsapp_allowlist_lid_resolution.py`
- **#1950: fix(messaging): resolve explicit delivery labels consistently**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#3860`, `#1945`
- **#2001: fix(messaging): resolve explicit delivery labels consistently (#1945)**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#1945`
- **#17189: fix(whatsapp): normalise JIDs and resolve LID aliases in dm_policy / group_policy allowlist check**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#53588`, `#14486`, `#53588`
- **#17449: fix(gateway): load WhatsApp home channel from env overrides**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#17295`, `#9220`, `#12520`
- **#27444: fix(tools): trust resolve_channel_name for plugin platform targets**. open
  - maintainer direction: category `merged_via`; implication `superseded`
  - files: `cron/scheduler.py`, `gateway/channel_directory.py`, `gateway/platform_registry.py`, `gateway/platforms/ADDING_A_PLATFORM.md`, `tests/cron/test_scheduler.py`, `tests/gateway/test_channel_directory.py`, `tests/tools/test_send_message_tool.py`, `tools/send_message_tool.py`
- **#31398: fix(send_message): accept WhatsApp JID targets**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#20718`, `#20718`, `#18899`
- **#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`
- **#37924: fix(whatsapp): recognise @lid and @s.whatsapp.net JIDs in send_message target resolution**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `ea49a79633d93202d8e495648b2586ee5a1fbecc`
- **#41667: fix(whatsapp): normalise bare phone numbers to JIDs before bridge send**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8639`, `#8639`
- **#41689: Normalize bare WhatsApp phone targets to JIDs**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#41667`, `#41660`
- **#45438: fix(send_message): recognize WhatsApp JIDs as explicit send targets**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#12620`, `#37924`
- **#51036: fix(whatsapp): resolve LID↔phone aliases in Python DM allowlist**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#53588`, `#14486`, `#53588`
- **#59166: fix(send_message): accept WhatsApp LID targets**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `ea49a79633d93202d8e495648b2586ee5a1fbecc`, `#59136`
- **#59199: fix(tools): recognize WhatsApp LID format in send_message target parsing**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#59166`, `#59136`, `#59166`, `#41415`, `#41581`
- **#68322: Fix/plugin target verbatim fallback**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `0ce93f2d4933`, `#67941`, `#68322`, `#68322`, `#68322`, `#67941`, `#64900`, `#374151`, `#67941`, `#68322`
  - files: `tests/tools/test_send_message_target_parse.py`, `tests/tools/test_send_message_tool.py`, `tools/send_message_tool.py`

## Fix status

- Merged to main in PR #3860 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-27`.
- Subsystem: `agent`
