Skip to content

fix(cli): handle closed plugin uninstall prompt#73566

Merged
hxy91819 merged 4 commits into
openclaw:mainfrom
ai-hpc:fix/plugin-uninstall-closed-stdin
May 5, 2026
Merged

fix(cli): handle closed plugin uninstall prompt#73566
hxy91819 merged 4 commits into
openclaw:mainfrom
ai-hpc:fix/plugin-uninstall-closed-stdin

Conversation

@ai-hpc

@ai-hpc ai-hpc commented Apr 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: openclaw plugins uninstall <id> could leave the confirmation prompt await unsettled when stdin closed before an answer, causing Node to exit 13 with Detected unsettled top-level await.
  • Why it matters: CI/scripts/detached shells get an internal Node crash instead of a clear CLI error; operators do not learn to use --force for non-interactive uninstall.
  • What changed: promptYesNo() now detects readline close/EOF before an answer and throws a typed prompt-closed error; plugins uninstall catches that case and exits 1 with an actionable message.
  • What did NOT change (scope boundary): Piped y / n answers and plugins uninstall --force remain supported; this does not change install records, uninstall planning, or plugin deletion semantics.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: promptYesNo() awaited readline.question() without handling readline close / EOF, so closed stdin could leave the top-level CLI await unsettled.
  • Missing detection / guardrail: Existing prompt and uninstall tests covered normal answers and --force, but not EOF-before-answer.
  • Contributing context (if known): plugins uninstall already supports --force, but the interactive path did not fail cleanly when no confirmation input was possible.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/cli/prompt.test.ts, src/cli/plugins-cli.uninstall.test.ts
  • Scenario the test should lock in: readline close before an answer rejects with PromptInputClosedError; plugins uninstall converts that specific prompt failure into exit 1 without config/index/file mutation.
  • Why this is the smallest reliable guardrail: The bug is in the prompt helper and uninstall command handler; focused tests cover both the shared primitive and command-level UX.
  • Existing test that already covers this (if any): None.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

openclaw plugins uninstall <id> < /dev/null now fails with a clear CLI error instead of surfacing Node's unsettled top-level-await warning. Users can rerun interactively, pipe y/n, or pass --force for non-interactive uninstall.

Diagram (if applicable)

Before:
closed stdin -> readline question never answers -> unsettled top-level await -> Node exit 13

After:
closed stdin -> prompt close detected -> plugins uninstall exits 1 with --force guidance

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Ubuntu/Linux
  • Runtime/container: Node 22.22.1, pnpm dev checkout
  • Model/provider: N/A
  • Integration/channel (if any): Plugins CLI
  • Relevant config (redacted): A managed plugin entry or install record for the uninstall target

Steps

  1. Ensure plugins uninstall <id> targets a managed plugin entry or install record.
  2. Run openclaw plugins uninstall <id> < /dev/null without --force.
  3. Observe the command result.

Expected

  • CLI exits 1 with an actionable confirmation-input error.
  • No config, install index, or plugin file mutation occurs.

Actual

  • Before this change, the prompt path could surface Node's Detected unsettled top-level await and exit 13.
  • After this change, the closed-input case is detected and handled before mutation.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Local verification:

  • pnpm test src/cli/prompt.test.ts src/cli/plugins-cli.uninstall.test.ts -- --reporter=verbose
  • pnpm check:changed -- --base upstream/main
  • git diff --check HEAD
  • Direct closed-stdin smoke against patched promptYesNo(): returns PromptInputClosedError with process exit 0 in the harness instead of Node exit 13.

Human Verification (required)

  • Verified scenarios: Prompt close before answer; uninstall prompt-close command path; existing --force uninstall path; normal prompt default/y/n behavior.
  • Edge cases checked: The fix avoids a blanket non-TTY ban, so piped answers remain supported by design.
  • What you did not verify: Full packaged pnpm openclaw plugins uninstall <id> < /dev/null against a real installed plugin, because the focused prompt and CLI seams directly cover the root cause.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: promptYesNo() is shared by a few CLI flows, so changing EOF handling can affect other confirmation prompts.
    • Mitigation: The new behavior only triggers when readline closes before an answer; normal answers, defaults, and global --yes remain covered by existing prompt tests.

Real behavior proof

Behavior or issue addressed: Closes #73562openclaw plugins uninstall <id> previously left the readline confirmation await unsettled when stdin closed before an answer (CI lanes, scripts, detached shells, < /dev/null). Node bailed with exit code 13 and the runtime warning "Detected unsettled top-level await", instead of a clean actionable CLI error.

Real environment tested: macOS Darwin 25.2.0 (arm64), Node 24.15.0 (Homebrew node@24), local OpenClaw source checkout at /Users/a1111/openclaw/ running directly via the source-aware launcher (node openclaw.mjs) and via node --import tsx against the patched src/cli/prompt.ts source — no test harness, no readline mocking.

Exact steps or command run after this patch:

  1. Checked out the patched branch fix/plugin-uninstall-closed-stdin (HEAD 4cd1aed4e9, rebased onto upstream b8f9137d31).
  2. Confirmed the source-aware launcher reports the patched build: node openclaw.mjs --versionOpenClaw 2026.5.4 (4cd1aed).
  3. Invoked the production promptYesNo directly, with stdin pushed null after 200ms to mirror a closed-pipe / < /dev/null scenario, wrapped in the same PromptInputClosedError catch block that plugins-uninstall-command.ts uses.

Evidence after fix:

Live terminal capture from the patched source running on my Mac:

$ node --import tsx --input-type=module -e "
import { promptYesNo, PromptInputClosedError } from './src/cli/prompt.ts';
setTimeout(() => process.stdin.push(null), 200);
try {
  const ans = await promptYesNo('Uninstall plugin \"demo\"?');
  console.log('answer:', ans);
} catch (e) {
  if (e instanceof PromptInputClosedError) {
    console.error('Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.');
    process.exit(1);
  }
  throw e;
}
"
Uninstall plugin "demo"? [y/N] Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.
exit: 1

For comparison, the same invocation against the original (pre-patch) prompt.ts — which has no PromptInputClosedError class and no questionUntilClose close-listener — never settles the await. Node prints Detected unsettled top-level await ... exit code 13, which is the original symptom from #73562.

The patched runtime path: prompt.ts questionUntilClose listens for the readline close event, rejects the awaiting promptYesNo() with a typed PromptInputClosedError, the plugins uninstall command's try/catch matches by type, prints the actionable hint, and runtime.exit(1).

Observed result after fix:

The CLI process now (a) renders the prompt as before, (b) detects readline EOF/close, (c) raises a typed PromptInputClosedError, (d) prints Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force. to stderr, and (e) exits with code 1. Node's "Detected unsettled top-level await" warning and exit code 13 from #73562 no longer occur on this code path.

What was not tested:

  • Live uninstall against a clawhub-installed managed plugin (no installRecords populated on this dev machine — exercise was against the production prompt module directly via a synthetic close-stdin signal that drives the same readline close event the production code listens for).
  • Windows / Linux behavior — only macOS exercised; Node readline close event semantics are platform-consistent per Node docs, but not separately confirmed live on those platforms.
  • Race where an answer arrives within microseconds of the close event — the settled guard in questionUntilClose covers this but was not separately exercised live.

@openclaw-barnacle openclaw-barnacle Bot added cli CLI command changes size: S labels Apr 28, 2026
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a Node.js exit 13 / unsettled top-level await crash when plugins uninstall is run without a TTY (e.g., < /dev/null in CI). The fix adds a questionUntilClose() helper that races readline.question() against the interface's close event, rejecting with a typed PromptInputClosedError on EOF-before-answer. The uninstall command catches that specific error and exits 1 with actionable guidance.

The implementation is well-scoped and correct: the settled flag in questionUntilClose prevents double-resolution, listener cleanup is handled on both code paths, and the .finally(() => rl.close()) in promptYesNo ensures the interface is always released. The previous review feedback about preferring instanceof over name-string matching has been addressed — isPromptInputClosedError now uses instanceof directly.

Confidence Score: 5/5

Safe to merge — targeted bug fix with correct implementation and good test coverage.

No P0 or P1 issues found. The logic in questionUntilClose is correct: the race between rl.question() and the close event is properly serialized by the settled flag, listener cleanup is handled on both paths, and the double-close in .finally() is safe since Node's readline ignores close() on an already-closed interface. Previous review feedback (use instanceof instead of name-string matching) has been fully addressed. Test coverage spans both the prompt primitive and the command-level UX path.

No files require special attention.

Reviews (2): Last reviewed commit: "fix(cli): match closed prompt errors by ..." | Re-trigger Greptile

Comment thread src/cli/plugins-cli.ts Outdated
Comment thread src/cli/plugins-cli.uninstall.test.ts Outdated
@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Summary
The PR adds EOF-aware promptYesNo() handling, catches that typed error in plugins uninstall with exit-1 guidance, and adds focused prompt/uninstall regression tests.

Reproducibility: yes. Current main reaches promptYesNo() from plugins uninstall without --force, and the helper directly awaits readline.question(); a minimal closed-stdin readline command reproduces Node exit 13 without mutating OpenClaw state.

Real behavior proof
Sufficient (terminal): The PR body includes after-fix terminal output from a real source checkout invoking the production prompt module and observing the improved exit-1 behavior.

Next step before merge
A narrow automated repair can add the missing changelog entry without changing the functional patch.

Security
Cleared: The diff only changes CLI prompt/uninstall handling and colocated tests, with no dependency, workflow, secret, permission, download, publishing, or generated/vendor surface changes.

Review findings

  • [P3] Add the changelog entry for this CLI fix — src/cli/plugins-uninstall-command.ts:158-160
Review details

Best possible solution:

Merge the EOF-aware prompt/uninstall handling after adding a single-line Unreleased Fixes changelog entry, then let the merged PR close #73562.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main reaches promptYesNo() from plugins uninstall without --force, and the helper directly awaits readline.question(); a minimal closed-stdin readline command reproduces Node exit 13 without mutating OpenClaw state.

Is this the best way to solve the issue?

Yes for the functional patch. Prompt-level EOF detection preserves piped answers while the uninstall handler adds narrow --force guidance; the remaining issue is the missing changelog entry, not the code strategy.

Full review comments:

  • [P3] Add the changelog entry for this CLI fix — src/cli/plugins-uninstall-command.ts:158-160
    This user-facing CLI fix changes openclaw plugins uninstall <id> with closed confirmation input from a Node exit 13/unsettled top-level-await failure into a clear OpenClaw error. Repo policy requires an active-version CHANGELOG.md Fixes entry, but the latest PR file list still only touches CLI source and tests.
    Confidence: 0.92

Overall correctness: patch is correct
Overall confidence: 0.9

Acceptance criteria:

  • git diff --check upstream/main...HEAD
  • pnpm test src/cli/prompt.test.ts src/cli/plugins-cli.uninstall.test.ts -- --reporter=verbose

What I checked:

  • Current main prompt awaits readline without close handling: At current main, promptYesNo() creates a readline interface and directly awaits rl.question(...), with no listener for readline close or EOF before the await settles. (src/cli/prompt.ts:16, 3f9e64869a31)
  • Current main uninstall reaches the prompt before mutation: runPluginUninstallCommand() calls promptYesNo() when --force is absent, after planning but before config/index writes and directory removal. (src/cli/plugins-uninstall-command.ts:145, 3f9e64869a31)
  • Dependency behavior reproduced: A minimal node:readline/promises closed-stdin command on local Node v24.14.1 emitted the unsettled top-level-await warning and exited 13, matching the reported failure mode.
  • PR implements narrow typed handling: The PR head exports PromptInputClosedError, races rl.question() against readline close, and catches that exact class in plugins uninstall to print the --force guidance and exit 1. (src/cli/plugins-uninstall-command.ts:157, 4cd1aed4e95f)
  • Regression coverage is focused: The PR adds prompt-level close-before-answer coverage and command-level uninstall coverage asserting exit 1 plus no config/index/registry/file mutation. (src/cli/plugins-cli.uninstall.test.ts:152, 4cd1aed4e95f)
  • Changelog entry is still missing: The latest PR file list contains only CLI source/test files, and the PR-head CHANGELOG.md has no new active Fixes entry for the plugin uninstall closed-input behavior. (CHANGELOG.md:69, 4cd1aed4e95f)

Likely related people:

  • steipete: GitHub commit history shows this handle introduced src/cli/prompt.ts, added src/cli/plugins-uninstall-command.ts, and recently touched plugin management/changelog surfaces. (role: introduced behavior and recent maintainer; confidence: high; commits: a89d7319a97f, eee3aeae00fd, d678bcfcc7d0; files: src/cli/prompt.ts, src/cli/plugins-uninstall-command.ts, CHANGELOG.md)
  • vincentkoc: Recent current-main history shows plugin uninstall teardown maintenance in the same command path shortly before this PR. (role: recent adjacent maintainer; confidence: medium; commits: 336303e48bf8; files: src/cli/plugins-uninstall-command.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 3f9e64869a31.

@ai-hpc ai-hpc force-pushed the fix/plugin-uninstall-closed-stdin branch from b001327 to af9af82 Compare April 28, 2026 16:51
@ai-hpc

ai-hpc commented Apr 28, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps

@ai-hpc ai-hpc force-pushed the fix/plugin-uninstall-closed-stdin branch from af9af82 to 0fd8a58 Compare April 28, 2026 18:53
@ai-hpc

ai-hpc commented Apr 28, 2026

Copy link
Copy Markdown
Member Author

Human verification update for a5e248d5989a0870d00630ae030509ce832980b1:

Verified scenarios:

  • Rebased fix/plugin-uninstall-closed-stdin onto current main without conflicts and pushed the rebased branch.
  • pnpm test src/cli/prompt.test.ts src/cli/plugins-cli.uninstall.test.ts -- --reporter=verbose passed: 2 files, 9 tests.
  • pnpm check:changed passed. It selected lanes=all because the rebased fork branch history made the changed-lane detector conservative; completed checks included conflict markers, changelog attributions, guarded/plugin-sdk wildcard re-export guards, runtime sidecar loader guard, tsgo:all, oxlint shards, and runtime import cycles.
  • git diff --check upstream/main...HEAD passed.
  • GitHub currently reports mergeable: true, mergeable_state: clean.

Edge cases checked:

  • Closed plugin uninstall prompt exits cleanly instead of leaving an unsettled top-level await.
  • promptYesNo rejects closed input before an answer.
  • Existing plugin uninstall paths remain covered: --force, dry-run, rollback after config write failure, delayed file removal, and unmanaged-target exit.

What I did not verify:

  • I did not run a live Gateway or provider-backed scenario; this PR is limited to local CLI prompt/uninstall behavior.
  • I did not test on macOS or Windows locally.

Re-review progress:

@ai-hpc ai-hpc force-pushed the fix/plugin-uninstall-closed-stdin branch from 0fd8a58 to a5e248d Compare April 29, 2026 00:52
@ai-hpc ai-hpc force-pushed the fix/plugin-uninstall-closed-stdin branch 3 times, most recently from fffbac7 to 4cd1aed Compare May 5, 2026 07:31
@openclaw-barnacle openclaw-barnacle Bot added triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 5, 2026
@hxy91819 hxy91819 force-pushed the fix/plugin-uninstall-closed-stdin branch from 4cd1aed to 93487c8 Compare May 5, 2026 15:00
@hxy91819 hxy91819 force-pushed the fix/plugin-uninstall-closed-stdin branch from 93487c8 to d754ddc Compare May 5, 2026 15:04
@hxy91819 hxy91819 merged commit a387068 into openclaw:main May 5, 2026
20 checks passed
@hxy91819

hxy91819 commented May 5, 2026

Copy link
Copy Markdown
Member

Merged via squash.

Thanks @ai-hpc!

@ai-hpc

ai-hpc commented May 7, 2026

Copy link
Copy Markdown
Member Author

Verified on installed OpenClaw 2026.5.6 (e301533).

Real CLI smoke test:

  1. Created an isolated temporary OPENCLAW_HOME.
  2. Installed a minimal managed local plugin with openclaw plugins install <path> --link.
  3. Ran openclaw plugins uninstall closed-stdin-demo < /dev/null.

Observed result:

Plugin: Closed Stdin Demo (closed-stdin-demo)
Will remove: config entry, install record, load path
Uninstall plugin "closed-stdin-demo"? [y/N] Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.

This confirms the installed CLI reaches the confirmation path and exits cleanly with the actionable error instead of Node's previous unsettled top-level await / exit 13 behavior.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Merged via squash.

Prepared head SHA: d754ddc
Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Merged via squash.

Prepared head SHA: d754ddc
Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
Merged via squash.

Prepared head SHA: d754ddc
Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Merged via squash.

Prepared head SHA: d754ddc
Co-authored-by: ai-hpc <183861985+ai-hpc@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: plugins uninstall can crash with exit 13 when confirmation stdin is closed

2 participants