hermes - 💡(How to fix) Fix hermes-simplex-bugs-report

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The SimpleX Chat platform adapter at plugins/platforms/simplex/adapter.py has three independent bugs that together prevent any incoming-message-to-reply flow from working. I found and patched all three over the course of an end-to-end debugging session. With patches applied, messaging works correctly: phone → SimpleX network → daemon → adapter → model → reply → phone.

Error Message

"type": "error",

Root Cause

The SimpleX Chat platform adapter at plugins/platforms/simplex/adapter.py has three independent bugs that together prevent any incoming-message-to-reply flow from working. I found and patched all three over the course of an end-to-end debugging session. With patches applied, messaging works correctly: phone → SimpleX network → daemon → adapter → model → reply → phone.

Fix Action

Fix

Use the internal API form, which works with the numeric chat_id already in scope:

 if chat_id.startswith("group:"):
     group_id = chat_id[6:]
-    cmd_str = f"#[{group_id}] {content}"
+    cmd_str = f"/_send #{group_id} text {content}"
 else:
-    cmd_str = f"@[{chat_id}] {content}"
+    cmd_str = f"/_send @{chat_id} text {content}"

Apply the same change in standalone_send (replace content with message per that function's parameter name).

Code Example

async with _wsclient.connect(self.ws_url, ping_interval=20, ping_timeout=20) as ws:
    self._ws = ws
    backoff = WS_RETRY_DELAY_INITIAL
    self._last_ws_activity = time.time()
    logger.info("SimpleX WS: connected")

    async for raw in ws:
        ...

---

logger.info("SimpleX WS: connected")
+                    await ws.send(json.dumps({"corrId": "hermes-init", "cmd": "/_start"}))

                     async for raw in ws:

---

elif resp_type == "newChatItems":
    items = event.get("chatItems") or []
    for item_wrapper in items:
        await self._handle_new_chat_item(item_wrapper)

---

{
  "resp": {
    "type": "newChatItems",
    "user": { ... },
    "chatItems": [ ... ]
  }
}

---

data = json.loads(raw_event)
print('TOP KEYS:', list(data.keys()))                       # ['resp']
print('RESP TYPE:', data['resp']['type'])                   # 'newChatItems'
print('chatItems in TOP:', 'chatItems' in data)             # False
print('chatItems in RESP:', 'chatItems' in data['resp'])    # True

---

elif resp_type == "newChatItems":
-    items = event.get("chatItems") or []
+    items = event.get("chatItems") or event.get("resp", {}).get("chatItems") or []
     for item_wrapper in items:
         await self._handle_new_chat_item(item_wrapper)

---

if chat_id.startswith("group:"):
    group_id = chat_id[6:]
    cmd_str = f"#[{group_id}] {content}"
else:
    cmd_str = f"@[{chat_id}] {content}"

---

{
  "resp": {
    "type": "chatCmdError",
    "chatError": {
      "type": "error",
      "errorType": {"type": "contactNotFound", "contactName": "[4]"}
    }
  }
}

---

if chat_id.startswith("group:"):
     group_id = chat_id[6:]
-    cmd_str = f"#[{group_id}] {content}"
+    cmd_str = f"/_send #{group_id} text {content}"
 else:
-    cmd_str = f"@[{chat_id}] {content}"
+    cmd_str = f"/_send @{chat_id} text {content}"
RAW_BUFFERClick to expand / collapse

SimpleX adapter: three bugs that prevent end-to-end messaging

Summary

The SimpleX Chat platform adapter at plugins/platforms/simplex/adapter.py has three independent bugs that together prevent any incoming-message-to-reply flow from working. I found and patched all three over the course of an end-to-end debugging session. With patches applied, messaging works correctly: phone → SimpleX network → daemon → adapter → model → reply → phone.

Environment

  • Hermes image: nousresearch/hermes-agent:latest
  • simplex-chat: latest stable from the simplex-chat/simplex-chat releases page (Ubuntu 24.04 x86_64 binary)
  • Host: Ubuntu 24.04, Docker Desktop on Linux
  • Topology: Hermes runs in Docker Desktop's VM; simplex-chat -p 5225 binds only to 127.0.0.1:5225 on the host with no flag to bind elsewhere. A socat TCP-LISTEN:5226,fork,reuseaddr TCP:127.0.0.1:5225 relay is required so the container can reach the daemon via host.docker.internal:5226. This is relevant context but is not the source of any bug — a Python websockets listener through the same relay receives events normally.

Bug 1: _ws_listener never sends /_start, so no events are pushed

File: plugins/platforms/simplex/adapter.py, inside SimplexAdapter._ws_listener

Current code

async with _wsclient.connect(self.ws_url, ping_interval=20, ping_timeout=20) as ws:
    self._ws = ws
    backoff = WS_RETRY_DELAY_INITIAL
    self._last_ws_activity = time.time()
    logger.info("SimpleX WS: connected")

    async for raw in ws:
        ...

The adapter opens the WebSocket and immediately iterates incoming frames without sending any subscribe command.

Why this fails

simplex-chat's WebSocket protocol pushes async events (newChatItems, chatItemUpdated, etc.) only to connections that have sent /_start. Without it, the daemon receives messages over SMP and stores them in its database (chatStats unreadCount increments, rcvNew items appear in /chats), but no event is pushed to the WebSocket subscriber. The connection just idles, producing the repeated SimpleX: WS idle for 148s, forcing reconnect warnings users see in logs.

Repro

Two Python listeners against the same daemon. The one that sends /_start receives events when a phone sends a message; the one that doesn't receives nothing, even though /chats queries afterward show the message arrived and is stored as rcvNew.

Fix

Send /_start immediately after the connection log line. The chatRunning response comes back with corrId="hermes-init", which starts with the existing _CORR_PREFIX = "hermes-" and is already filtered as an echo by _handle_event. No additional handling required.

                     logger.info("SimpleX WS: connected")
+                    await ws.send(json.dumps({"corrId": "hermes-init", "cmd": "/_start"}))

                     async for raw in ws:

Bug 2: newChatItems events are silently dropped due to wrong nesting

File: plugins/platforms/simplex/adapter.py, inside SimplexAdapter._handle_event

Current code

elif resp_type == "newChatItems":
    items = event.get("chatItems") or []
    for item_wrapper in items:
        await self._handle_new_chat_item(item_wrapper)

Why this fails

The daemon's actual newChatItems payload has this shape:

{
  "resp": {
    "type": "newChatItems",
    "user": { ... },
    "chatItems": [ ... ]
  }
}

chatItems lives inside resp, not at the top level. event.get("chatItems") returns None, the loop never runs, and every batched message event is silently dropped. The singular newChatItem path passes the full event to _handle_new_chat_item, which normalises both nesting layouts internally and works correctly — only the batched plural variant is broken, and the daemon emits the batched form for direct user messages.

Repro

data = json.loads(raw_event)
print('TOP KEYS:', list(data.keys()))                       # ['resp']
print('RESP TYPE:', data['resp']['type'])                   # 'newChatItems'
print('chatItems in TOP:', 'chatItems' in data)             # False
print('chatItems in RESP:', 'chatItems' in data['resp'])    # True

Fix

 elif resp_type == "newChatItems":
-    items = event.get("chatItems") or []
+    items = event.get("chatItems") or event.get("resp", {}).get("chatItems") or []
     for item_wrapper in items:
         await self._handle_new_chat_item(item_wrapper)

Bug 3: outbound send uses invalid command syntax

File: plugins/platforms/simplex/adapter.py, in both SimplexAdapter.send and the standalone_send helper

Current code

if chat_id.startswith("group:"):
    group_id = chat_id[6:]
    cmd_str = f"#[{group_id}] {content}"
else:
    cmd_str = f"@[{chat_id}] {content}"

(Same pattern in standalone_send with message instead of content.)

Why this fails

This produces commands like @[4] hello, which the daemon interprets as a message addressed to a literal contact named [4]. The daemon response:

{
  "resp": {
    "type": "chatCmdError",
    "chatError": {
      "type": "error",
      "errorType": {"type": "contactNotFound", "contactName": "[4]"}
    }
  }
}

The bracket syntax appears to be a custom convention that doesn't match either the user-facing form (@DisplayName text) or the internal API form (/_send @<id> text <body>).

Repro

Fix

Use the internal API form, which works with the numeric chat_id already in scope:

 if chat_id.startswith("group:"):
     group_id = chat_id[6:]
-    cmd_str = f"#[{group_id}] {content}"
+    cmd_str = f"/_send #{group_id} text {content}"
 else:
-    cmd_str = f"@[{chat_id}] {content}"
+    cmd_str = f"/_send @{chat_id} text {content}"

Apply the same change in standalone_send (replace content with message per that function's parameter name).

Combined effect

With all three patches applied, the full flow works for the first time:

  1. Phone sends "hello" via SimpleX
  2. Daemon receives, emits newChatItems event over WebSocket (now that Hermes subscribed with /_start)
  3. Hermes's _handle_event correctly extracts items from resp.chatItems and dispatches to _handle_new_chat_item
  4. Hermes generates a reply
  5. Hermes's send() issues /_send @4 text <reply> which the daemon accepts
  6. Phone receives the reply

Notes for maintainers

  • Bug 3's fix uses the internal API form (/_send @id text). There may be a preferred customer-facing form (e.g. resolving chat_id to a displayName and emitting @DisplayName text) — happy to adjust if there is a convention I am missing.
  • Bug 1's fix uses corrId="hermes-init", leveraging the existing _CORR_PREFIX echo-filtering.
  • All three patches are roughly one line each. Happy to open a PR with the diff if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix hermes-simplex-bugs-report