fix(onboard): release stdin event-loop ref so the wizard exits after its final prompt#2665
Merged
Conversation
…its final prompt
`readline.createInterface({ input: process.stdin })` resumes stdin
internally; on a TTY the underlying handle stays ref'd to the event
loop after `rl.close()`, so once the wizard's last prompt resolves
Node has no other refs but cannot exit. The user sees a hung terminal
after the post-onboard summary block and reaches for Ctrl+C. CI/non-TTY
runs do not hit it because the previous TTY-guarded cleanup did
pause+unref on pipes.
Switch the shared `prompt()` cleanup to always pause+unref, and add a
matching `process.stdin.ref()` at the top of every prompt path
(readline branch, secret branch, `promptSecret()` body, the four
raw-mode TUI selectors in onboard.ts) so a sticky `unref()` from a
prior prompt does not strand a follow-up read on a detached handle.
The same TTY-guarded cleanup pattern in `policies.ts` and the missing
pause/unref in `sandbox-config.ts:confirmYesNo` are aligned to the
same shape.
Tests cover the source-text contract (`ref()` precedes any branching
in `prompt()`, `promptSecret()` is self-contained, raw-mode sites in
onboard.ts ref()-then-setRawMode and unref on cleanup) plus PTY-attached
behavioural runs of two sequential `prompt()` calls and a `prompt()`
followed by `prompt({ secret: true })` to catch sticky-unref regressions.
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Contributor
📝 WalkthroughWalkthroughThe PR ensures interactive prompts explicitly manage stdin event-loop references: each prompt re-attaches stdin with Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
The PTY-attached behavioural cases used util-linux `script -qfec`, which BSD `script` (macOS) does not accept. The macos-e2e job ran the tests with the wrong flag set and reported AssertionError on the exit status check. The bug being verified only reproduces on Linux interactive TTY, so the source-text assertions on `ref()/unref()` placement are sufficient cross-platform; manual Linux repro covers the qualitative behavioural verification. Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
cv
approved these changes
Apr 29, 2026
DemianHeyGen
pushed a commit
to DemianHeyGen/NemoClaw
that referenced
this pull request
Apr 30, 2026
…its final prompt (NVIDIA#2665) ## Summary `nemoclaw onboard` on Linux interactive TTY hangs after the post-onboard summary box — the wizard finishes, prints the dashboard URL, and then sits there with no shell prompt until the user reaches for Ctrl+C. The cause is `readline.createInterface({ input: process.stdin })` resuming stdin internally and leaving the underlying TTY handle ref'd to the event loop after `rl.close()`; once the wizard's last prompt resolves Node has no other refs but still cannot exit. CI and other non-TTY callers did not hit this because the previous cleanup pause+unref'd stdin only when `!process.stdin.isTTY`. This switches the shared `prompt()` cleanup to always pause+unref and adds a matching `process.stdin.ref()` at every prompt entry — the readline branch, the secret branch, `promptSecret()` itself, and the four raw-mode TUI selectors in `onboard.ts` — so a sticky `unref()` from a prior prompt cannot strand a follow-up read on a detached handle. The same TTY-guarded cleanup pattern in `policies.ts` and the missing pause/unref in `sandbox-config.ts:confirmYesNo` are aligned to the same shape. ## Related Issues Fixes NVIDIA#2518 ## Changes - `src/lib/credentials.ts`: drop the `if (!process.stdin.isTTY)` guard from `prompt()` cleanup so pause+unref runs on every code path; hoist `process.stdin.ref()` above the secret-vs-readline branch so both paths re-attach stdin before the next read; make `promptSecret()` self-contained — ref() at the top of the body and pause+unref in its own cleanup so a direct caller (or two sequential direct calls) is safe regardless of prior stdin state. - `src/lib/onboard.ts`: add `process.stdin.ref()` before the `setRawMode(true) + resume()` block in the messaging-channels selector and in the three identical arrow-key picker patterns (sandbox-prompt, model-prompt, policy-preset-prompt) so a sticky unref() from earlier `prompt()` cleanup does not leave the raw-mode read attached to a detached handle; add `unref()` to each cleanup so wizards that end on a TUI selector also exit naturally. - `src/lib/policies.ts`: drop the `if (!process.stdin.isTTY)` guards in both preset-selection callbacks (lines 446 and 770) and add ref() at the top of each prompt block. - `src/lib/sandbox-config.ts`: add the matching ref() + pause/unref pair to `confirmYesNo()`, which previously only ran `rl.close()` and would have leaked the same stdin ref had it ever been the wizard's last prompt. - `test/credentials.test.ts`: source-text assertions for the new contract — TTY guard is gone from `prompt()` cleanup, ref() precedes the silent/secret branch in `prompt()`, `promptSecret()` is self-contained, and the secret-path cleanup pauses+unrefs. - `test/policies.test.ts`: source-text assertions for the same contract on the two preset-selection prompts — TTY guard removed and a ref() precedes each `createInterface`. - `test/onboard.test.ts`: source-text assertion that all four raw-mode TUI sites (one `input.ref()` alias + three `process.stdin.ref()` calls) ref before `setRawMode(true)` and unref after `setRawMode(false)`, so any one of them stays safe to be the wizard's last prompt. ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification - [x] `npx prek run --all-files` passes - [x] `npm test` passes - [x] Tests added or updated for new or changed behavior - [x] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ## AI Disclosure - [x] AI-assisted — tool: Claude Code, Codex --- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved reliability of sequential interactive prompts (credentials, onboarding, policies, and configuration flows) so prompts no longer hang and the process exits cleanly after the final prompt. * **Tests** * Added and extended tests to validate interactive prompt lifecycle and ensure multiple sequential prompts complete without leaving lingering handles or blocking subsequent prompts. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
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.
Summary
nemoclaw onboardon Linux interactive TTY hangs after the post-onboard summary box — the wizard finishes, prints the dashboard URL, and then sits there with no shell prompt until the user reaches for Ctrl+C. The cause isreadline.createInterface({ input: process.stdin })resuming stdin internally and leaving the underlying TTY handle ref'd to the event loop afterrl.close(); once the wizard's last prompt resolves Node has no other refs but still cannot exit. CI and other non-TTY callers did not hit this because the previous cleanup pause+unref'd stdin only when!process.stdin.isTTY.This switches the shared
prompt()cleanup to always pause+unref and adds a matchingprocess.stdin.ref()at every prompt entry — the readline branch, the secret branch,promptSecret()itself, and the four raw-mode TUI selectors inonboard.ts— so a stickyunref()from a prior prompt cannot strand a follow-up read on a detached handle. The same TTY-guarded cleanup pattern inpolicies.tsand the missing pause/unref insandbox-config.ts:confirmYesNoare aligned to the same shape.Related Issues
Fixes #2518
Changes
src/lib/credentials.ts: drop theif (!process.stdin.isTTY)guard fromprompt()cleanup so pause+unref runs on every code path; hoistprocess.stdin.ref()above the secret-vs-readline branch so both paths re-attach stdin before the next read; makepromptSecret()self-contained — ref() at the top of the body and pause+unref in its own cleanup so a direct caller (or two sequential direct calls) is safe regardless of prior stdin state.src/lib/onboard.ts: addprocess.stdin.ref()before thesetRawMode(true) + resume()block in the messaging-channels selector and in the three identical arrow-key picker patterns (sandbox-prompt, model-prompt, policy-preset-prompt) so a sticky unref() from earlierprompt()cleanup does not leave the raw-mode read attached to a detached handle; addunref()to each cleanup so wizards that end on a TUI selector also exit naturally.src/lib/policies.ts: drop theif (!process.stdin.isTTY)guards in both preset-selection callbacks (lines 446 and 770) and add ref() at the top of each prompt block.src/lib/sandbox-config.ts: add the matching ref() + pause/unref pair toconfirmYesNo(), which previously only ranrl.close()and would have leaked the same stdin ref had it ever been the wizard's last prompt.test/credentials.test.ts: source-text assertions for the new contract — TTY guard is gone fromprompt()cleanup, ref() precedes the silent/secret branch inprompt(),promptSecret()is self-contained, and the secret-path cleanup pauses+unrefs.test/policies.test.ts: source-text assertions for the same contract on the two preset-selection prompts — TTY guard removed and a ref() precedes eachcreateInterface.test/onboard.test.ts: source-text assertion that all four raw-mode TUI sites (oneinput.ref()alias + threeprocess.stdin.ref()calls) ref beforesetRawMode(true)and unref aftersetRawMode(false), so any one of them stays safe to be the wizard's last prompt.Type of Change
Verification
npx prek run --all-filespassesnpm testpassesmake docsbuilds without warnings (doc changes only)AI Disclosure
Signed-off-by: Tinson Lai tinsonl@nvidia.com
Summary by CodeRabbit
Bug Fixes
Tests