Skip to content

fix(onboard): release stdin event-loop ref so the wizard exits after its final prompt#2665

Merged
cv merged 2 commits into
mainfrom
fix/2518-onboard-stdin-cleanup
Apr 29, 2026
Merged

fix(onboard): release stdin event-loop ref so the wizard exits after its final prompt#2665
cv merged 2 commits into
mainfrom
fix/2518-onboard-stdin-cleanup

Conversation

@laitingsheng
Copy link
Copy Markdown
Contributor

@laitingsheng laitingsheng commented Apr 29, 2026

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 #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

  • 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

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • 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 (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

AI Disclosure

  • AI-assisted — tool: Claude Code, Codex

Signed-off-by: Tinson Lai tinsonl@nvidia.com

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.

…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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

The PR ensures interactive prompts explicitly manage stdin event-loop references: each prompt re-attaches stdin with ref() before reads and pauses+unref() after cleanup across credentials, onboard, policies, and sandbox-config modules so the process can exit after the final prompt.

Changes

Cohort / File(s) Summary
Stdin lifecycle (core prompt code)
src/lib/credentials.ts, src/lib/onboard.ts, src/lib/policies.ts, src/lib/sandbox-config.ts
Add explicit process.stdin.ref() / input.ref() before interactive/readline/raw-mode prompts and perform symmetric process.stdin.pause() + process.stdin.unref() (when available) after closing/cleanup so prompts don't leave stdin in a detached state that blocks process exit or later prompts.
Tests (source-inspection & assertions)
test/credentials.test.ts, test/onboard.test.ts, test/policies.test.ts
Add static/source-text assertions verifying prompts call ref() before entering raw-mode/readline and call pause()/unref() during cleanup; extend tests to catch regressions where a prior sticky unref() would block subsequent prompts or prevent process termination.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 In the terminal burrow where bytes like to hop,
I nudged stdin gently — "please don't just stop."
ref() before dancing, unref() when we're done,
No more stuck shells — now exit and run! 🎋

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the root cause of the issue: releasing stdin's event-loop reference so the wizard exits cleanly after the final prompt on Linux.
Linked Issues check ✅ Passed Changes comprehensively implement stdin ref/unref lifecycle management across all interactive prompt paths (credentials, onboard, policies, sandbox-config) with matching test assertions to prevent the hang described in #2518.
Out of Scope Changes check ✅ Passed All changes focus exclusively on managing stdin event-loop references in interactive prompts and their tests; no unrelated modifications are present.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2518-onboard-stdin-cleanup

Comment @coderabbitai help to get the list of available commands and usage tips.

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 cv merged commit 771edeb into main Apr 29, 2026
20 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Linux][Onboard] nemoclaw onboard process does not exit after printing post-onboard summary; user must Ctrl+C to recover terminal

3 participants