fix(cli): surface unhandled rejections and render errors instead of swallowing them - #7406
Conversation
…wallowing them Unhandled promise rejections were emitted to AppEvent.LogError which has no production listener, making crashes completely invisible in debug logs and stderr. Additionally, the main App tree had no top-level ErrorBoundary, so React render errors were caught only by Ink's internal boundary which silently exits via exitPromise.catch(noop). - Write unhandled rejection details to the debug logger so they appear in ~/.qwen/debug/<session>.txt - Wrap the interactive UI tree in an ErrorBoundary that logs fatal render errors and shows a fallback message instead of silent exit
|
Thanks for the PR! Template looks good ✓ Problem: observed bug with code-level evidence. The Direction: aligned — surfacing errors that are currently swallowed is core CLI reliability work. CHANGELOG has no direct reference but the area is clearly relevant. Size: not applicable (no core paths touched; 15 additions / 4 deletions across 2 files in Approach: minimal and well-targeted. One line adds Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,有代码级证据。 方向:对齐——将当前被吞掉的错误暴露出来是 CLI 可靠性的核心工作。 规模:不适用(未触及核心路径;2 个文件共 15 行新增 / 4 行删除,均在 方案:最小化且精准。一行在已有的 rejection 处理器中添加 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewIndependent proposal: given the two described gaps (no listener for Comparison with the diff: the PR does exactly this. No simpler path missed.
Reuse check: reuses the existing Verified claims:
No critical blockers. No AGENTS.md violations. TestingUnit tests (run from Typecheck: Smoke test (tmux, CLI starts, renders, responds ("Hello"), and exits cleanly — the 中文说明代码审查独立方案: 针对描述的两个缺口( 与 diff 对比: PR 完全这样做了。没有遗漏更简路径。
复用检查: 复用现有 声明验证:
无关键阻塞。无 AGENTS.md 违规。 测试单元测试全部通过。类型检查无错误。Smoke 测试确认 CLI 正常启动、渲染、响应并退出—— — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 5/5 — clean across every stage; would merge without hesitation. This is a textbook minimal fix. The problem is real and verified: My independent proposal was identical to what the PR does — I couldn't find a simpler path. The unit tests pass, typecheck is clean, and the smoke test confirms no regression in normal operation. If I had to maintain this in six months, I'd thank the author — silent crashes are a debugging nightmare, and this ensures they leave a trace. 中文说明置信度:5/5 —— 每个阶段都干净;毫不犹豫合并。 这是一个教科书式的最小修复。问题真实且已验证: 我的独立方案与 PR 完全一致——找不到更简路径。单元测试通过,类型检查干净,smoke 测试确认正常运行无回归。 — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
chiga0
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: chunk 1 — no agent reported covering these; nobody read them. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Agent 1a: Line-by-line correctness, Agent 2: Security, Agent 3: Code quality, Agent 4: Performance & efficiency, Agent 5: Test coverage, Agent 6a: Undirected audit — attacker mindset, Agent 6b: Undirected audit — 3 AM oncall mindset, Agent 6c: Undirected audit — six-months-later maintainer, Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
chiga0
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
gwinthis
left a comment
There was a problem hiding this comment.
Architecture Review — PR #7406
Verdict: ✅ Approve — fixes two invisible error paths with minimal changes.
Problem: Errors that go nowhere
| Error class | Before | After |
|---|---|---|
| Unhandled rejection | Emitted to AppEvent.LogError with no listener |
debugLogger.error() before emit |
| Render error (outside TranscriptView) | Ink's internal boundary → silent exit | Top-level ErrorBoundary → [FATAL_RENDER_ERROR] in debug log |
Design insight: Errors must leave a trace
An error that goes nowhere is worse than an error that crashes — at least a crash is visible. The minimum bar for error handling is "write it to the debug log."
The ErrorBoundary placement is correct: it wraps the entire AppWrapper tree (including StrictMode in debug builds), so any render error anywhere in the component tree leaves a trace. The existing ErrorBoundary around TranscriptView remains for its specific recovery behavior; this new one is the last-resort catch-all.
Note
The onError callback logs error.message + componentStack + error.stack — this is the right level of detail for post-mortem debugging without flooding the log during normal operation (it only fires on fatal render errors).
ytahdn
left a comment
There was a problem hiding this comment.
Requesting changes for two failure modes in the new top-level render-error path. The unhandled-rejection logging change itself looks correct.
…d exit path - Normalize non-Error thrown values (strings, null, etc.) to Error instances in ErrorBoundary before logging/rendering, preventing secondary TypeErrors from sanitizeTerminalText(undefined) - Schedule a graceful exit (5s delay) from the fatal render error fallback so the session does not hang under the Kitty keyboard protocol where Ctrl+C is a keypress, not SIGINT
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
— qwen3.7-max via Qwen Code /review
ytahdn
left a comment
There was a problem hiding this comment.
Re-reviewed at 6faf884ee96d0c0b86707e1803fb45d189331c57. Both blocking findings are fixed:
- The fatal render fallback now schedules bounded graceful cleanup followed by exit, so Kitty keyboard protocol sessions cannot remain stuck without a working Ctrl+C path.
- Non-
Errorthrown values are normalized before entering boundary state and beforeonError, with regression coverage for the string case.
I found no new blocking issues. The remaining missing integration coverage for the delayed cleanup/exit and unhandled-rejection logger wiring is non-blocking.
Maintainer E2E Verification ReportVerdict: ✅ Verified — ready to merge Built and tested locally on macOS (Apple Silicon, Node 22) at PR head Test 1: Render Error BoundaryInjected a render-time throw in Terminal output — the ErrorBoundary catches the error and shows a visible fallback instead of a silent exit: Debug log — The process then exits gracefully (exit code 1) after the 5-second cleanup window, rather than hanging without Ctrl+C handling. Test 2: Unhandled Rejection LoggingInjected a delayed Debug log — the rejection is now written to the debug log (previously it was emitted to The CLI continues running after logging the rejection — no crash, no silent exit. Unit TestsCI passes (9 successful, 21 skipped). Local macOS runs hit a pre-existing Test EvidenceFull terminal captures and debug logs:
中文说明维护者 E2E 验证报告结论:✅ 已验证 — 可以合入 在 macOS(Apple Silicon, Node 22)上基于 PR head 测试 1:渲染错误边界通过环境变量控制的测试钩子在 终端输出 — ErrorBoundary 捕获错误并显示可见的兜底消息,而非静默退出: Debug 日志 — 测试 2:未处理 Rejection 日志记录通过环境变量控制的测试钩子注入延迟 Debug 日志 — rejection 现在会写入 debug 日志(此前仅发送到无生产监听器的 单元测试CI 全部通过(9 成功,21 跳过)。本地 macOS 运行遇到 测试证据完整终端捕获和 debug 日志:wenshao/qwen-code 的 — Maintainer E2E verification · macOS · Node 22 · PR head |
…ndler and error visibility (QwenLM#8088) * fix(cli): prevent silent VP-mode crash by adding uncaughtException handler and error visibility VP (alternate-screen) mode swallows all error output: uncaught exceptions write their stack trace to stderr which lands on the alternate screen buffer, then gets discarded when teardown switches back to the primary buffer. The user sees a silent exit with no error message and nothing in the debug log. Root cause: no `uncaughtException` handler existed anywhere in the CLI. PR QwenLM#7406's ErrorBoundary and unhandledRejection handler only cover React render errors and promise rejections — synchronous exceptions bypass both. Changes: - Add `setupUncaughtExceptionHandler()` with sync debug-log write, alternate-screen exit before stderr output, and clean process.exit(1) - Add `consumeLastRenderError()` to ErrorBoundary for VP main-screen echo after unmount leaves the alternate screen - Remove SIGTERM/SIGINT handlers from kittyProtocolDetector.ts that raced with the main signal handlers (QwenLM#7779) - Add SIGHUP handler alongside SIGTERM/SIGINT (QwenLM#7781) - Guard ThinkBody's MarkdownDisplay with per-item ErrorBoundary so partial markdown during thought streaming degrades to plain text Related: QwenLM#7971 QwenLM#7972 QwenLM#7779 QwenLM#7781 * fix(cli): consolidate uncaughtException handler into a single listener (QwenLM#8088) The VP-crash handler added a second uncaughtException listener that conflicted with the pre-existing one in runCliEntryPoint: the first listener's process.exit(1) ran before the second, leaving the visibility feature inert for real errors, and the new listener lacked the PTY-race guard, crashing the session on benign teardown errors. Replace the startup handler with one session-aware listener (PTY guard, isTTY-guarded alternate-screen leave, writeStderrLineSafe), sanitize the inline render-error fallback, gate the exit-time render-error echo on onError, and cover SIGHUP exit code. * fix(cli): harden uncaughtException handler and scope render-error echo (QwenLM#8088) * fix(cli): address review feedback on VP crash handler (QwenLM#8088) - Close over Config instead of sessionId string so the uncaughtException handler reads the current session ID at crash time - Create debug log directory before appending so the write succeeds on fresh installs where ~/.qwen/debug/ does not yet exist - Sanitize error stack with sanitizeTerminalText before writing to stderr to prevent ANSI injection on the persistent main buffer - Add onError debug logging to the ThinkBody ErrorBoundary - Gate the '(logged to debug file)' hint on isDebugLogFileEnabled() so the message is accurate when debug file logging is disabled * fix(cli): move uncaught-exception helpers to a leaf module (QwenLM#8088) gemini.tsx statically imported handleUncaughtException and isExpectedPtyRaceError from cli.ts, the esbuild entry point. gemini.tsx is only ever loaded lazily, so that import formed an entry<->lazy cycle; with splitting enabled esbuild hoisted the entry body into a shared chunk and left dist/cli.js as a re-export stub. The bootstrap guard at the bottom of cli.ts then never ran and the bundled CLI exited 0 silently. Move the helpers (and the private getErrnoCode) into a new leaf module, utils/uncaught-exception-handler.ts, imported by both cli.ts and gemini.tsx. cli.ts re-exports them so existing importers (cli.test.ts) are unaffected, and the handler reuses the shared writeStderrLine from stdioHelpers.ts. The uncaughtException behavior itself is unchanged. --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
What this PR does
When the interactive CLI encounters an unhandled promise rejection or a React render error, the error is now written to the debug log and (for render errors) displayed in the terminal UI. Previously both classes of errors were completely invisible: unhandled rejections were emitted to an event with no production listener, and render errors were caught only by Ink's internal error boundary which silently exits the process.
Why it's needed
Users have reported the CLI process exiting silently during model API streaming — no error message in the terminal, no entry in the debug log, empty stderr. Investigation confirmed two code paths that swallow errors:
The
unhandledRejectionhandler constructs a detailed error message ("CRITICAL: Unhandled Promise Rejection! Please file a bug report...") and emits it toAppEvent.LogError, but no production code listens for that event. The error goes nowhere — not to the debug logger, not to stderr, not to the UI.The main App tree has no top-level
ErrorBoundary(the existing one only wrapsTranscriptView). When a render error occurs outside that subtree, Ink's internal boundary catches it and callsexitPromise.catch(noop), causing the process to exit with no error trace.Together these make it impossible to diagnose certain crash scenarios. This PR ensures both error classes leave a trace in the debug log, and render errors show a visible fallback message instead of a silent exit.
Reviewer Test Plan
How to verify
Unhandled rejection logging: set
QWEN_DEBUG_LOG_FILE=1, trigger an unhandled rejection (e.g. via a misbehaving MCP server or extension), and confirm the error appears in~/.qwen/debug/<session-id>.txtwith the[STARTUP]tag and full stack trace.Render error boundary: temporarily introduce a render-time throw in a top-level component (e.g.
AppContainer), launch the interactive CLI, and confirm: (a) the debug log contains[FATAL_RENDER_ERROR]with the error and component stack, and (b) the terminal shows "Something went wrong while rendering" instead of a silent exit.Existing tests pass:
cd packages/cli && npx vitest run src/gemini.test.tsx -t "unhandled"andnpx vitest run src/ui/components/shared/ErrorBoundary.test.tsx.Evidence (Before & After)
Before: unhandled rejection during streaming → process exits silently, debug log ends abruptly at the last successful entry, stderr empty.
After: unhandled rejection →
[ERROR] [STARTUP] CRITICAL: Unhandled Promise Rejection! ...written to debug log. Render error →[ERROR] [STARTUP] [FATAL_RENDER_ERROR] ...in debug log + visible error message in terminal.Tested on
Environment (optional)
Unit tests only (
vitest). No runtime environment needed.Risk & Scope
ErrorBoundarywrapping the full App tree will catch render errors that previously crashed the process. This is strictly better (visible error + debug log vs silent exit), but the fallback UI is minimal by design.Linked Issues
N/A — discovered during user session debugging.
中文说明
本 PR 做了什么
当交互式 CLI 遇到未处理的 Promise rejection 或 React 渲染错误时,错误现在会写入 debug 日志,并且(对于渲染错误)在终端 UI 中显示。此前这两类错误完全不可见:未处理的 rejection 被发送到一个没有生产环境监听器的事件上,而渲染错误仅被 Ink 内部的 error boundary 捕获后静默退出进程。
为什么需要
用户报告 CLI 进程在模型 API 流式输出期间静默退出——终端无错误信息、debug 日志无记录、stderr 为空。调查确认了两条吞掉错误的代码路径:
unhandledRejection处理器构造了详细的错误消息("CRITICAL: Unhandled Promise Rejection! Please file a bug report...")并将其发送到AppEvent.LogError,但生产代码中没有任何监听器。错误无处可去——不写 debug logger、不写 stderr、不显示在 UI 中。主 App 树没有顶层
ErrorBoundary(现有的只包裹了TranscriptView)。当渲染错误发生在该子树之外时,Ink 内部的 boundary 捕获它并调用exitPromise.catch(noop),导致进程无任何错误痕迹地退出。这两个问题使得某些崩溃场景完全无法诊断。本 PR 确保两类错误都会在 debug 日志中留下痕迹,渲染错误还会显示可见的兜底消息而非静默退出。
风险与范围
ErrorBoundary会捕获以前会导致进程崩溃的渲染错误。这严格来说更好(可见错误 + debug 日志 vs 静默退出),但兜底 UI 按设计是最简的。