hermes - ✅(Solved) Fix fix(tui): SGR mouse sequence leakage into composer under heavy render in tmux [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#18658Fetched 2026-05-03 04:55:09
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×3cross-referenced ×1

Root Cause

Root cause

Fix Action

Fixed

PR fix notes

PR #18695: fix(tui): drop corrupted SGR mouse fragments that escape existing recovery

Description (problem / solution / changelog)

Problem

Fixes #18658.

Under heavy render load in tmux with mouse tracking enabled, SGR mouse escape sequences leak into the chat composer as visible garbage text (e.g. 5;34M;34M35;16;35M...).

The existing recovery in parseTextWithSgrMouseFragments() (commits 71b685aee, ded011c5a) handles fragments with explicit [< or < prefixes and multi-fragment bursts, but fails when:

  1. Extreme concatenation produces adjacent fragments without separators
  2. Multiple M terminators appear in a row from corrupted/overlapping writes

Solution

Add an early-exit check in parseTextWithSgrMouseFragments(): if the entire text consists exclusively of SGR-mouse-fragment characters ([0-9;Mm]) AND contains the event-boundary signature (\d[Mm]\d — one event ending, another starting), silently discard it by returning an empty array.

This is safe because real user-typed text will never consist entirely of digits, semicolons, and M/m characters while also containing the boundary pattern. The negative lookahead (?!.*\d{4}) ensures strings with 4+ digit runs (which can't be valid mouse coords 0-255) pass through normally.

Changes

  • parse-keypress.ts: Add CORRUPTED_SGR_FRAGMENT_RE constant and early-exit guard in parseTextWithSgrMouseFragments()
  • parse-keypress.test.ts: 3 new test cases covering concatenated fragments, corrupted fragments, and mixed-text preservation

Testing

All 19 tests in parse-keypress.test.ts pass via vitest run.

Changed files

  • ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts (modified, +18/-0)
  • ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts (modified, +5/-0)

Code Example

5;34M;34M35;16;35M35;18;35M36M35;23;36M25;36M6M;29;37M5;32;37M5;...

---

stdin chunk → createTokenizer({x10Mouse: true}) → tokens
for each 'text' token:
      parseTextWithSgrMouseFragments(token.value)  // tries to recover
        if recovery fails: parseKeypress(token.value)  // falls through
default key with name="" → leaks into input

// App.tsx flush timer:
incompleteEscapeTimer → tokenizer.flush()
  → splits ESC from continuation
ESC'escape' key event
'[<...M' → tries parseTextWithSgrMouseFragments
RAW_BUFFERClick to expand / collapse

Describe the bug

Under heavy render load in hermes --tui (especially inside tmux with mouse tracking enabled), SGR mouse escape sequences leak into the chat composer as visible garbage text. The composer fills with patterns like:

5;34M;34M35;16;35M35;18;35M36M35;23;36M25;36M6M;29;37M5;32;37M5;...

This is NOT model output — it's terminal mouse-report leakage.

Steps to reproduce

  1. Start Hermes TUI inside tmux: hermes --tui
  2. Ensure mouse tracking is on (default: display.tui_mouse: true)
  3. Have a large session (200k+ tokens) so rendering is heavy
  4. Move the mouse across the terminal rapidly for several seconds
  5. Observe that the composer () fills with numeric garbage matching digit;digit;digitM patterns

Root cause

The mechanism

  1. Hermes activates DECSET 1000+1002+1003+1006 (SGR mouse tracking) via ENABLE_MOUSE_TRACKING in packages/hermes-ink/src/ink/termio/dec.ts
  2. The terminal responds with SGR mouse reports: \x1b[<btn;col;rowM (press) and \x1b[<btn;col;rowm (release)
  3. Under heavy render (React reconciler busy), stdin is not read promptly
  4. Mouse events accumulate in the OS tty buffer
  5. The 50ms incomplete-escape flush timer in App.tsx (line ~138, NORMAL_TIMEOUT = 50) fires
  6. The \x1b prefix is flushed as a lone ESC key event (via input-event.ts)
  7. The remaining [<btn;col;rowM arrives as text on the next chunk
  8. parseTextWithSgrMouseFragments() in parse-keypress.ts attempts to recover these fragments via SGR_MOUSE_FRAGMENT_RE

Why the current fix is incomplete

The existing fix (commits 71b685aee and ded011c5a) added SGR_MOUSE_FRAGMENT_RE and parseTextWithSgrMouseFragments() to catch orphaned SGR fragments. This works for simple cases but fails when:

  1. Extreme concatenation: Under sustained render backpressure, dozens of mouse events pile up. Fragments concatenate without separators, and the regex can't always match overlapping/adjacent fragments correctly.

  2. Corrupted fragments: When multiple writes to the composer buffer overlap during re-entry to the event loop, partial fragments get interleaved, producing patterns that don't match any regex (e.g., 37M37M6; — two M final bytes in a row).

Code path

The critical path is in parse-keypress.ts:

stdin chunk → createTokenizer({x10Mouse: true}) → tokens
  → for each 'text' token:
      parseTextWithSgrMouseFragments(token.value)  // tries to recover
        if recovery fails: parseKeypress(token.value)  // falls through
          → default key with name="" → leaks into input

// App.tsx flush timer:
incompleteEscapeTimer → tokenizer.flush()
  → splits ESC from continuation
  → ESC → 'escape' key event
  → '[<...M' → tries parseTextWithSgrMouseFragments

Expected behavior

SGR mouse events should never appear as visible text in the composer, even under extreme render load and tmux.

Suggested improvements

  1. Buffer-escaping: If parseTextWithSgrMouseFragments can't recover, strip the pattern rather than letting it leak as visible text (whitelist approach: if text looks like a corrupted SGR mouse fragment, drop it silently)

  2. Staggered flush: Instead of always flushing at exactly 50ms, push the timer back up to ~200ms when CSI state is active and stdin data is arriving rapidly (indicates mouse tracking is active)

  3. Rate-limit mouse tracking re-assertion: The onStdinResume handler re-enables mouse tracking. Consider adding a cooldown or detecting the leak and temporarily disabling mouse tracking when fragments are detected

  4. Disambiguate flush vs keypress ESC: When the flush timer fires with a buffered ESC where the next byte would start a CSI sequence, emit a synthetic "mouse leak detected" event instead of an ESC keypress

Environment

  • OS: Linux (WSL2 Ubuntu)
  • Terminal: tmux with mouse on
  • Hermes version: f98b5d00a (main)
  • Session size at time of leak: ~271k tokens

extent analysis

TL;DR

Implement a combination of buffer-escaping, staggered flush, and rate-limiting mouse tracking re-assertion to prevent SGR mouse events from leaking into the composer as visible text.

Guidance

  1. Buffer-escaping: Modify parseTextWithSgrMouseFragments to strip patterns that resemble corrupted SGR mouse fragments instead of letting them leak as visible text.
  2. Staggered flush: Adjust the NORMAL_TIMEOUT in App.tsx to push the timer back up to ~200ms when CSI state is active and stdin data is arriving rapidly.
  3. Rate-limit mouse tracking re-assertion: Introduce a cooldown or detection mechanism in the onStdinResume handler to temporarily disable mouse tracking when fragments are detected.
  4. Disambiguate flush vs keypress ESC: Consider emitting a synthetic "mouse leak detected" event instead of an ESC keypress when the flush timer fires with a buffered ESC where the next byte would start a CSI sequence.

Example

// Example of buffer-escaping in parseTextWithSgrMouseFragments
function parseTextWithSgrMouseFragments(tokenValue) {
  const sgrMouseFragmentRe = /\d+;?\d*M/g;
  const match = tokenValue.match(sgrMouseFragmentRe);
  if (match) {
    // Strip the pattern instead of letting it leak as visible text
    return tokenValue.replace(sgrMouseFragmentRe, '');
  }
  // ...
}

Notes

The suggested improvements may require additional testing and fine-tuning to ensure they do not introduce new issues or affect the overall performance of the application.

Recommendation

Apply the suggested improvements, starting with buffer-escaping and staggered flush, to mitigate the SGR mouse event leakage issue. This approach should help prevent the leakage of mouse events into the composer as visible text, even under extreme render load and tmux.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

SGR mouse events should never appear as visible text in the composer, even under extreme render load and tmux.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix fix(tui): SGR mouse sequence leakage into composer under heavy render in tmux [1 pull requests, 1 participants]