-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[Bug]: Cron jobs to WhatsApp fail with Baileys jidDecode error when deliver uses human-readable contact label #1945
Description
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
- 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"
}
-
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).
-
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>).
-
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.
- Now edit the same job in ~/.hermes/cron/jobs.json to instead use a valid JID:
• Either deliver: "origin"
• Or deliver: "whatsapp:12345678901234@lid" - 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)
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)
-
Enhance
_resolve_delivery_target()incron/scheduler.pyto 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.
- 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