The previous post in this series was about a small Esc-handling bug — the first character of a rapid post-Esc command was being eaten as a meta-key sequence. The fix was one line. The deeper lesson there was that the interactive REPL had been getting tested by inspection, not by exercising real keyboard input under realistic timing.

This week's probe continued that theme. The question: what happens when the attorney pastes a multi-line block of text into the prompt? The kind of thing that happens dozens of times a day in legal work — copying a paragraph of case notes from a document, a sentence from an email, a chunk of medical-record context. The clipboard content has embedded newlines, and the attorney expects to paste it, edit if needed, then hit Enter once to submit the whole thing as one query.

That's not what happened.

What was happening

The expect script for the probe sent three lines of text with embedded newlines, then sent Enter, then "exit". The terminal output showed something different from what the test expected. The first line "first line of pasted text" appeared at the prompt — and then the first embedded newline submitted that line as a query. The model started processing. While the model was thinking, the second line of the paste arrived as another Enter, which queued another query. By the time the model returned a response to the first line, the second-line query was already in flight. The third line ended up as another query after that.

From the attorney's perspective: I pasted three lines, expected one query, got three. The model's first response addressed "first line of pasted text" as if it were a complete question. By the time the response appeared, two more confused queries had been processed. The terminal scrolled past the first response before the attorney could read it.

This isn't an obscure edge case. It's exactly what happens when you do Cmd-V with multi-line clipboard content. Every modern legal-work workflow involves this kind of paste at some point.

What bracketed paste mode is

Terminal emulators have known about this problem since the 1990s. The solution they converged on is called bracketed paste mode (BPM). When an application asks the terminal to enable BPM — by sending the escape sequence \x1b[?2004h on startup — the terminal changes how it transmits pastes. Instead of sending the raw clipboard bytes, it wraps them in special start and end markers: \x1b[200~ at the start of the paste, \x1b[201~ at the end. The bytes in between are the clipboard content, unmodified.

The application then knows: this isn't typing, this is a paste. The newlines inside the markers are not Enter key presses; they're literal newline characters that were in the clipboard. The application can decide what to do with them. For a single-line REPL like aaicase, the right answer is to flatten the newlines to spaces (the attorney wanted one query) and insert the result into the prompt buffer as one chunk. Then when the attorney presses Enter for real, the whole thing submits.

BPM has been supported by every modern terminal emulator for over a decade. macOS Terminal, iTerm2, Alacritty, Kitty, Windows Terminal, VS Code's integrated terminal, every xterm-derived emulator. Terminals that don't support it silently ignore the enable sequence — the application gets the legacy behavior (pastes arrive as raw keystrokes) and the user notices nothing different.

What the wiring required

The hard part wasn't enabling BPM. It was getting the application's input layer to actually see and handle the markers before Node's readline turned the embedded newlines into Enter events.

Node's readline is a state machine that consumes bytes from stdin and produces "keypress" events. A byte that's \x0A (line feed) or \x0D (carriage return) triggers a line-submit event. The first attempt at the fix added a stdin.on('data', ...) listener that watched for the paste-start marker. That listener saw the markers and the content. But readline ALSO saw the same bytes — both listeners run on the same event — and turned the embedded newlines into Enter presses before the application's listener could react.

The fix needed to happen one layer deeper. Readline has an internal method called _ttyWrite that's called for every byte arriving on the TTY input. The "right" approach is to wrap _ttyWrite so that during a paste, the bytes never reach the rest of readline's processing — they get accumulated in a local buffer. When the end marker arrives, the accumulated buffer gets joined with spaces and inserted into rl.line at the current cursor, then rl._refreshLine() repaints. Readline never saw any newlines during the paste, so it never fired any Enter events.

This is a slightly ugly intercept — _ttyWrite is a private API, and wrapping it means future Node versions could break the wrap. But the public surface (the keypress event) doesn't expose the byte-level granularity needed. The alternative would be to replace readline entirely with a custom input loop, which is a much bigger change than wrapping one method. The pragmatic tradeoff was the wrap.

What the test showed

Before the fix: a three-line paste produced three queries. The expect script could see this in the terminal output — the "Connecting..." spinner appeared three times, each time interrupting the previous response.

After the fix: the same expect script produced one query. The three lines of clipboard content appeared at the prompt as one line ("first line of pasted text second line third line") — the newlines flattened to spaces. The attorney's Enter at the end of the paste submitted the whole thing. The model received one query.

Verified that this didn't break anything else. Normal single-line typing still works. The previous fix (50ms escape timeout for the Esc-clears-line gesture) still works. Arrow-key history navigation still works. Backspace still works. The cursor blinker and the redraw on Esc still work.

What I'm not handling yet

Flattening newlines to spaces is a judgment call. For most legal-work pastes (case notes, sentence fragments, claim descriptions), spaces are the right delimiter — the attorney wanted text, not structure. But if someone pastes a code snippet, or a structured list, the newline-to-space flattening loses information. The current implementation always flattens; a future version could detect "this paste looks structured" and preserve the newlines as literal \n characters inside the query.

Tabs get flattened to spaces too, for the same reason: a single-line prompt can't render tabs meaningfully, and the visual mess of \t characters in the line buffer would be worse than the loss of indentation.

The paste content arrives without sanitization — if someone pastes ANSI escape sequences or control characters, those go straight into the prompt. The audit log will capture whatever ends up in the query field, so the trail is honest about what was submitted. A future version might strip control characters from pastes (similar to the endpoint-level control-character refusal we already have on the API side) but that's a separate hardening pass.

The release

The version on the install URL is 4.5.77. The change is roughly 70 lines of bracketed-paste handling around the readline interface — and a one-line escape sequence sent at startup to ask the terminal to enable the feature. The benefit shows up the first time an attorney pastes multi-line content and notices that it didn't fire several queries.

The work continues at the pace of one verified release at a time, and the keyboard surface is, again, a little more honest about what the attorney is actually doing.