Skip to content

fix(cli): recover leaked mouse tracking terminal state - #17701

Merged
OutThisLife merged 4 commits into
mainfrom
bb/mouse-mode-self-heal
Apr 30, 2026
Merged

fix(cli): recover leaked mouse tracking terminal state#17701
OutThisLife merged 4 commits into
mainfrom
bb/mouse-mode-self-heal

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • reset sticky terminal modes (?1006/?1003/?1002/?1000, focus, paste, alt-screen, Kitty keyboard, modifyOtherKeys) when the TUI starts and again on graceful signal/OOM shutdown
  • expand CLI leaked-terminal sanitizer to strip SGR mouse report fragments (ESC[<...M/m), including visible and bare degraded forms, and recover terminal modes in-place if they are observed
  • add regression coverage for escaped, visible, bare, and concatenated mouse-report leak forms plus TUI reset coverage

Test plan

  • scripts/run_tests.sh tests/cli/test_cli_terminal_response_sanitizer.py
  • scripts/run_tests.sh tests/cli/test_cli_bracketed_paste_sanitizer.py tests/cli/test_cli_terminal_response_sanitizer.py
  • npm test -- --run src/__tests__/terminalModes.test.ts
  • npm run type-check

Detect leaked SGR mouse-report fragments in CLI input, strip them, and reset terminal modes in-place so scroll and typing recover without reopening the tab. Add regression tests for escaped, visible, and bare leak forms.
Reset sticky mouse/focus/paste terminal modes before the TUI starts and during graceful shutdown paths so stale tab state from prior crashes cannot poison the next session.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the CLI’s defensive input sanitization to handle leaked SGR mouse-report fragments and adds a best-effort terminal mode recovery when those leaks are detected, backed by regression tests.

Changes:

  • Add SGR mouse report stripping (ESC / visible ^[[ / bare <...M/m fragments) to the leaked-terminal response sanitizer.
  • Introduce a “with meta” sanitizer variant returning (cleaned_text, had_mouse_reports) and trigger an in-place terminal mode reset when mouse leaks are detected.
  • Add regression tests covering escaped, visible, bare, and concatenated mouse-report leak forms while preserving normal angle-bracket text.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

File Description
cli.py Adds regexes for SGR mouse leaks, returns meta from sanitizer, and performs terminal input-mode recovery when leaks are detected.
tests/cli/test_cli_terminal_response_sanitizer.py Adds test coverage for stripping multiple forms of leaked SGR mouse-report fragments without over-matching normal <...> text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cli.py Outdated
Comment thread cli.py Outdated
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Apr 30, 2026
@OutThisLife
OutThisLife requested a review from Copilot April 30, 2026 03:09
Handle unbounded SGR mouse report coordinates and avoid regex work on ordinary prompt-buffer edits by short-circuiting before sanitizer passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@OutThisLife
OutThisLife requested a review from Copilot April 30, 2026 17:07
@OutThisLife
OutThisLife merged commit 285e9ef into main Apr 30, 2026
14 checks passed
@OutThisLife
OutThisLife deleted the bb/mouse-mode-self-heal branch April 30, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cli.py
Comment on lines +1583 to +1598
if not any(marker in text for marker in _TERMINAL_RESPONSE_SENTINELS):
return text, False

had_mouse_reports = False

if "\x1b[" in text:
text = _DSR_CPR_ESC_RE.sub("", text)
text, count = _SGR_MOUSE_ESC_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0

if "^[" in text:
text = _DSR_CPR_VISIBLE_RE.sub("", text)
text, count = _SGR_MOUSE_VISIBLE_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0

if "<" in text:

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _strip_leaked_terminal_responses_with_meta, the sentinel fast-path is likely to be bypassed for a lot of normal developer input because _TERMINAL_RESPONSE_SENTINELS includes "<" (common in code/HTML/generics). Also, because "<" is last in the tuple, any(marker in text ...) will scan the entire buffer for "\x1b[" and "^[" before it ever checks for <, and the function then repeats additional in scans in the per-marker blocks. Since this runs from _on_text_changed (per-keystroke), consider restructuring the fast-path to avoid multiple full-string scans (e.g., compute has_esc/has_visible/has_bare once and early-return if all false, and/or make the bare check more specific like requiring ; + M/m before running the bare regex).

Suggested change
if not any(marker in text for marker in _TERMINAL_RESPONSE_SENTINELS):
return text, False
had_mouse_reports = False
if "\x1b[" in text:
text = _DSR_CPR_ESC_RE.sub("", text)
text, count = _SGR_MOUSE_ESC_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
if "^[" in text:
text = _DSR_CPR_VISIBLE_RE.sub("", text)
text, count = _SGR_MOUSE_VISIBLE_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
if "<" in text:
has_esc = "\x1b[" in text
has_visible = "^[" in text
has_mouse_suffix = "M" in text or "m" in text
has_bare_mouse = "<" in text and ";" in text and has_mouse_suffix
if not (has_esc or has_visible or has_bare_mouse):
return text, False
had_mouse_reports = False
if has_esc:
text = _DSR_CPR_ESC_RE.sub("", text)
text, count = _SGR_MOUSE_ESC_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
if has_visible:
text = _DSR_CPR_VISIBLE_RE.sub("", text)
text, count = _SGR_MOUSE_VISIBLE_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
if has_bare_mouse:

Copilot uses AI. Check for mistakes.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
jsboige pushed a commit to jsboige/hermes-agent that referenced this pull request May 14, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
OutThisLife added a commit that referenced this pull request Jun 4, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the #17701#18113#26781#28463#35512
regex hardening chain in favor of the one correct parser fix.
Yuki-14544869 pushed a commit to Yuki-14544869/hermes-agent that referenced this pull request Jun 4, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
davidgut1982 pushed a commit to davidgut1982/hermes-agent that referenced this pull request Jun 5, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the #17701#18113#26781#28463#35512
regex hardening chain in favor of the one correct parser fix.
kossteg pushed a commit to kossteg/hermes-agent that referenced this pull request Jun 16, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
donbowman pushed a commit to donbowman/hermes-agent that referenced this pull request Jul 13, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…-self-heal

fix(cli): recover leaked mouse tracking terminal state
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
With the tokenizer reassembling split CSI sequences across a flush (prior
commit), no SGR mouse fragment can reach a text token anymore — terminals
write a mouse report as one atomic sequence, and any read/flush split now
re-joins in the tokenizer buffer instead of leaking. That makes the whole
downstream recovery layer dead code:

- SGR_MOUSE_FRAGMENT_RE, MOUSE_BURST_NOISE_RE, MOUSE_BURST_RESIDUE_RE
- parseTextWithSgrMouseFragments / parseSgrMouseFragment /
  normalizeSgrMouseFragment
- the whole-text mouse-burst noise fast path in parseMultipleKeypresses

Remove all of it (~185 lines) and the tests that only exercised it. The
narrow legacy X10 wheel-tail resynth stays (distinct mechanism, kept with
its own test). This retires the NousResearch#17701NousResearch#18113NousResearch#26781NousResearch#28463NousResearch#35512
regex hardening chain in favor of the one correct parser fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants