You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Hackathon] fix(prompt): a follow-up prompt on an already-ended stdin never resolves — setup exits 13 as 'unsettled top-level await' #639
When the CLI shows a second prompt on an input stream that has already ended — e.g. the setup target-confirmation loop (CONFIRM_TARGETS_MAX_ATTEMPTS re-prompt after an invalid answer) or agent install\'s target prompt — promptText/promptSecretnever resolves.readLineonly callsisInputEnded(input) when leftover buffered input exists (src/lib/prompt.tslines 138-147), then unconditionally attachesinput.on('end', …)at line 150. Butendalready fired, so the promise never settles. With nothing left keeping the event loop alive, Node aborts withWarning: Detected unsettled top-level await … program.parseAsync` and exits with code 13; if another handle keeps the loop alive, the prompt simply hangs forever.
Repro (real PTY): run testsprite setup (no flags) and at Install skills for … [claude]: type a typo like clade and press Ctrl-D Ctrl-D (terminal EOF). Actual: after unknown target \"clade\" the re-prompt prints, then the process dies with the unsettled-TLA warning and status 13 — no error envelope, no telemetry flush. Expected: the re-prompt notices stdin has ended and resolves '' (accepting the default, like the first prompt's onEnd does) or rejects with a proper error — exiting with a documented error code, not Node's internal unsettled-await one.
Same failure at unit level on a18d37c: const input = new PassThrough(); input.end('typo\n'); await promptText('q1', {input}); await promptText('q2', {input}) — the second call never settles (verified with a Promise.race timeout).
Real-world impact: exit 13 is documented in DOCUMENTATION.md as "Feature gated (plan entitlement or per-account flag)", so scripts and CI wrappers misread this crash as a billing/entitlement failure. A typing mistake + Ctrl-D — a normal terminal habit — silently kills interactive setup.
Details / implementation notes
Root cause: readLine (src/lib/prompt.ts) checks isInputEnded(input) only inside the pending !== undefined branch (lines 138-147). With no buffered leftovers it falls through and attaches on('end', onEnd) to a stream whose readableEnded is already true — the event already fired and will never fire again, so the promise stays pending. Once stdin's EOF stops holding the event loop open, Node's unsettled-top-level-await path exits with code 13 and prints the internal warning.
Fix: after processing any pending text, check isInputEnded(input) (or readableEnded) unconditionally before attaching listeners, and finish() when ended — resolving with whatever was typed ('' if nothing), matching the first prompt's onEnd semantics. Optionally track ended streams (e.g. a WeakSet marked in onEnd) so later prompts on the same stream resolve immediately. Regression test: input.end('typo\n'); await promptText(); await promptText() must resolve '' rather than hang. The same helper backs agent install's target prompt, so the fix covers both interactive paths.
A PR with the fix + regression test will follow referencing this issue.
Confirmations
I have searched existing issues and this is not a duplicate.
I have provided my Discord identity above for reward coordination.
Thanks @Satianurag. It takes a typo followed by Ctrl-D at an interactive prompt, and scripted runs pass flags instead of answering prompts, so no one has hit this in practice. Closing as not planned.
Activity
fyZhang66 commentedon Oct 22, 2026
Thanks @Satianurag. It takes a typo followed by Ctrl-D at an interactive prompt, and scripted runs pass flags instead of answering prompts, so no one has hit this in practice. Closing as not planned.