Found by the November 2026 pre-launch audit (finding termstate-10). Deferred past launch on purpose: only hostile or corrupt terminal output reaches it, no harm was found, and the fix is a grammar change in the tokenizer every byte of output passes through.
What disagrees
TerminalControlStreamFilter (lib/src/lib/terminal-controls.ts) and TerminalProtocolParser (lib/src/lib/terminal-protocol.ts) frame string controls (OSC, DCS, SOS, PM, APC) with a grammar that differs from xterm.js's EscapeSequenceParser in three ways:
- Start. Dormouse needs
ESC immediately followed by ] P X ^ _. xterm.js executes a C0 control met in its ESCAPE state and stays in that state, so ESC BEL ] opens an OSC for the renderer and is ground text for Dormouse. For a ESC BEL ]0;title BEL b, Dormouse then strips the OSC's own BEL terminator as a standalone bell, reports a bell, and forwards an unterminated OSC that swallows the text after it. The same bytes split across reads as ESC BEL / ] / … are framed as an OSC, so the result depends on read boundaries.
- End at C1. xterm.js leaves an OSC, DCS or APC string at any C1 control. Dormouse's end scans know only
U+009C (ST). So ESC ]8;;http://x U+009B >q BEL is one OSC to Dormouse and, to xterm.js, an OSC cut short followed by a device-attributes query, which xterm.js answers itself. That is the answer the ground-only rule exists to prevent (docs/specs/terminal-escapes.rationale.md, "Why the device-attributes query is recognized in ground text only").
- End at ESC. xterm.js dispatches an OSC that a bare
ESC ends; Dormouse calls it cancelled. Stricter, and harmless.
Consequence
For hostile or corrupt output, two rules in docs/specs/terminal-escapes.md do not hold: a string control may split across PTY reads at any position, and a consumed OSC's payload is never promoted to text or its terminating BEL to an alert. A sequence can also reach xterm.js that the parser never saw as one. The audit found nothing xterm.js acts on this way that the parser would otherwise have blocked: no clipboard addon is loaded, and mirrors drop replies.
Proposed fix
Make the grammar match the renderer's: allow C0 controls other than CAN, SUB and ESC between ESC and the introducer's final byte, and end a string at any C1 without consuming it. Two choices the fix has to make, which is why it was not done as a small repair:
- For
ESC <C0> ], the C0 in the gap must be forwarded (xterm.js executes it, so a BEL there does ring) without leaving a bare ESC that eats the next byte.
- Ending a string at a C1 that xterm.js then executes (
U+009B starts a CSI) reopens the ground-only reply rule, which must be restated for the bytes after the cut.
Test
A differential fuzz test: random byte streams, at every split position, through Dormouse's filter and through xterm.js's own parser, asserting they agree on where each string control starts and ends. The audit's probe found 581 disagreements in 150,000 random cases, all of shape 1. Plus the fixed cases above in lib/src/lib/terminal-controls.test.ts, whole and at every split.