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
- Start Hermes TUI inside tmux:
hermes --tui
- Ensure mouse tracking is on (default:
display.tui_mouse: true)
- Have a large session (200k+ tokens) so rendering is heavy
- Move the mouse across the terminal rapidly for several seconds
- Observe that the composer (
❯) fills with numeric garbage matching digit;digit;digitM patterns
Root cause
The mechanism
- Hermes activates DECSET 1000+1002+1003+1006 (SGR mouse tracking) via
ENABLE_MOUSE_TRACKING in packages/hermes-ink/src/ink/termio/dec.ts
- The terminal responds with SGR mouse reports:
\x1b[<btn;col;rowM (press) and \x1b[<btn;col;rowm (release)
- Under heavy render (React reconciler busy), stdin is not read promptly
- Mouse events accumulate in the OS tty buffer
- The 50ms incomplete-escape flush timer in
App.tsx (line ~138, NORMAL_TIMEOUT = 50) fires
- The
\x1b prefix is flushed as a lone ESC key event (via input-event.ts)
- The remaining
[<btn;col;rowM arrives as text on the next chunk
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:
-
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.
-
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
-
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)
-
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)
-
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
-
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
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:This is NOT model output — it's terminal mouse-report leakage.
Steps to reproduce
hermes --tuidisplay.tui_mouse: true)❯) fills with numeric garbage matchingdigit;digit;digitMpatternsRoot cause
The mechanism
ENABLE_MOUSE_TRACKINGinpackages/hermes-ink/src/ink/termio/dec.ts\x1b[<btn;col;rowM(press) and\x1b[<btn;col;rowm(release)App.tsx(line ~138,NORMAL_TIMEOUT = 50) fires\x1bprefix is flushed as a loneESCkey event (viainput-event.ts)[<btn;col;rowMarrives as text on the next chunkparseTextWithSgrMouseFragments()inparse-keypress.tsattempts to recover these fragments viaSGR_MOUSE_FRAGMENT_REWhy the current fix is incomplete
The existing fix (commits
71b685aeeandded011c5a) addedSGR_MOUSE_FRAGMENT_REandparseTextWithSgrMouseFragments()to catch orphaned SGR fragments. This works for simple cases but fails when: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.
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;— twoMfinal bytes in a row).Code path
The critical path is in
parse-keypress.ts:Expected behavior
SGR mouse events should never appear as visible text in the composer, even under extreme render load and tmux.
Suggested improvements
Buffer-escaping: If
parseTextWithSgrMouseFragmentscan'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)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)
Rate-limit mouse tracking re-assertion: The
onStdinResumehandler re-enables mouse tracking. Consider adding a cooldown or detecting the leak and temporarily disabling mouse tracking when fragments are detectedDisambiguate 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
mouse onf98b5d00a(main)