Repository navigation
Input ownership: keys wait for the look, however long a loaded machine makes it (#426) - #433
Merged
Merged
Conversation
…e makes it #426: under heavy load, a reply typed right after starting a prompt-less read (`read -r reply`, `read -s`, `read -k 1`) was routed before NMSh knew who read it. The frontend held such keys for at most 800 ms while the session looked; a look that took longer (a prompt-less line read needs two process-table samples, and each starts stty and ps) released them without evidence, so Enter queued the reply and the read kept waiting. A password typed at once for a prompt-less `read -s` became a queued shell command, and under load a hidden reply could be drawn in the composer before the look confirmed it. - TerminalApp: held keys are routed when the look answers, bounded at 5 s (a look is itself bounded by its probes' timeouts). Until then nothing typed is drawn, sent or queued. After 800 ms the composer says "Checking whether <program> is waiting for input · your typing is kept", so nobody types it again. The exec wait before the look uses the same bound. Ctrl+C still releases at once; a look that answers quickly (the usual case) adds no delay. - InputWatch: a first sample that finds a process on a CPU with nothing earlier to compare is "runnable", not computing. When the shell holds its terminal, the follow-up sample decides, so a builtin still runnable for a moment before it blocks in read is not taken for a busy shell. Every other case keeps its previous meaning. Deterministic regressions drive a look the test settles on mocked timers: replies to prompt-less line, hidden and prompted (sudo) reads typed at once are never queued, drawn or run; keys after a held Enter stay behind it; a look that never answers releases at the bound; the command ending while keys are held runs the line once as the next command; quick looks add no delay. Seven of them fail on the previous code. Refs #426
This was referenced Oct 18, 2026
raiseCatError
added a commit
that referenced
this pull request
Oct 18, 2026
This was referenced Oct 18, 2026
…er drawn Review of the first #426 change found that its 5 s fallback, and any look that ended without evidence, still routed held keys as ordinary typing: a password typed for read -s, sudo or ssh could be drawn in the composer, queued, and run as a command; a command that ended while keys were held (read -s -t that gave up) replayed them at the prompt and ran them; Ctrl+C replayed held keys before interrupting, so a held reply plus Enter became a paused queue entry. - InputWatch: echo off with line input is a hidden request whatever else is known (confirmed when the group is idle, likely when it computes or the kernel says it is blocked elsewhere). directInput treats hidden input as direct at either confidence, so NMSh never draws what the terminal would not echo. - lookNow says what it saw: hidden, visible, unknown (no sample said how the terminal echoes: a probe failed or timed out), idle or unavailable. The echo state comes from any sample taken, even one overtaken by output. input-looked carries it (optional field; services advertise input-look-result; older frontends ignore it). - TerminalApp: held keys are settled explicitly. visible or evidence routes as before. unknown gets one more look; if that is blind too, or no answer comes within 5 s, the typing stays held and hidden and the activity rows say "Can't tell whether <program> is reading your typing": Ctrl+S sends it to the program, Tab shows it in the composer (a final Enter is not replayed), Esc discards it, and later evidence decides it the same way. Ctrl+C discards held typing, as a terminal flushes its input on an interrupt. A command that ends while its typing was held past 800 ms never runs it (a note says it was discarded); a quick end still runs type-ahead. An older service's silent answer is routed as before (documented limit). Adversarial regressions drive looks the test settles with what they saw, on mocked timers, and assert the secret is nowhere: not on screen, in the composer, the queue, the transcript or a submitted command. 10 of them fail on the previous head; all pass here. Refs #426
raiseCatError
added a commit
that referenced
this pull request
Oct 19, 2026
This was referenced Oct 19, 2026
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.
What was wrong (#426)
While a command runs and NMSh has no evidence yet that it reads input, the frontend holds the keys that decide where typing goes (the first key of a draft, an Enter that would queue) while the session looks. On
devthe hold ended after a fixed 800 ms whether or not the look had answered, so on a loaded machine a reply to a prompt-lessreadwas queued and a password typed at once for a prompt-lessread -sbecame a queued shell command.Review of the timeout path (requested)
The first commit replaced the 800 ms deadline with a 5 s bound. Review showed the bound did not make it private; the second commit fixes each case at its cause.
Can't tell whether <program> is reading your typing · held, not shownandCtrl+S sends it to <program> · Tab shows it · Esc discards it. Later evidence decides it automaticallyread -s/sudo/sshpassword reach the composer, queue, transcript or logs?sudo/sshon a CPU in the only sample, which the shell-only follow-up did not cover)unknownis never treated as permission to draw. Key diagnostics already exclude running-program input; drafts are not journaledread -s -t 2with a slow look) replayed the password at the prompt and Enter ran it… ended before NMSh could tell whether your typing was for it; it was discarded, not run.). A quick end (type-ahead after a short command) still runs once. A program that changes state gets keys only on evidence (a hidden or line request, or the terminal handed over), as a terminal would deliver themShellSession.interruptalready discards input held behind terminal answers), then interrupts. Order is preserved on every other path: keys after a held Enter stay behind it; Ctrl+S sends composer text then held keys as typedunknown; it gets one more look (evidence, not waiting longer), then the held state. Long-lived processes keep the held state until a choice or evidence; their end discards held typing with a noteA bounded timeout alone does not guarantee privacy; the guarantee now rests on two rules: NMSh never draws input the terminal would not echo, and it never routes typing on a guess.
What changed
InputWatch: echo-off line input → hidden request (confirmed or likely). A first sample that finds the shell on a CPU with nothing to compare isrunnable, not computing; the follow-up decides.lookNow()returnshidden | visible | unknown | idle | unavailable, using the echo state of any sample taken (even one overtaken by output).input-lookedgains an optionalresult; services advertiseinput-look-result. Old frontends ignore the field. A new frontend talking to an older service gets no result and routes as before (documented limit).inputState.directInput: hidden input is direct at either confidence.TerminalApp: one hold with explicit outcomes (route,discard,send,reveal);Checking whether … · your typing is keptafter 800 ms; the held state with its rows and composer hint; one evidence re-look; Ctrl+C discard; settle on command end.docs/design/input-awareness.md) and changelog rewritten for the final behavior.Verification
Regressions first.
tests/inputOwnershipLook.test.ts(each look is a promise the test settles with what it saw; mocked timers; anowhere()check that the secret is not on screen, in the composer, the queue, the transcript or a submitted command) and four new watch/protocol tests intests/inputAwareness.test.ts:deva8b383c2: the first commit's 7 regressions failed;6958c277: 8 of 15 final frontend tests and 4 watch/protocol tests fail (12 in all; the second commit message says 10, an undercount);Suites. Focused input/queue/session suites, unit and real-PTY (zsh, Bash, Fish): 81/81. Full canonical
npm run verifyon this head (NMSH_TEST_TMPDIR=/Volumes/T7Tmp/ai-dev, load 5–17): exit 0, 3,161 tests, 3,159 pass, 2 skipped, 0 failed. (First commit: 3,152 / 3,150 / 2 / 0.)Live, first commit (4 copies of
tests/inputOwnershipLive.test.tsin parallel on this 8 GB Mac): the unfixed baseline reproduced #426 (timed out waiting for ANSWER=hello,Probably waiting for input · zsh · ≡ 1 queued) and, in another round, a drawn hidden reply; the first commit was clean in 15 of 16 copies; the one failure is tracked separately as #436 and is not claimed fixed here. CPU contention alone does not reproduce #426 (a probe stays ~10 ms at load 64); slow looks come with memory pressure. Fish #356: 32/32 at 8× parallel on this branch; not declared fixed.CI on this head (
e566a1e6):tests/commandQueueLive.test.ts, a multiline queue entry uses native grouping). A queuedprintfprintedwrite: Interrupted system call, so Fish's write was interrupted by a signal.Limits
Physical QA (not done)
Checklist:
docs/testing/v022-physical-qa.mdpass 1 (in #435). Ghostty and Terminal.app; zsh, Bash and Fish:read -r reply; echo got=$reply, answer at once and after a pause →got=…, nothing queued.read -s(no prompt) andsudo -k true, password at once → never shown, never queued.sleep 5, typeecho next+ Enter → queued at once.Checking whether …note may appear; answers still reach the read; nothing shown or queued.Refs #426