NousResearch/hermes-agent

Telegram streaming message left frozen with cursor character

Cluster 7183. Data as of

Nothing open on GitHub. Fix merged in #12472 on 19 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #12472. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #12472 fix(gateway): strip cursor from frozen message on empty fallback continuation (#7183)

    merged to main

    files: gateway/stream_consumer.py, tests/gateway/test_stream_consumer.py

    Show the diff
    • gateway/stream_consumer.py
      @@ -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
      @@ -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 ▉"

1 PR closed without merging

From the close comments: done elsewhere 1

  • #7429 fix(gateway): strip cursor from frozen message when fallback continuation is empty · done elsewhere → #12472, #10807
Closed issues · 1
  • Closed#7183[Bug]: Telegram streaming message frozen with cursor (▉) when final cursor-removal edit fails after tool call
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#12472
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
19 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.