Skip to content

fix(stream_consumer): always strip cursor on stream end — never get stuck#7426

Closed
dangelo352 wants to merge 3 commits into
NousResearch:mainfrom
dangelo352:fix/stream-consumer-never-stuck
Closed

fix(stream_consumer): always strip cursor on stream end — never get stuck#7426
dangelo352 wants to merge 3 commits into
NousResearch:mainfrom
dangelo352:fix/stream-consumer-never-stuck

Conversation

@dangelo352

Copy link
Copy Markdown
Contributor

Summary

Fix: the streaming message in Telegram gets stuck with the cursor never being removed.

Root cause: When the stream ends, got_done only flushed if _accumulated was non-empty. If no new text arrived after the last progressive edit (which appended the cursor), _accumulated was empty and the cursor stayed stuck on the Telegram message forever.

Fix:

  1. _last_edit_had_cursor bool is set after every successful edit/send — tracks whether the last edit appended the cursor
  2. When stream ends with empty _accumulated but _last_edit_had_cursor is True, do one final edit to strip the cursor from _last_sent_text

That's it. No other changes.

Test Plan

  • All 25 existing stream consumer tests pass
  • Manual Telegram test: stream that ends with cursor visible

Closes: streaming message never stops / cursor stuck forever

…tuck

When the stream ends, the final flush only ran if _accumulated was
non-empty.  If no new text arrived after the last progressive edit
(with cursor appended), _accumulated was empty and the cursor was left
stuck on the Telegram message forever.

Fix: track _last_edit_had_cursor after every successful edit/send.
When the stream ends with _accumulated empty but a stuck cursor,
do one final edit to strip it.

Root cause:
  - Progressive edit sends 'Hello...' with cursor
  - Stream ends before next token arrives
  - got_done: if self._accumulated → empty → skip final flush
  - Cursor stays on message forever

Fix:
  - Add _last_edit_had_cursor bool, set after every successful edit
  - got_done handler now has elif branch: when _accumulated empty
    but _last_edit_had_cursor, strip cursor from _last_sent_text
    and do one final clean edit
… chat

The model's stream can include terminal spinner characters (▍ ▉ █ ░),
box-drawing chars, and ANSI escape sequences that render as black
rectangles or garbage in Telegram/Discord.  Extend _clean_for_display
to strip:

- Block/quarter-block chars (U+2588..U+258F, U+2591..U+2595)
- Box-drawing chars (U+2500..U+257F)
- Spinner/progress glyphs (U+25B6..U+25CF, U+2600..U+266F)
- Braille patterns (U+2800..U+28FF)
- ANSI escape sequences (colors, cursor moves, clears)
- Zero-width/control chars (U+200B..U+200F, U+FEFF, etc.)

Also update test that now correctly expects cursor chars stripped
from all outgoing messages.
…reeze

When context compaction fires during a streamed conversation, the
compression LLM call would edit the same Telegram message as the
agent's response.  After compaction completed, the stream consumer
still held state pointing at the old message, causing subsequent
edits to fail and the chat to appear "stuck" or never finish.

Fix: after compression, reset the stream consumer's accumulated text,
message ID, and fallback state.  The next API response streams to a
fresh Telegram message instead of fighting over the old one.

Also wire _stream_consumer_instance from the gateway's stream_consumer
holder into the agent so _compress_context can access it.
kshitijk4poor pushed a commit to kshitijk4poor/hermes-agent that referenced this pull request Apr 11, 2026
…reeze

When context compaction fires during a streamed conversation, the
compression LLM call would edit the same Telegram message as the
agent's response.  After compaction completed, the stream consumer
still held state pointing at the old message, causing subsequent
edits to fail and the chat to appear "stuck" or never finish.

Fix: after compression, reset the stream consumer's accumulated text,
message ID, and fallback state.  The next API response streams to a
fresh Telegram message instead of fighting over the old one.

Also wire _stream_consumer_instance from the gateway's stream_consumer
holder into the agent so _compress_context can access it.

Cherry-picked from PR NousResearch#7426 by @dangelo352.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the report and for digging into this, D'Angelo — the symptom you identified (agent appearing to stop/freeze mid-session on Telegram) was real and affecting multiple users.

The root cause turned out to be different from what the three commits here targeted. The stream consumer state, cursor tracking, and compaction reset paths are all actually correct on current main (we traced every code path in detail).

The actual bug was in the gateway's response delivery: when the stream consumer had sent at least one message (already_sent=True), the gateway skipped sending the final response to avoid duplicates. But this also silently swallowed error messages when the agent failed mid-loop — rate limit exhaustion, context overflow, inactivity timeout, etc. The user would see the last streamed content and then nothing. No error, no explanation.

This is gateway-only (never CLI) because CLI has no already_sent dedup — every response/error prints directly to stdout.

Fixed in PR #7652. Your report helped us find it — appreciate it.

@teknium1 teknium1 closed this Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants