Repository navigation
Description
Observed
In the v0.21.0 release audit, the local canonical suite on dev 433af8fd (macOS arm64, Node 26.10, 8 GB, load average 10–16 during the run) failed one test:
tests/inputOwnershipLive.test.ts → live zsh: a waiting read gets the answer typed at once or after a pause; nothing is queued, run or drawn — timed out waiting for ANSWER=hello; the status row showed Probably waiting for input · zsh · … · ≡ 1 queued: the answer had been queued instead of reaching read -r QA_REPLY.
Reproduction (load-dependent, pre-existing)
Four copies of that test file at once (each through the repository harness with NMSH_TEST_TMPDIR=/Volumes/T7Tmp/ai-dev), load average rising from 8 to about 27:
| Code | Copies failing | Failures (all … nothing was queued) |
|---|---|---|
dev 433af8fd (v0.21 candidate) |
2 of 4 | zsh hidden line after 300 ms; bash single key after 0 ms |
master 6af5cbab (v0.20.0 line) |
3 of 4 | bash single key after 0 ms; bash line after 300 ms; bash hidden line after 300 ms |
The same file alone passed 3 of 3 runs (6/6 each) at load 5–8, and CI is green. v0.21 does not change input ownership, the session layer or InputWatch; this is not a v0.21 regression.
Cause (from the code)
A key typed while a command runs, before NMSh has evidence that the command reads input, is held for at most OWNERSHIP_LOOK_MS (800 ms, src/app/TerminalApp.ts) while the session looks (InputWatch.lookNow). On macOS a prompt-less read/read -n 1 is recognized only from the shell holding its own terminal and being idle across two process-table samples (src/session/InputWatch.ts, evidence 4). When those samples and the session round trip take longer than 800 ms, the held keys are routed without evidence, so Enter queues the line.
Impact
Only on a heavily loaded machine, and only for a reply typed within about a second of starting a command that reads without a prompt. The read keeps waiting, the status row says it probably is, and the queue shows the typed reply as an entry. Commands that print a prompt, and replies typed later, are detected from other evidence.
Acceptance
- Decide the intended behavior when the look misses its deadline (for example, keep holding an Enter while a look is still in flight, bounded), without delaying type-ahead for an ordinary next command.
- A deterministic regression for a look slower than the deadline (fake probe), not a timing-dependent one.
- The live test passes 4× in parallel on the same 8 GB Mac.
Activity
raiseCatError commentedon Oct 18, 2026
Correction after a closer look (diagnostic run with the queue entries recorded):
The failures seen in CI (PR #425, Ubuntu) and in most of the parallel local runs were a test race, not this product behavior. The test typed its follow-up
echo BACK-…as soon as the answer's output appeared and checked the queue at once. On a busy host the shell's prompt for the answered command arrives later, so that Enter queued the follow-up, as designed. The recorded queued entry was{"c":"echo BACK-0-21","q":true}, and the answer had reached the program. Fixed in #427 (test only).This issue is the other failure: the answer itself queued, so the read never got it. The run times out waiting for the program's output while the status row shows
Probably waiting for input · … · ≡ 1 queued. Seen ondev433af8fd:ANSWER=hellonever arrived).I have not reproduced it at ordinary load or in CI. The cause and acceptance criteria above stand. Pre-existing on
master(v0.20 line); v0.21 does not change this code.raiseCatError commentedon Oct 18, 2026
v0.22: fixed in draft #433 (held until #430 merges; part of #431). Physical QA still required, so this stays open.
Decided behavior when the look is slow: keys typed before NMSh knows who reads them now wait for the look (bounded at 5 s) instead of an 800 ms deadline. Nothing typed is drawn, sent or queued until then; after 800 ms the composer says
Checking whether <program> is waiting for input · your typing is kept. A look that answers quickly adds no delay, and Ctrl+C still releases at once.Why not "route text at 800 ms, hold only the Enter": it fixes the queueing but still draws a hidden reply whenever the look is slow, for a prompt-less
read -sand for a promptedsudo/sshpassword alike. The unfixed baseline failed the live test'sthe hidden answer is never drawnassertion exactly that way under load.Second cause, also fixed: one process-table sample that caught the shell still runnable (normal on a loaded CPU right after its exec marker) was read as busy, so no follow-up sample was taken. A runnable first sample is now inconclusive when the shell holds its terminal; the follow-up decides.
Evidence
a8b383c2, pass on the fix. On the old code a password typed at once for a prompt-lessread -sis queued as a shell command.inputOwnershipLive4× in parallel on this Mac: the unfixed baseline reproduced this issue (timed out waiting for ANSWER=hello,Probably waiting for input · zsh · ≡ 1 queued) and, in another round, a drawn hidden reply; the fix was clean in 15 of 16 copies. The one failure was the Ctrl+C typing test, which then passed 18/18 targeted runs on both builds (details in Input ownership: keys wait for the look, however long a loaded machine makes it (#426) #433).npm run verifyon the branch: 3,152 tests, 3,150 pass, 2 skipped, 0 failed.ps -o wchanprints-for every process, so macOS cannot get a one-sample "reading the terminal" signal like Linux's.Input ownership: never route typing on a guess; echo-off input is nev…
Input ownership: a possible password never runs as a command by itself
typingAfterRead: accept a command NMSh holds after a hidden answer, r…
raiseCatError commentedon Oct 19, 2026
Release security review for v0.22.0 (#444). On the green candidate, two paths still let a hidden reply run as a shell command. Both are now fixed in #437 (
6486d9c5); details are there.The second path was reproduced live on
567cf533with real zsh and Bash; the fixed build passes the same test.Limits that remain, documented in the changelog:
This issue stays open until physical QA (pass 9 of
docs/testing/v022-physical-qa.md).raiseCatError commentedon Oct 19, 2026
A residual of this issue, seen in release CI (run 39224469156, first attempt, macOS shard 1,
inputOwnershipLive).What happened:
zsh: read -r QA_REPLYwith no prompt, andhellotyped at once. The answer was queued (≡ 1 queued) while the status row saidProbably waiting for input.Cause. On macOS, a
readwith no prompt is recognised only when the shell is seen holding its terminal and idle in two process-table samples (InputWatch.decide).readmoments later.Scope
567cf533. The same code passed this test on18144fb9(run 39223339352). None of the release review's changes run in this case: there is no hidden answer and no hidden question.Not changed for v0.22.0. Holding typing whenever the shell itself holds the terminal would also delay ordinary type-ahead during shell builtins (loops,
source), and sometimes discard it when the command ends. That is too risky for the release freeze. It will be listed as a known limitation in the final release commit. The failed job was re-run to measure the failure rate, not to replace this result.Release v0.22.0 candidate: security gate review, limitation wording, …
Input ownership: a function definition or plain shell syntax typed ah…
Input ownership: a function definition or plain shell syntax typed ah…
Release v0.22.0 candidate: post-QA stabilization (F01–F04, #426, #443)…
raiseCatError commentedon Oct 19, 2026
v0.22 post-QA security review (candidate
fa7ec665)Physical QA A9 (Ghostty + zsh) passed on
68c1cbb7; its CPU-load step was not run. That review re-examined the documented residual cases with synthetic secrets and bounded tests.Residual reproduced and fixed (#448). A reply typed and entered before a question with no prompt exists is queued, since nothing has asked yet. If that read gives up before any look sees it (a timed
readbetween looks seconds apart, or any program's read on macOS, which cannot be reported) and the command succeeds, the entry ran as a shell command. Reproduced live in zsh, Bash and Fish (sleep 2; perl -e 'alarm 1; <STDIN>'; true): the "reply" entered NMSh's journal, and so the shell's history and itscommand not foundoutput. Now a line entered with a plain Enter while a command runs, whose first command word isn't one the shell knows, waits for approval; the note never repeats its text. Real commands, function definitions and assignments typed ahead still run. Live test: fails first on all three shells, 7/7 after.Also: a window attached to an older session service (v0.20.0's: same protocol, so new windows attach) now warns that typing protections aren't active there, and when the service restarts (#456). In Always mode the intent model can no longer re-read a refused Ask request (#447).
Still open:
yes) typed before an unseen timed read runs after the command succeeds.Leaving this open for those; the limitation wording in the changelog states each case.
Unchanged and re-verified on the integrated candidate: Tab/Esc/Ctrl+C/Ctrl+Q on held typing, hidden input never drawn or logged (
NMSH_DEBUG_KEYSskips it), consecutive prompt-less hidden reads, delayedsudo-style prompts, entries queued while a question is seen. Deterministic input/queue suites and the live secrets, ownership, queue and typing-after-read suites pass (45/45 live).Input ownership: a line begun after the shell said done is type-ahead…
Input ownership: a program that survives Ctrl+C gets the line typed f…
Tests: live commands typed for the prompt wait for it; Dock Top and p…
Input ownership: a late report of the question just answered never ta…
Docs: v0.22 hardening ledger, exit-time typing limitation (#426, #443)
Tests: decide from the journal and queue, never from redrawn screen t…
Merge #457: v0.22 hardening: input ownership by lifecycle (#426), sta…
Tests: GNU screen's resize probe waits for each probe's completion be…
raiseCatError commentedon Oct 19, 2026
Final hardening (#457, merged into
release/v0.22.0=e8dd44e4): ownership, not vocabulary. #448's rule (hold a line only if its first word isn't a command) is replaced. Execution now depends on the input's lifecycle: a line runs as a shell command only when it is submitted at a verified prompt, queued with Ctrl+Q, or approved.Both remaining routes are closed:
fa7ec665: the replylsran in zsh, Bash and Fish. Now it is held.fa7ec665, in all three shells:echo QZ-RANran, thenQZSECRETecho NEXTran (a partial secret glued onto the next command).Also:
Remaining limits (changelog Known limitations):
Stays open for physical QA (R4, R5).
raiseCatError commentedon Nov 11, 2026
#541 (draft, targets
dev) adds Queue Mode as an explicit opt-out of this quarantine. It doesn't change the default.Also found while reproducing: zsh's
!"history expansion leaves an unfinished line at an invisible PS2. That looked like this ownership problem: "Can't tell whether echo is reading your typing". #541 now detects it, cancels the line and explains it.Automated tests: unit, plus live zsh, Bash and Fish. A scripted Rex run is described in #541. Physical-keyboard testing is still needed.
Queue: hold lines queued during a command once it is seen asking for …
Stack on #514: merge the input-ownership race fix
Queue: replace standing Queue Mode with a per-command queue composer