{"repo":"NousResearch/hermes-agent","public_id":"5233","display_label":"Matrix cron delivery missing HTML/markdown formatting","summary":"A cluster of bug reports and fixes addressing Matrix notifications sent as plain text without formatted_body or markdown rendering, traced to a missing markdown dependency in the default Docker image.","subsystem":"cron","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":38649},"resolution":{"state":"partially_resolved","by":"pr-38649"},"freshness":{"state":"verified","last_verified_at":"2026-06-04T23:46:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":32486,"pr_number":38649,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":5233,"title":"[Bug]: Cron delivery to Matrix sends plain text — no formatted_body or markdown rendering","url":null,"state":"closed"},{"kind":"issue","number":32486,"title":"[Bug]: Default Docker image missing `markdown` dep — cron delivery silently falls back to plain text despite PR #5271","url":null,"state":"closed"},{"kind":"pr","number":5236,"title":"fix(matrix): add formatted_body with HTML rendering for cron deliveries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5271","number":5271}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":32492,"title":"fix(tools): regex-fallback Matrix formatted_body when markdown lib missing","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":38649,"title":"fix(deps): promote markdown to a core dependency so rich delivery works out of the box (#32486)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/matrix.py","pyproject.toml","tools/lazy_deps.py"],"fix_hunks":[{"file":"gateway/platforms/matrix.py","hunks":["@@ -2799,11 +2799,11 @@ def _mxc_to_http(self, mxc_url: str) -> str:\n     def _markdown_to_html(self, text: str) -> str:\n         \"\"\"Convert Markdown to Matrix-compatible HTML (org.matrix.custom.html).\n \n-        Uses the ``markdown`` library when available (installed with the\n-        ``matrix`` extra).  Falls back to a comprehensive regex converter\n-        that handles fenced code blocks, inline code, headers, bold,\n-        italic, strikethrough, links, blockquotes, lists, and horizontal\n-        rules — everything the Matrix HTML spec allows.\n+        Uses the ``markdown`` library (a core dependency) when available.\n+        Falls back to a comprehensive regex converter that handles fenced\n+        code blocks, inline code, headers, bold, italic, strikethrough,\n+        links, blockquotes, lists, and horizontal rules — everything the\n+        Matrix HTML spec allows.\n         \"\"\"\n         try:\n             import markdown as _md"]},{"file":"pyproject.toml","hunks":["@@ -61,6 +61,16 @@ dependencies = [\n   \"prompt_toolkit==3.0.52\",\n   # Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).\n   \"croniter==6.0.0\",\n+  # Markdown -> HTML conversion for rich message delivery (Matrix\n+  # `formatted_body`, and the `send_message` tool's HTML path). Now on the\n+  # DEFAULT delivery path, not matrix-specific: without it both\n+  # gateway/platforms/matrix.py and tools/send_message_tool.py silently fall\n+  # back to plain text, so cron/agent deliveries render raw `##`/`**`/tables\n+  # in clients like Element (see #32486). Pure-Python py3-none-any wheel\n+  # (~108KB, no compiled extensions, no platform constraints), so unlike the\n+  # matrix extra's `mautrix`/`python-olm` it's safe to ship everywhere — keeps\n+  # it out of the lazy-install path that exists only for the heavy matrix deps.\n+  \"Markdown==3.10.2\",\n   # Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)\n   \"PyJWT[crypto]==2.12.1\",  # CVE-2026-32597\n   # Windows has no IANA tzdata shipped with the OS, so Python's ``zoneinfo``","@@ -104,7 +114,7 @@ dev = [\"debugpy==1.8.20\", \"pytest==9.0.2\", \"pytest-asyncio==1.3.0\", \"pytest-time\n messaging = [\"python-telegram-bot[webhooks]==22.6\", \"discord.py[voice]==2.7.1\", \"aiohttp==3.13.3\", \"brotlicffi==1.2.0.1\", \"slack-bolt==1.27.0\", \"slack-sdk==3.40.1\", \"qrcode==7.4.2\"]\n cron = []  # croniter is now a core dependency; this extra kept for back-compat\n slack = [\"slack-bolt==1.27.0\", \"slack-sdk==3.40.1\", \"aiohttp==3.13.3\"]\n-matrix = [\"mautrix[encryption]==0.21.0\", \"Markdown==3.10.2\", \"aiosqlite==0.22.1\", \"asyncpg==0.31.0\", \"aiohttp-socks==0.11.0\"]\n+matrix = [\"mautrix[encryption]==0.21.0\", \"aiosqlite==0.22.1\", \"asyncpg==0.31.0\", \"aiohttp-socks==0.11.0\"]\n # WeCom callback-mode adapter — parses untrusted XML POST bodies from\n # WeCom-controlled callback endpoints, so we use defusedxml (drop-in\n # replacement for stdlib xml.etree.ElementTree) to block billion-laughs"]},{"file":"tools/lazy_deps.py","hunks":["@@ -135,7 +135,6 @@\n     ),\n     \"platform.matrix\": (\n         \"mautrix[encryption]==0.21.0\",\n-        \"Markdown==3.10.2\",\n         \"aiosqlite==0.22.1\",\n         \"asyncpg==0.31.0\",\n         \"aiohttp-socks==0.11.0\","]},{"file":"uv.lock","hunks":["@@ -1398,6 +1398,7 @@ dependencies = [\n     { name = \"fire\" },\n     { name = \"httpx\", extra = [\"socks\"] },\n     { name = \"jinja2\" },\n+    { name = \"markdown\" },\n     { name = \"openai\" },\n     { name = \"pathspec\" },\n     { name = \"prompt-toolkit\" },","@@ -1509,7 +1510,6 @@ matrix = [\n     { name = \"aiohttp-socks\" },\n     { name = \"aiosqlite\" },\n     { name = \"asyncpg\" },\n-    { name = \"markdown\" },\n     { name = \"mautrix\", extra = [\"encryption\"] },\n ]\n mcp = [","@@ -1647,7 +1647,7 @@ requires-dist = [\n     { name = \"httpx\", extras = [\"socks\"], specifier = \"==0.28.1\" },\n     { name = \"jinja2\", specifier = \"==3.1.6\" },\n     { name = \"lark-oapi\", marker = \"extra == 'feishu'\", specifier = \"==1.5.3\" },\n-    { name = \"markdown\", marker = \"extra == 'matrix'\", specifier = \"==3.10.2\" },\n+    { name = \"markdown\", specifier = \"==3.10.2\" },\n     { name = \"mautrix\", extras = [\"encryption\"], marker = \"extra == 'matrix'\", specifier = \"==0.21.0\" },\n     { name = \"mcp\", marker = \"extra == 'computer-use'\", specifier = \"==1.26.0\" },\n     { name = \"mcp\", marker = \"extra == 'dev'\", specifier = \"==1.26.0\" },\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}