fix(cli): call process.exit(1) in root help fast path error handler (#97793)#97807
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 11:42 AM ET / 15:42 UTC. Summary PR surface: Source 0, Tests +41. Total +41 across 3 files. Reproducibility: yes. Current-main source shows the default fast-path failure handlers only set process.exitCode, and the PR body gives a high-confidence child-process proof shape with an active HTTP server for the after-fix behavior. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land the narrow fast-path exit fix after normal maintainer review, then let the linked issue close after merge. Do we have a high-confidence way to reproduce the issue? Yes. Current-main source shows the default fast-path failure handlers only set process.exitCode, and the PR body gives a high-confidence child-process proof shape with an active HTTP server for the after-fix behavior. Is this the best way to solve the issue? Yes. The fix belongs at the fast-path error boundary: root help should terminate on default failure, while root version should use its existing injected exit seam instead of adding a broader runtime dependency. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 1052652a7168. Label changesLabel justifications:
Evidence reviewedPR surface: Source 0, Tests +41. Total +41 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
…or handlers (openclaw#97793) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
638268f to
2cfba46
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 84cd3aa)
…or handlers (openclaw#97793) (openclaw#97807) The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes openclaw#97793. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 84cd3aa)
Fixes #97793.
What Problem This Solves
The error handler in
tryHandleRootHelpFastPathsetsprocess.exitCode = 1but does not callprocess.exit(1). Since the fast path skips the main runtime teardown, any dangling async handles (open connections, timers, promises) keep the Node event loop alive, hanging the CLI indefinitely instead of returning to the terminal. The same pattern exists intryHandleRootVersionFastPath.Why This Change Was Made
src/entry.ts(tryHandleRootHelpFastPath): Changedprocess.exitCode = 1toprocess.exit(1)so the process terminates immediately after logging the error, regardless of any dangling handles.src/entry.version-fast-path.ts(tryHandleRootVersionFastPath): Changed the default error handler fromprocess.exit(1)toexit(1)— using the localexitvariable that resolves todeps.exit ?? process.exit. This preserves the existing injection seam so that tests and embedded callers that supplyexitcan control shutdown behavior. The success path already routes through the same injectedexithook; the error path now matches.User Impact
CLI no longer hangs when
openclaw --helporopenclaw --versionhits an error during the fast path with open async handles. The exit code remains 1.Evidence
Behavior addressed: Error handlers in root help and root version fast paths only set
exitCode/ bypassed the injectedexitseam, risking CLI hangs from dangling async handles.Real environment tested: Node 24.13.1, local OpenClaw checkout at
/home/0668000787/open-source-pr/openclaw/openclaw.Exact steps or command run after this patch:
Evidence after fix:
Key observations from the terminal output:
process.exit()onErrorinjection —[openclaw] Failed to display help: Error: TEST: config load failedproof-default-exit.mjs:12→entry.ts:168→handleError→process.exit(1)process.exit(1)terminates immediately despite the open HTTP server — exit code 1, not timeout 124[proof] UNEXPECTEDmessage never appears — process was killed byprocess.exit(1)before reaching itBefore fix:
process.exit(1)called directly, bypassing the injectedexitdependency.After fix:
exit(1)uses the localexitvariable (deps.exit ?? process.exit), honoring the injection seam.call exit(1) via injected exit hook when resolveVersion rejects— NEWcall injected onError when provided and resolveVersion rejects— NEWprints version output and skips host handling when container-targeted— existingObserved result after fix: Root-help fast-path default error handler calls
process.exit(1)which terminates the process immediately even with active async handles (open HTTP server, port 44653). The version fast-path error handler routes through the injectedexitdependency, matching the success path, so tests and embedded callers can observe and control exit behavior on both paths.What was not tested: Live E2E with a real
openclaw --helpCLI command hitting the error path (requires corrupting the config-sensitive plugins module at runtime). The fix is verified through a child-process scenario that exercises the DEFAULT error handler (noonErrorinjection) with a dangling HTTP server as the active handle that would otherwise keep the event loop alive.Regression Test Plan
node scripts/run-vitest.mjs src/entry.version-fast-path.test.ts --run— Test Files 1 passed (1), Tests 3 passed (3)node scripts/run-vitest.mjs src/entry.test.ts --run— Test Files 1 passed (1), Tests 21 passed (21)node scripts/run-vitest.mjs src/entry.root-help-fast-path.test.ts --run— Test Files 1 passed (1), Tests 4 passed (4)node scripts/run-oxlint.mjs src/entry.version-fast-path.ts src/entry.version-fast-path.test.ts— exit 0Root Cause
In
tryHandleRootVersionFastPath, the defaultonErrorhandler calledprocess.exit(1)directly instead of routing through the localexitvariable (deps.exit ?? process.exit). The success path already usesexit(0)preserving the injection seam, but the error path bypassed it. IntryHandleRootHelpFastPath, the error handler only setprocess.exitCode = 1without callingprocess.exit(); when dangling async handles exist, the Node event loop stays open and the CLI hangs indefinitely.AI Assistance