# NousResearch/hermes-agent · Matrix cron delivery missing HTML/markdown formatting

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #38649. Verify it addresses your case.
- **Recommended:** read PR #38649 (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-38649`. 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.34`: 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.

## Closed / duplicate issues

- #5233: \[Bug\]: Cron delivery to Matrix sends plain text — no formatted_body or markdown rendering
- #32486: \[Bug\]: Default Docker image missing \`markdown\` dep — cron delivery silently falls back to plain text despite PR #5271

## Prior & competing PRs

- **#38649: fix(deps): promote markdown to a core dependency so rich delivery works out of the box (#32486)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/matrix.py`, `pyproject.toml`, `tools/lazy_deps.py`
  - fix hunks:
  - `gateway/platforms/matrix.py`

    ```diff
    @@ -2799,11 +2799,11 @@ def _mxc_to_http(self, mxc_url: str) -> str:
         def _markdown_to_html(self, text: str) -> str:
             """Convert Markdown to Matrix-compatible HTML (org.matrix.custom.html).
     
    -        Uses the ``markdown`` library when available (installed with the
    -        ``matrix`` extra).  Falls back to a comprehensive regex converter
    -        that handles fenced code blocks, inline code, headers, bold,
    -        italic, strikethrough, links, blockquotes, lists, and horizontal
    -        rules — everything the Matrix HTML spec allows.
    +        Uses the ``markdown`` library (a core dependency) when available.
    +        Falls back to a comprehensive regex converter that handles fenced
    +        code blocks, inline code, headers, bold, italic, strikethrough,
    +        links, blockquotes, lists, and horizontal rules — everything the
    +        Matrix HTML spec allows.
             """
             try:
                 import markdown as _md
    ```
  - `pyproject.toml`

    ```diff
    @@ -61,6 +61,16 @@ dependencies = [
       "prompt_toolkit==3.0.52",
       # Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter).
       "croniter==6.0.0",
    +  # Markdown -> HTML conversion for rich message delivery (Matrix
    +  # `formatted_body`, and the `send_message` tool's HTML path). Now on the
    +  # DEFAULT delivery path, not matrix-specific: without it both
    +  # gateway/platforms/matrix.py and tools/send_message_tool.py silently fall
    +  # back to plain text, so cron/agent deliveries render raw `##`/`**`/tables
    +  # in clients like Element (see #32486). Pure-Python py3-none-any wheel
    +  # (~108KB, no compiled extensions, no platform constraints), so unlike the
    +  # matrix extra's `mautrix`/`python-olm` it's safe to ship everywhere — keeps
    +  # it out of the lazy-install path that exists only for the heavy matrix deps.
    +  "Markdown==3.10.2",
       # Skills Hub (GitHub App JWT auth — optional, only needed for bot identity)
       "PyJWT[crypto]==2.12.1",  # CVE-2026-32597
       # Windows has no IANA tzdata shipped with the OS, so Python's ``zoneinfo``
    ```

    ```diff
    @@ -104,7 +114,7 @@ dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-time
     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"]
     cron = []  # croniter is now a core dependency; this extra kept for back-compat
     slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"]
    -matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
    +matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
     # WeCom callback-mode adapter — parses untrusted XML POST bodies from
     # WeCom-controlled callback endpoints, so we use defusedxml (drop-in
     # replacement for stdlib xml.etree.ElementTree) to block billion-laughs
    ```
  - `tools/lazy_deps.py`

    ```diff
    @@ -135,7 +135,6 @@
         ),
         "platform.matrix": (
             "mautrix[encryption]==0.21.0",
    -        "Markdown==3.10.2",
             "aiosqlite==0.22.1",
             "asyncpg==0.31.0",
             "aiohttp-socks==0.11.0",
    ```
  - `uv.lock`

    ```diff
    @@ -1398,6 +1398,7 @@ dependencies = [
         { name = "fire" },
         { name = "httpx", extra = ["socks"] },
         { name = "jinja2" },
    +    { name = "markdown" },
         { name = "openai" },
         { name = "pathspec" },
         { name = "prompt-toolkit" },
    ```

    ```diff
    @@ -1509,7 +1510,6 @@ matrix = [
         { name = "aiohttp-socks" },
         { name = "aiosqlite" },
         { name = "asyncpg" },
    -    { name = "markdown" },
         { name = "mautrix", extra = ["encryption"] },
     ]
     mcp = [
    ```

    ```diff
    @@ -1647,7 +1647,7 @@ requires-dist = [
         { name = "httpx", extras = ["socks"], specifier = "==0.28.1" },
         { name = "jinja2", specifier = "==3.1.6" },
         { name = "lark-oapi", marker = "extra == 'feishu'", specifier = "==1.5.3" },
    -    { name = "markdown", marker = "extra == 'matrix'", specifier = "==3.10.2" },
    +    { name = "markdown", specifier = "==3.10.2" },
         { name = "mautrix", extras = ["encryption"], marker = "extra == 'matrix'", specifier = "==0.21.0" },
         { name = "mcp", marker = "extra == 'computer-use'", specifier = "==1.26.0" },
         { name = "mcp", marker = "extra == 'dev'", specifier = "==1.26.0" },
    ```
- **#5236: fix(matrix): add formatted_body with HTML rendering for cron deliveries**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#5271`
- **#32492: fix(tools): regex-fallback Matrix formatted_body when markdown lib missing**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`

## Fix status

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