Skip to content

[Bug]: Cron jobs to WhatsApp fail with Baileys jidDecode error when deliver uses human-readable contact label #1945

@dan-and

Description

@dan-and

Bug Description

Summary

When scheduling Hermes cron jobs that auto-deliver results to WhatsApp, jobs execute successfully but delivery fails if the deliver field is set using the human-readable label shown by
send_message(action="list") (e.g. whatsapp:Alice (dm)). The cron scheduler treats the portion after whatsapp: as a literal WhatsApp JID and passes it to the WhatsApp bridge, which then
fails with a Baileys jidDecode error. Manual WhatsApp conversations and gateway-driven messages work fine; the issue is specific to cron auto-delivery.

Environment

• OS: Linux (modern 6.x kernel)
• Hermes Agent: current main (as of mid‑March 2026)
• Node: 18.x
• Python: 3.12.x
• WhatsApp bridge: scripts/whatsapp-bridge/bridge.js (Baileys-based)

What works

• Gateway WhatsApp integration is healthy:
• Manual conversations between the agent and a WhatsApp contact work.
• ~/.hermes/logs/gateway.log shows lines like:

  2026-03-18 08:15:00,708 INFO gateway.platforms.base: [Whatsapp] Sending response (626 chars) to 12345678901234@lid
  2026-03-18 11:44:09,279 INFO gateway.platforms.base: [Whatsapp] Sending response (682 chars) to 12345678901234@lid
  2026-03-18 13:22:07,525 INFO gateway.platforms.base: [Whatsapp] Sending response (147 chars) to 12345678901234@lid

• ~/.hermes/channel_directory.json correctly maps the WhatsApp contact:

      {
        "platforms": {
          "whatsapp": [
            {
              "id": "12345678901234@lid",
              "name": "Alice",
              "type": "dm",
              "thread_id": null
            }
          ]
        }
      }

What fails

When a cron job is configured with deliver using the label shown in the channel directory or send_message(action="list"), e.g.:

{
   "id": "315e6f2a8f57",
   "name": "whatsapp-cron-example",
   "deliver": "whatsapp:Alice (dm)",
   "origin": {
     "platform": "whatsapp",
     "chat_id": "12345678901234@lid",
     "chat_name": "Alice",
     "thread_id": null
   },
   ...
 }

the cron job executes and output is written to ~/.hermes/cron/output/<job_id>/...md, but delivery to WhatsApp fails with errors like:

2026-03-18 14:00:33,347 ERROR cron.scheduler: Job '315e6f2a8f57': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is
undefined."}

A similar earlier run showed:

2026-03-18 13:24:35,109 ERROR cron.scheduler: Job '9cc2b836678f': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is
undefined."}

The cron scheduler reports the job as executed and last_status: "ok", but the WhatsApp device never receives the cron result.

Steps to Reproduce

  1. Run gateway + WhatsApp bridge and pair successfully, so that:
    • Manual chats with a contact (e.g. “Alice”) work.
    • channel_directory.json contains an entry similar to:
       {
         "id": "12345678901234@lid",
         "name": "Alice",
         "type": "dm"
       }

  1. Create a cron job from that WhatsApp conversation, with e.g.:
    • origin.platform = "whatsapp"
    • origin.chat_id = "12345678901234@lid"
    • deliver = "whatsapp:Alice (dm)"

    (i.e. use the human-readable label that appears in the “Available messaging targets” list, including the (dm) suffix).

  2. Let the job run at its scheduled time, or trigger it manually (e.g. via cron.jobs.trigger_job or hermes cron run <job_id>).

  3. Observe:
    • A new output file is created under ~/.hermes/cron/output/<job_id>/...md (job ran successfully).
    • ~/.hermes/logs/errors.log or gateway.log contains:

    ERROR cron.scheduler: Job '<job_id>': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined."}

• No message is delivered to WhatsApp for that cron run.
  1. Now edit the same job in ~/.hermes/cron/jobs.json to instead use a valid JID:
    • Either deliver: "origin"
    • Or deliver: "whatsapp:12345678901234@lid"
  2. Trigger the job again and observe that delivery now succeeds and the cron result appears in the WhatsApp chat.

Expected Behavior

• Cron jobs should be able to use the same human-friendly deliver target format that the documentation and send_message(action="list") present to the user, e.g.:
• deliver: "whatsapp:Alice (dm)"
• In that case, the cron scheduler should:
1. Parse platform_name = "whatsapp", target_ref = "Alice (dm)".
2. Strip display-only suffixes like " (dm)".
3. Use gateway.channel_directory.resolve_channel_name("whatsapp", "Alice") to resolve to the underlying JID 12345678901234@lid.
4. Pass the resolved JID to _send_to_platform() / the WhatsApp bridge.
• No Baileys jidDecode errors, and the message appears on the WhatsApp device.

Actual Behavior

• cron/scheduler.py::_resolve_delivery_target() currently does:

    if ":" in deliver:
        platform_name, chat_id = deliver.split(":", 1)
        return {
            "platform": platform_name,
            "chat_id": chat_id,
            "thread_id": None,
        }

• For deliver = "whatsapp:Alice (dm)", this yields:
• platform_name = "whatsapp"
• chat_id = "Alice (dm)"
• This literal string is passed to:

from tools.send_message_tool import _send_to_platform
...
result = asyncio.run(_send_to_platform(platform, pconfig, chat_id, content, thread_id=thread_id))

• _send_to_platform() then calls _send_whatsapp(extra, chat_id, message), which POSTs:


    { "chatId": "Alice (dm)", "message": "..." }

    to the bridge’s /send endpoint:

    app.post('/send', async (req, res) => {
      const { chatId, message } = req.body;
      const sent = await sock.sendMessage(chatId, { text: prefixed });
      ...
    });

• Baileys attempts to decode chatId as a JID and fails, producing:

Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined.

• Cron therefore logs a delivery error, and the user never sees the message on WhatsApp, despite the job itself having succeeded.

Affected Component

Gateway (Telegram/Discord/Slack/WhatsApp)

Messaging Platform (if gateway-related)

WhatsApp

Operating System

Ubuntu 24.04

Python Version

3.11.15

Hermes Version

0.4.0 (2026.06.18)

Relevant Logs / Traceback

.hermes/logs/errors.log

2026-03-18 13:24:35,109 ERROR cron.scheduler: Job '9cc2b836678f': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined."}
2026-03-18 13:44:34,664 ERROR cron.scheduler: Job '9cc2b836678f': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined."}
2026-03-18 14:00:33,347 ERROR cron.scheduler: Job '315e6f2a8f57': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined."}
2026-03-18 14:30:09,169 ERROR cron.scheduler: Job '9cc2b836678f': delivery error: WhatsApp bridge error (500): {"error":"Cannot destructure property 'user' of 'jidDecode(...)' as it is undefined."}

Root Cause Analysis (optional)

No response

Proposed Fix (optional)

  1. Enhance _resolve_delivery_target() in cron/scheduler.py to resolve human-friendly names via the channel directory, mirroring send_message behavior.

    Example logic:

    from gateway.channel_directory import resolve_channel_name

     if ":" in deliver:
         platform_name, target_ref = deliver.split(":", 1)
         platform_name = platform_name.strip()
         target_ref = target_ref.strip()
         # Strip display suffixes like " (dm)" / " (group)"
         normalized = target_ref
         if normalized.endswith(")") and " (" in normalized:
             normalized = normalized.rsplit(" (", 1)[0].strip()
         resolved = None
         try:
             resolved = resolve_channel_name(platform_name.lower(), normalized)
         except Exception:
             resolved = None
         return {
             "platform": platform_name,
             "chat_id": resolved or target_ref,
             "thread_id": None,
         }
 This preserves compatibility:
• If deliver is already a raw JID (e.g. whatsapp:12345678901234@lid), resolve_channel_name will typically return None, and the code falls back to using target_ref unchanged.
  1. Clarify the docs for deliver, explicitly stating the supported formats and how name-based targets are resolved for WhatsApp.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Workaround:

What works, but is pretty hacky is following code changes:

git diff cron/scheduler.py tools/send_message_tool.py
diff --git a/cron/scheduler.py b/cron/scheduler.py
index ea7ff0e9..f8476ef3 100644
--- a/cron/scheduler.py
+++ b/cron/scheduler.py
@@ -80,10 +80,31 @@ def _resolve_delivery_target(job: dict) -> Optional[dict]:
         }

     if ":" in deliver:
-        platform_name, chat_id = deliver.split(":", 1)
+        platform_name, target_ref = deliver.split(":", 1)
+        platform_name = platform_name.strip()
+        target_ref = target_ref.strip()
+
+        # Cron delivery often gets configured using the human-friendly
+        # labels from `send_message(action="list")`, e.g.:
+        #   "whatsapp:Daniel Andersen (dm)"
+        # Those labels are not valid WhatsApp JIDs; resolve them via the
+        # channel directory so the bridge receives a real "<number>@lid".
+        resolved = None
+        try:
+            from gateway.channel_directory import resolve_channel_name
+
+            normalized = target_ref
+            if normalized.endswith(")") and " (" in normalized:
+                # Strip display suffix like "(dm)" or "(group)"
+                normalized = normalized.rsplit(" (", 1)[0].strip()
+
+            resolved = resolve_channel_name(platform_name.lower(), normalized)
+        except Exception:
+            resolved = None
+
         return {
             "platform": platform_name,
-            "chat_id": chat_id,
+            "chat_id": resolved or target_ref,
             "thread_id": None,
         }

diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py
index 3ebd6c5b..eb6cb6ff 100644
--- a/tools/send_message_tool.py
+++ b/tools/send_message_tool.py
@@ -94,7 +94,13 @@ def _handle_send(args):
     if target_ref and not is_explicit:
         try:
             from gateway.channel_directory import resolve_channel_name
-            resolved = resolve_channel_name(platform_name, target_ref)
+            normalized = target_ref
+            # WhatsApp list entries often include display suffixes like " (dm)".
+            # The directory stores the plain name without that suffix.
+            if platform_name == "whatsapp" and normalized.endswith(")") and " (" in normalized:
+                normalized = normalized.rsplit(" (", 1)[0].strip()
+
+            resolved = resolve_channel_name(platform_name, normalized)
             if resolved:
                 chat_id, thread_id, _ = _parse_target_ref(platform_name, resolved)
             else:
@@ -194,6 +200,26 @@ def _parse_target_ref(platform_name: str, target_ref: str):
         match = _TELEGRAM_TOPIC_TARGET_RE.fullmatch(target_ref)
         if match:
             return match.group(1), match.group(2), True
+
+    # WhatsApp: accept direct JIDs (e.g. "<number>@lid") and also convert bare
+    # numbers ("<number>"/"+<number>") to "<number>@<suffix>" using the configured
+    # home channel (WHATSAPP_HOME_CHANNEL).
+    if platform_name == "whatsapp":
+        t = (target_ref or "").strip()
+
+        # Direct JID-like target
+        if "@" in t:
+            return t, None, True
+
+        # Bare phone number / digits
+        t_digits = t[1:] if t.startswith("+") else t
+        if t_digits.isdigit():
+            home = os.getenv("WHATSAPP_HOME_CHANNEL", "").strip()
+            if "@" in home:
+                suffix = home.split("@", 1)[1]
+                return f"{t_digits}@{suffix}", None, True
+            return t_digits, None, True
+
     if target_ref.lstrip("-").isdigit():
         return target_ref, None, True
     return None, None, False

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions