# NousResearch/hermes-agent · Telegram streaming message left frozen with cursor character

## Open now

- Nothing open on GitHub.

These items concern a streaming message in the gateway (reported for Telegram) that can stay frozen with a cursor (▉) when the final cursor-removal edit fails after a tool call. Two members are fix proposals that strip the cursor when the fallback continuation is empty, and one is the bug report; their outcomes are not stated in the titles.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #12472. Verify it addresses your case.
- **Recommended:** read PR #12472 (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: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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

- #7183: \[Bug\]: Telegram streaming message frozen with cursor (▉) when final cursor-removal edit fails after tool call

## Prior & competing PRs

- **#12472: fix(gateway): strip cursor from frozen message on empty fallback continuation (#7183)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/stream_consumer.py`, `tests/gateway/test_stream_consumer.py`
  - fix hunks:
  - `gateway/stream_consumer.py`

    ```diff
    @@ -571,6 +571,30 @@ async def _send_fallback_final(self, text: str) -> None:
                 if final_text.strip() and final_text != self._visible_prefix():
                     continuation = final_text
                 else:
    +                # Defence-in-depth for #7183: the last edit may still show the
    +                # cursor character because fallback mode was entered after an
    +                # edit failure left it stuck.  Try one final edit to strip it
    +                # so the message doesn't freeze with a visible ▉.  Best-effort
    +                # — if this edit also fails (flood control still active),
    +                # _try_strip_cursor has already been called on fallback entry
    +                # and the adaptive-backoff retries will have had their shot.
    +                if (
    +                    self._message_id
    +                    and self._last_sent_text
    +                    and self.cfg.cursor
    +                    and self._last_sent_text.endswith(self.cfg.cursor)
    +                ):
    +                    clean_text = self._last_sent_text[:-len(self.cfg.cursor)]
    +                    try:
    +                        result = await self.adapter.edit_message(
    +                            chat_id=self.chat_id,
    +                            message_id=self._message_id,
    +                            content=clean_text,
    +                        )
    +                        if result.success:
    +                            self._last_sent_text = clean_text
    +                    except Exception:
    +                        pass
                     self._already_sent = True
                     self._final_response_sent = True
                     return
    ```
  - `tests/gateway/test_stream_consumer.py`

    ```diff
    @@ -1216,3 +1216,87 @@ async def test_default_mode_still_triggers_intermediate_edits(self):
             # text, the consumer may send then edit, or just send once at got_done.
             # The key assertion: this doesn't break.
             assert adapter.send.call_count >= 1
    +
    +
    +# ── Cursor stripping on fallback (#7183) ────────────────────────────────────
    +
    +
    +class TestCursorStrippingOnFallback:
    +    """Regression: cursor must be stripped when fallback continuation is empty (#7183).
    +
    +    When _send_fallback_final is called with nothing new to deliver (the visible
    +    partial already matches final_text), the last edit may still show the cursor
    +    character because fallback mode was entered after a failed edit.  Before the
    +    fix this would leave the message permanently frozen with a visible ▉.
    +    """
    +
    +    @pytest.mark.asyncio
    +    async def test_cursor_stripped_when_continuation_empty(self):
    +        """_send_fallback_final must attempt a final edit to strip the cursor."""
    +        adapter = MagicMock()
    +        adapter.MAX_MESSAGE_LENGTH = 4096
    +        adapter.edit_message = AsyncMock(
    +            return_value=SimpleNamespace(success=True, message_id="msg-1")
    +        )
    +
    +        consumer = GatewayStreamConsumer(
    +            adapter, "chat-1",
    +            config=StreamConsumerConfig(cursor=" ▉"),
    +        )
    +        consumer._message_id = "msg-1"
    +        consumer._last_sent_text = "Hello world ▉"
    +        consumer._fallback_final_send = False
    +
    +        await consumer._send_fallback_final("Hello world")
    +
    +        adapter.edit_message.assert_called_once()
    +        call_args = adapter.edit_message.call_args
    +        assert call_args.kwargs["content"] == "Hello world"
    +        assert consumer._already_sent is True
    +        # _last_sent_text should reflect the cleaned text after a successful strip
    +        assert consumer._last_sent_text == "Hello world"
    +
    +    @pytest.mark.asyncio
    +    async def test_cursor_not_stripped_when_no_cursor_configured(self):
    +        """No edit attempted when cursor is not configured."""
    +        adapter = MagicMock()
    +        adapter.MAX_MESSAGE_LENGTH = 4096
    +        adapter.edit_message = AsyncMock()
    +
    +        consumer = GatewayStreamConsumer(
    +            adapter, "chat-1",
    +            config=StreamConsumerConfig(cursor=""),
    +        )
    +        consumer._message_id = "msg-1"
    +        consumer._last_sent_text = "Hello world"
    +        consumer._fallback_final_send = False
    +
    +        await consumer._send_fallback_final("Hello world")
    +
    +        adapter.edit_message.assert_not_called()
    +        assert consumer._already_sent is True
    +
    +    @pytest.mark.asyncio
    +    async def test_cursor_strip_edit_failure_handled(self):
    +        """If the cursor-stripping edit itself fails, it must not crash and
    +        must not corrupt _last_sent_text."""
    +        adapter = MagicMock()
    +        adapter.MAX_MESSAGE_LENGTH = 4096
    +        adapter.edit_message = AsyncMock(
    +            return_value=SimpleNamespace(success=False, error="flood_control")
    +        )
    +
    +        consumer = GatewayStreamConsumer(
    +            adapter, "chat-1",
    +            config=StreamConsumerConfig(cursor=" ▉"),
    +        )
    +        consumer._message_id = "msg-1"
    +        consumer._last_sent_text = "Hello ▉"
    +        consumer._fallback_final_send = False
    +
    +        await consumer._send_fallback_final("Hello")
    +
    +        # Should still set already_sent despite the cursor-strip edit failure
    +        assert consumer._already_sent is True
    +        # _last_sent_text must NOT be updated when the edit failed
    +        assert consumer._last_sent_text == "Hello ▉"
    ```
- **#7429: fix(gateway): strip cursor from frozen message when fallback continuation is empty**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#12472`, `#10807`

## Fix status

- Merged to main in PR #12472 (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.
