fix(hermes-ink): suppress SGR mouse fragments split across flush boundaries - #29337
Closed
Mamidi7 wants to merge 2 commits into
Closed
fix(hermes-ink): suppress SGR mouse fragments split across flush boundaries#29337Mamidi7 wants to merge 2 commits into
Mamidi7 wants to merge 2 commits into
Conversation
…daries
When AlternateScreen is active, Ink enables MOUSE_ANY (DEC 1003) so the
TUI can handle click/drag/hover events. The app's 50ms flush timer can
fire while an SGR mouse event (ESC[<btn;col;rowM) is still arriving on
stdin, splitting the escape sequence across two stdin chunks.
The tokenizer handles the first chunk (e.g. ESC[<0;35;) correctly as a
sequence token, but the remainder ('46M') arrives as a text token with
no ESC prefix — so parseKeypress never enters its ESC-anchored regexes
and returns a default key with name=''.
The existing InputEvent guard (/^\[<\d+;\d+;\d+[Mm]/) only catches the
specific split where ESC alone is flushed and the full 3-field fragment
arrives intact. It misses:
- 2-field fragments: ESC[<0; flushed -> text '35;46M'
- 1-field fragments: ESC[<0;35; flushed -> text '46M'
Add two additional guards that match these partial fragments. The
!keypress.name gate ensures typed input like '46M' (where
parseKeypress sets name='m') is not accidentally suppressed.
Fixes #39379
Collaborator
…env values When TERMINAL_ENV is set in ~/.hermes/.env (e.g. from a previous Docker setup) but absent from config.yaml's terminal section, load_cli_config() skips exporting the config value because _file_has_terminal_config is False and the env var is already set. This leaves a stale TERMINAL_ENV in the environment, causing the terminal backend to silently diverge from what config.yaml says. Fix: add an explicit exception for TERMINAL_ENV in the config→env bridge. This env var selects the terminal backend and should always be controlled by config.yaml. All other terminal env vars (ssh_host, docker_image, etc.) continue to respect the existing design where .env can override defaults when no explicit terminal section exists. Fixes #29186
This was referenced May 29, 2026
Merged
4 tasks
Collaborator
|
Superseded by #38564. That PR replaces the three stacked narrow regexes with a single flush-aware guard that also covers the cases this one leaves open:
It also accounts for the upstream Thanks @Mamidi7 — the root-cause analysis here (1-/2-field flush-boundary splits) is exactly right and carried straight into the replacement. The unrelated Closing in favor of #38564. |
Collaborator
|
Closing as superseded by #38564. |
OutThisLife
added a commit
that referenced
this pull request
Jun 4, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes #29337.
OutThisLife
added a commit
that referenced
this pull request
Jun 4, 2026
…-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes #29337)
stanleylai
pushed a commit
to stanleylai/hermes-agent
that referenced
this pull request
Jun 4, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
Yuki-14544869
pushed a commit
to Yuki-14544869/hermes-agent
that referenced
this pull request
Jun 4, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
Yuki-14544869
pushed a commit
to Yuki-14544869/hermes-agent
that referenced
this pull request
Jun 4, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 5, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 5, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 5, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes #29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes #29337.
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes #29337)
kossteg
pushed a commit
to kossteg/hermes-agent
that referenced
this pull request
Jun 16, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
kossteg
pushed a commit
to kossteg/hermes-agent
that referenced
this pull request
Jun 16, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
donbowman
pushed a commit
to donbowman/hermes-agent
that referenced
this pull request
Jul 13, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
donbowman
pushed a commit
to donbowman/hermes-agent
that referenced
this pull request
Jul 13, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…are rule When App's 50ms flush watchdog fires mid-CSI during a render stall, an SGR mouse report (ESC[<btn;col;row M/m) is split across stdin chunks: the tokenizer force-emits the buffered prefix and resets to ground, so both the prefix and the ESC-less remainder reach InputEvent as nameless tokens. The previous guard only matched a full `[<\d+;\d+;\d+[Mm]` fragment, so the flushed prefixes (`ESC[<0;35;`) and the 1-/2-field and leading-`;` tails (`46M`, `35;46M`, `;46M`) still leaked into the composer as `46M35;40M...` during long sessions. Replace the three would-be narrow regexes with one consolidated rule that covers every split position. A `(?=...\d)` lookahead keeps typed `<`, `[`, `;`, and `M` safe (no coordinate digit), and the embedded M/m terminator in the param class leaves stuck-together fragments / prose intact. The existing `!keypress.name` gate continues to protect real keystrokes, which arrive one char per chunk with a name set. Supersedes NousResearch#29337 (covers the prefix-leak and leading-`;`/1-/2-field tail cases that PR's two added guards missed).
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…rop the regex sink Root-cause fix for the SGR mouse fragment leak (`46M35;40M...` typed into the prompt). The leak was never really about the fragments — it was the flush emitting them. When App's 50ms watchdog fires mid-CSI during a render stall, the tokenizer was force-emitting the buffered partial as a token and resetting to ground, so both the prefix and the ESC-less remainder surfaced as unparseable input. Make the flush state-aware (xterm.js discipline): a bare ESC still flushes to the Escape key (the legitimate ESCDELAY case), but a buffer still inside a multi-byte control sequence (csi/osc/dcs/apc/ss3/intermediate) is NOT emitted — it's kept so the continuation reassembles on the next feed. A one-tick truncation valve in createTokenizer.flush() drops a partial that survives a second flush with no progress, so a genuinely truncated write can't fuse into the next keypress. With partials never entering the input stream, the downstream scrubber is dead code: remove the SGR fragment guard from input-event.ts (both the original `/^\[<\d+;\d+;\d+[Mm]/` and the consolidated form added earlier in this PR). The parse-keypress burst-recovery regexes (MOUSE_BURST_*) are now also redundant but left in place as a safety net for one release; they can be removed in a follow-up once this soaks. Tests: tokenize.test.ts proves a mid-CSI flush keeps/reassembles and that a stale partial is dropped after a second flush and a bare ESC still emits; parse-keypress.test.ts adds the end-to-end split-then-reassemble case yielding a single clean mouse event with no leaked key. Supersedes NousResearch#29337.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…use-fragment-leak fix(hermes-ink): reassemble split SGR mouse sequences at the tokenizer (supersedes NousResearch#29337)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When the Hermes TUI has been running for a few hours, moving the mouse cursor over the terminal window causes literal text like
46M35;40M35to appear in the input box.Root cause
Ink enables MOUSE_ANY (DEC 1003) during alt-screen — every pixel of mouse movement generates an SGR mouse event (
\x1b[<btn;col;rowM). This is intentional for click/drag/hover support in the TUI.The 50ms flush timer can split events across stdin chunks — when a heavy React commit blocks the event loop (rendering a large message, tool output, scrolling), the flush timer fires mid-sequence. The incomplete escape sequence is flushed as a sequence token, and the remainder arrives as a text token with no ESC prefix.
Existing guard only catches one split boundary — the existing InputEvent guard (
/^\[<\d+;\d+;\d+[Mm]/) catches the case where ESC alone is flushed and the full 3-field[<btn;col;rowMfragment arrives intact. But it misses:ESC[<0;flushed → text35;46M)ESC[<0;35;flushed → text46M)Text fragments reach InputEvent with empty key name —
parseKeypressdoes not recognize these ESC-less text chunks, so it returns a default key withname=. The existing guards cannot filter them becausekeypress.nameis falsy.Symptoms
46M35;40M35appear in the input box when moving the mouse cursor[or<symbols — only alphanumeric + semicolon separatorsFix
Add two guards in
InputEvent.parseKey()after the existing 3-field SGR mouse fragment guard:2-field guard (
/^\d+;\d+[Mm]$/): catches fragments like35;46Mthat lost both the ESC and the[<prefix during a split at the second semicolon boundary.1-field guard (
/^\d+[Mm]$/): catches fragments like46Mfrom a deeper split at the last semicolon boundary.Both guards use the
!keypress.nameprecondition, ensuring intentionally typed input like46M(whereparseKeypresssetsname=m) is not suppressed — only unparseable fragment artifacts.Verification
keypress.name46M(fragment)35;46M(fragment)46M(typed)m!namefails → skiptest(typed)