# NousResearch/hermes-agent · Feishu message loss and code block rendering fixes

## Open now

- Nothing open on GitHub.

A cluster of bugs and fixes related to Feishu (Lark) channel message content being dropped or lost, alongside fixes for how fenced code blocks are split and rendered in Feishu post payloads. Also includes streaming fixes to prevent final responses from being suppressed when commentary messages are sent.

## TL;DR

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

- #8341: \[Bug\]: Lost last content of Feishu Channel
- #10454: \[Bug\]: 飞书对话中最后一条消息偶尔会被吞掉

## Prior & competing PRs

- **#10540: fix(streaming): don't suppress final response when commentary message is sent**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/stream_consumer.py`
  - fix hunks:
  - `gateway/stream_consumer.py`

    ```diff
    @@ -609,12 +609,15 @@ async def _send_commentary(self, text: str) -> bool:
                     content=text,
                     metadata=self.metadata,
                 )
    -            if result.success:
    -                self._already_sent = True
    -                return True
    +            # Note: do NOT set _already_sent = True here.
    +            # Commentary messages are interim status updates (e.g. "Using browser
    +            # tool..."), not the final response. Setting already_sent would cause
    +            # the final response to be incorrectly suppressed when there are
    +            # multiple tool calls. See: https://github.com/NousResearch/hermes-agent/issues/10454
    +            return result.success
             except Exception as e:
                 logger.error("Commentary send error: %s", e)
    -        return False
    +            return False
     
         async def _send_or_edit(self, text: str) -> bool:
             """Send or edit the streaming message.
    ```
- **#12512: fix(feishu): split fenced code blocks into dedicated post rows**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/feishu.py`, `tests/gateway/test_feishu.py`
  - fix hunks:
  - `gateway/platforms/feishu.py`

    ```diff
    @@ -119,6 +119,8 @@
         re.MULTILINE,
     )
     _MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
    +_MARKDOWN_FENCE_OPEN_RE = re.compile(r"^```([^\n`]*)\s*$")
    +_MARKDOWN_FENCE_CLOSE_RE = re.compile(r"^```\s*$")
     _MENTION_RE = re.compile(r"@_user_\d+")
     _MULTISPACE_RE = re.compile(r"[ \t]{2,}")
     _POST_CONTENT_INVALID_RE = re.compile(r"content format of the post type is incorrect", re.IGNORECASE)
    ```

    ```diff
    @@ -430,23 +432,66 @@ def _coerce_required_int(value: Any, default: int, min_value: int = 0) -> int:
     
     
     def _build_markdown_post_payload(content: str) -> str:
    +    rows = _build_markdown_post_rows(content)
         return json.dumps(
             {
                 "zh_cn": {
    -                "content": [
    -                    [
    -                        {
    -                            "tag": "md",
    -                            "text": content,
    -                        }
    -                    ]
    -                ],
    +                "content": rows,
                 }
             },
             ensure_ascii=False,
         )
     
     
    +def _build_markdown_post_rows(content: str) -> List[List[Dict[str, str]]]:
    +    """Build Feishu post rows while isolating fenced code blocks.
    +
    +    Feishu's `md` renderer can swallow trailing content when a fenced code block
    +    appears inside one large markdown element. Split the reply at real fence
    +    lines so prose before/after the code block remains visible while code stays
    +    in a dedicated row.
    +    """
    +    if not content:
    +        return [[{"tag": "md", "text": ""}]]
    +    if "```" not in content:
    +        return [[{"tag": "md", "text": content}]]
    +
    +    rows: List[List[Dict[str, str]]] = []
    +    current: List[str] = []
    +    in_code_block = False
    +
    +    def _flush_current() -> None:
    +        nonlocal current
    +        if not current:
    +            return
    +        segment = "\n".join(current)
    +        if segment.strip():
    +            rows.append([{"tag": "md", "text": segment}])
    +        current = []
    +
    +    for raw_line in content.splitlines():
    +        stripped_line = raw_line.strip()
    +        is_fence = bool(
    +            _MARKDOWN_FENCE_CLOSE_RE.match(stripped_line)
    +            if in_code_block
    +            else _MARKDOWN_FENCE_OPEN_RE.match(stripped_line)
    +        )
    +
    +        if is_fence:
    +            if not in_code_block:
    +                _flush_current()
    +            current.append(raw_line)
    +            in_code_block = not in_code_block
    +            if not in_code_block:
    +                _flush_current()
    +            continue
    +
    +        current.append(raw_line)
    +
    +    _flush_current()
    +    return rows or [[{"tag": "md", "text": content}]]
    +
    +
     def parse_feishu_post_payload(payload: Any) -> FeishuPostParseResult:
         resolved = _resolve_post_payload(payload)
         if not resolved:
    ```
  - `tests/gateway/test_feishu.py`

    ```diff
    @@ -2370,6 +2370,134 @@ async def _direct(func, *args, **kwargs):
             elements = payload["zh_cn"]["content"][0]
             self.assertEqual(elements, [{"tag": "md", "text": "可以用 **粗体** 和 *斜体*。"}])
     
    +    @patch.dict(os.environ, {}, clear=True)
    +    def test_send_splits_fenced_code_blocks_into_separate_post_rows(self):
    +        from gateway.config import PlatformConfig
    +        from gateway.platforms.feishu import FeishuAdapter
    +
    +        adapter = FeishuAdapter(PlatformConfig())
    +        captured = {}
    +
    +        class _MessageAPI:
    +            def create(self, request):
    +                captured["request"] = request
    +                return SimpleNamespace(
    +                    success=lambda: True,
    +                    data=SimpleNamespace(message_id="om_codeblock"),
    +                )
    +
    +        adapter._client = SimpleNamespace(
    +            im=SimpleNamespace(
    +                v1=SimpleNamespace(
    +                    message=_MessageAPI(),
    +                )
    +            )
    +        )
    +
    +        async def _direct(func, *args, **kwargs):
    +            return func(*args, **kwargs)
    +
    +        content = (
    +            "确认已入库 ✓\n"
    +            "文件路径：`/root/.hermes/profiles/agent_cto/cron/jobs.json`\n"
    +            "**解码后的内容：**\n"
    +            "```json\n"
    +            '{"cron": "list"}\n'
    +            "```\n"
    +            "后续说明仍应保留。"
    +        )
    +
    +        with patch("gateway.platforms.feishu.asyncio.to_thread", side_effect=_direct):
    +            result = asyncio.run(
    +                adapter.send(
    +                    chat_id="oc_chat",
    +                    content=content,
    +                )
    +            )
    +
    +        self.assertTrue(result.success)
    +        self.assertEqual(captured["request"].request_body.msg_type, "post")
    +        payload = json.loads(captured["request"].request_body.content)
    +        rows = payload["zh_cn"]["content"]
    +        self.assertEqual(
    +            rows,
    +            [
    +                [
    +                    {
    +                        "tag": "md",
    +                        "text": "确认已入库 ✓\n文件路径：`/root/.hermes/profiles/agent_cto/cron/jobs.json`\n**解码后的内容：**",
    +                    }
    +                ],
    +                [{"tag": "md", "text": "```json\n{\"cron\": \"list\"}\n```"}],
    +                [{"tag": "md", "text": "后续说明仍应保留。"}],
    +            ],
    +        )
    +
    +    @patch.dict(os.environ, {}, clear=True)
    +    def test_build_post_payload_keeps_fence_like_code_lines_inside_code_block(self):
    +        from gateway.config import PlatformConfig
    +        from gateway.platforms.feishu import FeishuAdapter
    +
    +        adapter = FeishuAdapter(PlatformConfig())
    +        payload = json.loads(
    +            adapter._build_post_payload(
    +                "before\n```python\n```oops\n```\nafter"
    +            )
    +        )
    +
    +        self.assertEqual(
    +            payload["zh_cn"]["content"],
    +            [
    +                [{"tag": "md", "text": "before"}],
    +                [{"tag": "md", "text": "```python\n```oops\n```"}],
    +                [{"tag": "md", "text": "after"}],
    +            ],
    +        )
    +
    +    @patch.dict(os.environ, {}, clear=True)
    +    def test_build_post_payload_preserves_trailing_spaces_in_code_block(self):
    +        from gateway.config import PlatformConfig
    +        from gateway.platforms.feishu import FeishuAdapter
    +
    +        adapter = FeishuAdapter(PlatformConfig())
    +        payload = json.loads(
    +            adapter._build_post_payload(
    +                "before\n```python\nline with two spaces  \n```\nafter"
    +            )
    +        )
    +
    +        self.assertEqual(
    +            payload["zh_cn"]["content"],
    +            [
    +                [{"tag": "md", "text": "before"}],
    +                [{"tag": "md", "text": "```python\nline with two spaces  \n```"}],
    +                [{"tag": "md", "text": "after"}],
    +            ],
    +        )
    +
    +    @patch.dict(os.environ, {}, clear=True)
    +    def test_build_post_payload_splits_multiple_fenced_code_blocks(self):
    +        from gateway.config import PlatformConfig
    +        from gateway.platforms.feishu import FeishuAdapter
    +
    +        adapter = FeishuAdapter(PlatformConfig())
    +        payload = json.loads(
    +            adapter._build_post_payload(
    +                "before\n```python\nprint(1)\n```\nmiddle\n```json\n{}\n```\nafter"
    +            )
    +        )
    +
    +        self.assertEqual(
    +            payload["zh_cn"]["content"],
    +            [
    +                [{"tag": "md", "text": "before"}],
    +                [{"tag": "md", "text": "```python\nprint(1)\n```"}],
    +                [{"tag": "md", "text": "middle"}],
    +                [{"tag": "md", "text": "```json\n{}\n```"}],
    +                [{"tag": "md", "text": "after"}],
    +            ],
    +        )
    +
         @patch.dict(os.environ, {}, clear=True)
         def test_send_falls_back_to_text_when_post_payload_is_rejected(self):
             from gateway.config import PlatformConfig
    ```
- **#8361: fix(feishu): avoid post rendering for fenced code block replies**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#8638: fix(feishu): split fenced code blocks in post payload**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#12512`
- **#10519: fix(streaming): don't suppress final response when commentary message is sent**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#10530: fix(streaming): don't suppress final response when commentary message is sent**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#10540`

## Fix status

- Merged to main in PR #10540 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-19`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
