fix(cli): prevent silent VP-mode crash by adding uncaughtException handler and error visibility - #8088
Conversation
…ndler 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
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR — and thanks to @wenshao for the two rounds of real-pty A/B validation already in this thread. Template: mostly there — Problem: real and reproduced — not theoretical hardening. The linked issues (#7971 #7972 #7779 #7781) report the CLI exiting silently in VP / alternate-screen mode, and the mechanism is concretely demonstrated: Node's default uncaught-exception trace goes to stderr → alternate buffer → discarded when teardown writes Direction: aligned. Crash visibility / diagnostics is squarely within a CLI's core mission, and the scope maps cleanly to the linked issues (uncaughtException visibility, the Kitty signal race in #7779, SIGHUP in #7781). No auth / sandbox / model-selection / telemetry surface touched. Size: not a core-module PR — every change sits under Approach: the scope feels right. Each change ties to a stated goal and a linked issue, and the leaf-module extraction ( Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明感谢贡献,也感谢 @wenshao 已在本帖中完成的两轮真实 pty A/B 验证。 模板: 基本完整 —— 问题: 真实且已复现,不是理论性加固。关联 issue(#7971 #7972 #7779 #7781)报告 CLI 在 VP / 备用屏模式下静默退出,机制已被具体证明:Node 默认的未捕获异常堆栈写到 stderr → 备用缓冲区 → teardown 写 方向: 对齐。崩溃可见性 / 诊断完全属于 CLI 的核心职责,范围也与关联 issue 清晰对应(uncaughtException 可见性、#7779 的 Kitty 信号竞态、#7781 的 SIGHUP)。未触及 auth / sandbox / 模型选择 / telemetry 等敏感面。 规模: 非核心模块 PR —— 所有改动都在 方案: 范围合理。每处改动都对应一个明确目标和某个关联 issue,而叶子模块( 风险: 无升级风险信号 —— 改动文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewMy independent take before reading the diff: for "silent VP crash", add a process-level No critical blockers and no AGENTS.md violations. The one thing that was a blocker — the esbuild entry cycle that turned A few things I checked specifically and was satisfied with:
Non-blocking follow-ups (both already raised by @wenshao, both verified by him, neither affecting the VP path this PR targets):
Files changed (9)
Test evidence (the PR's own CI)This is an unattended run, so I'm quoting the PR's CI rather than executing anything. The previously-red Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The behavioural claim itself does not rest on the author's word: @wenshao (a maintainer, write access) ran two rounds of decisive real-pty A/B validation — fault injected as a genuine 中文说明代码审查我在看 diff 之前的独立方案:针对「VP 静默崩溃」,加一个进程级 无关键阻塞项,也无 AGENTS.md 违规。 唯一曾经构成阻塞的点 —— esbuild entry 环把 我专门核对并放心的几点:监听器恰好一个(先移除基础处理器与上一次 session 的处理器再安装,避免两个监听器互相抢先 非阻塞后续(均由 @wenshao 提出并由其验证,都不影响本 PR 主攻的 VP 路径):其一,处理器退备用屏只由 测试证据(本 PR 自身的 CI)这是无人值守运行,因此我引用 PR 的 CI 而非自行执行任何代码。此前变红的 行为层面的结论并不依赖作者自述:@wenshao(维护者,有写权限)做了两轮决定性的真实 pty A/B 验证 —— 以真正的 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — solid, well-scoped fix that does exactly what it claims; the only reservations are two non-blocking follow-ups already flagged by the maintainer, and the current head's unit CI is still finishing. Stepping back: this PR is honest about what it is — it does not fix the reported crashes, it makes the next one diagnosable — and the problem is real and reproduced, not a hypothesis. The implementation is straightforward and matches what I would have written: one consolidated The reason this is 4/5 and not 5/5 is the I'd maintain this in six months without cursing the author — the comments explain the non-obvious constraints, the tests pin the behaviours that matter (single listener, consume-once echo, SIGHUP 129), and there's no speculative scaffolding. Verdict: approve. I'm not posting the approval this instant because the current head's 中文说明置信度:4/5 —— 一个扎实、范围恰当的修复,做到了它所声称的一切;唯一的保留是维护者已经指出的两个非阻塞后续项,以及当前 head 的单元 CI 仍在收尾。 退一步看:这个 PR 对自身定位很诚实 —— 它不修复上报的崩溃,而是让下一次崩溃可被诊断 —— 而问题是真实且已复现的,不是假设。实现很直接,与我会写的一致:一个合并后的 之所以是 4/5 而非 5/5,是因为 六个月后维护它我不会骂作者 —— 注释解释了非显而易见的约束,测试钉住了关键行为(单一监听器、消费即清的回显、SIGHUP 129),也没有投机性的脚手架。 结论:批准。 我此刻不立即提交批准,因为当前 head 的 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
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.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressedAll six findings were resolved in code in a single commit. No conflicts ( Critical
Suggestions (all implemented)
VerificationCommands actually run (all from a clean
中文说明已处理的评审反馈全部六条发现均已在一次提交中通过代码修复。无冲突( Critical(严重)
Suggestions(建议,全部已实现)
验证实际运行的命令(均基于 runner 执行的干净
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its E2E suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 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
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its E2E suite did not run locally.
[Critical] packages/cli/src/gemini.tsx:185 — the previously-filed blocker (inline comment 3681539981) STILL STANDS at the current HEAD. The session-aware handler is typed (error: Error) => void (line 172) and dereferences error.message / error.stack directly (line 190) with no instanceof Error guard, regressing the basic handleUncaughtException it replaces (cli.ts uses error: unknown + instanceof + String(error) fallback). Node delivers the thrown value as unknown at runtime. Failure scenario: a non-Error reaches the handler — for throw null / throw undefined, isExpectedPtyRaceError returns false (cli.ts:402 guards on instanceof Error), so control reaches line 190 where null.message throws a TypeError inside this last-resort handler, skipping the debug-log write, the alternate-screen leave, the stderr Fatal message and process.exit(1) — the original error is lost and the crash visibility this PR adds is defeated in exactly the VP mode it targets; a thrown string/number logs undefined instead of the value the old handler surfaced via String(error). Fix: widen the stored type to ((error: unknown) => void) and normalize at the top of the handler (const error = rawError instanceof Error ? rawError : new Error(String(rawError));). The other prior blocker (handler conflict, comment 3680065843) is FIXED by this diff — process.removeListener('uncaughtException', handleUncaughtException) (line 180) plus the isExpectedPtyRaceError guard (line 186) resolve both sub-issues.
— qwen3.8-max-preview via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressedAll 8 inline findings (1 Critical, 7 Suggestion) are resolved in this commit. Critical
Suggestions
Verification
中文说明已处理的审查反馈本次提交解决了全部 8 条内联发现(1 条 Critical,7 条 Suggestion)。 Critical
Suggestion
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
Real-environment validation ✅I built this PR merged into current Setup
Faults were injected as genuine asynchronous The core bug, reproduced and fixedOn Results
Row 6 is the specific thing the follow-up commit fixes, measured in the live process rather than inferred: exactly one listener is active at crash time, so the session-aware handler is the one that actually runs. Row 4 confirms the Static checks
Not covered by this validationStating these plainly so the coverage claim isn't overread:
Follow-up finding (non-blocking)The debug-log half of the handler silently no-ops on a fresh machine.
I verified this by isolating the single variable — same build, same injected fault,
Impact is limited — the primary fix (stack trace on the main screen) works in both cases; only the persisted record is lost. Suggested one-liner: fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.appendFileSync(logPath, line, 'utf8');Happy to see this land as-is and fix the above in a follow-up. 🇨🇳 中文版本真实环境验证 ✅我把这个 PR 合并到当前 环境
故障通过 核心问题的复现与修复在 结果
第 6 行正是后续 commit 所修复的问题,这里是在活进程中实测而非推断:崩溃时刻恰好只有一个监听器,因此真正执行的是那个感知 session 的处理器。 第 4 行确认 静态检查
本次验证未覆盖的部分明确说明,避免高估覆盖范围:
后续问题(不阻塞合并)处理器中写调试日志的那一半,在全新机器上会静默失效。
我通过只改变这一个变量做了验证(同一构建、同一注入故障,两次运行都未设置
影响有限 —— 主要修复(堆栈显示在主屏幕)在两种情况下都正常,只有持久化记录会丢失。建议的一行修复: fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.appendFileSync(logPath, line, 'utf8');我认为可以先按现状合并,上述问题在后续 PR 中处理即可。 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
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
|
🤖 AutoFix could not start — a setup step failed (or the run was cancelled) before the agent ran, so no fix was attempted. This is normally a transient infra issue, a broken base build, or a cancelled run — not this PR. It will retry on the next scan. AutoFix failed before producing a verified commit (the run crashed or timed out before it could explain why). Run log: https://github.com/QwenLM/qwen-code/actions/runs/30547201647 🧠 Handled by Qwen Code · model/模型 |
|
🤖 AutoFix could not start — a setup step failed (or the run was cancelled) before the agent ran, so no fix was attempted. This is normally a transient infra issue, a broken base build, or a cancelled run — not this PR. It will retry on the next scan. AutoFix failed before producing a verified commit (the run crashed or timed out before it could explain why). Run log: https://github.com/QwenLM/qwen-code/actions/runs/30551642622 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its E2E suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Round summary — no action takenNo new review feedback (inline comments, reviews, or issue-level comments) was Failed CI check investigationThe
The failure is likely a flaky or environment-specific test unrelated to this 中文说明本轮总结 — 未做任何改动本轮未收到新的评审反馈(行内评论、评审或 Issue 级别评论)。延后的非 Critical 反馈处于仅处理 Critical 模式,按策略未做改动。 CI 失败检查调查
该失败很可能是与本 PR 改动无关的不稳定测试或环境特定测试。建议重新运行 CI。 Deferred non-Critical feedbackCritical-only mode is active after 5 change-producing rounds: the automated reviewer's non-Critical suggestions below are deferred and stay open for human follow-up — do not modify code, resolve threads, or reply on their behalf. Maintainer feedback defers only once its author has already had 2 regular feedback batches addressed in this window's Critical-only tail — an account can host an automated reviewer loop, so the brake keys on measured regeneration, not identity; authors at their budget, if any, are named below. (A maintainer can lift the mode itself:
中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式:以上为自动评审的非 Critical 建议,予以延后、保持开放并留待人工跟进——不要为其修改代码、解决线程或代为回复。维护者的反馈仅在其本人于本窗口 Critical-only 阶段已被处理 2 批常规反馈之后才会被延后——账号可能挂着自动评审循环,因此刹车依据实测的再生频度而非身份;达到预算的作者(如有)在下方点名。(如需解除该模式,评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its E2E suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
|
🤖 AutoFix ran out of time before finishing (timeout (3000000ms)) (attempt 6/100) — it will retry on the next scan. What I found before stopping: See the Qwen Autofix agent step logs for model/tool output. Run log: https://github.com/QwenLM/qwen-code/actions/runs/30601427258 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenNo new review feedback (reviews, inline comments, or issue-level comments) was received this round. The deferred non-Critical suggestions remain deferred under Critical-only mode. Failed CI check: pre-existing, unrelated to this PRThe Evidence this is pre-existing: I restored the The failing test exercises the daemon's Todo Stop Guard retry loop, which is unrelated to this PR's changes (uncaught-exception handler hardening, SIGHUP exit code, ErrorBoundary exit-time echo, and kitty-protocol signal-handler scoping). No code path touched by this PR is involved in the guard's request-count behavior. Verification
中文说明未采取任何操作本轮没有收到新的评审反馈(评审、行内评论或 Issue 级评论)。延后的非 Critical 建议在 Critical-only 模式下继续保持延后状态。 CI 检查失败:属于已有问题,与本 PR 无关
证明这是已有问题的证据: 我将本 PR 修改的所有文件恢复为 失败的测试验证的是守护进程的 Todo Stop Guard 重试循环,与本 PR 的更改(uncaught-exception 处理器加固、SIGHUP 退出码、ErrorBoundary 退出时回显、kitty 协议信号处理器作用域)无关。本 PR 触及的任何代码路径都不涉及 guard 的请求计数行为。 验证
Deferred non-Critical feedbackCritical-only mode is active after 5 change-producing rounds: the automated reviewer's non-Critical suggestions below are deferred and stay open for human follow-up — do not modify code, resolve threads, or reply on their behalf. Maintainer feedback defers only once its author has already had 2 regular feedback batches addressed in this window's Critical-only tail — an account can host an automated reviewer loop, so the brake keys on measured regeneration, not identity; authors at their budget, if any, are named below. (A maintainer can lift the mode itself:
中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式:以上为自动评审的非 Critical 建议,予以延后、保持开放并留待人工跟进——不要为其修改代码、解决线程或代为回复。维护者的反馈仅在其本人于本窗口 Critical-only 阶段已被处理 2 批常规反馈之后才会被延后——账号可能挂着自动评审循环,因此刹车依据实测的再生频度而非身份;达到预算的作者(如有)在下方点名。(如需解除该模式,评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
| export function setupUncaughtExceptionHandler(config: Config) { | ||
| // runCliEntryPoint() registered the basic handleUncaughtException at startup, | ||
| // before the session ID existed. Replace it now: two listeners conflict — the | ||
| // first calls process.exit(1) so the second never runs — and the basic one | ||
| // lacks the debug-log write and the alternate-screen handling below. Also drop | ||
| // any handler a previous call installed so exactly one listener is ever active. | ||
| process.removeListener('uncaughtException', handleUncaughtException); |
There was a problem hiding this comment.
[Suggestion] setupUncaughtExceptionHandler (~60 lines) has no dedicated unit tests despite having multiple testable branches. — Concrete cost: handler replacement logic (two-listener conflict), synchronous debug log write (async abandoned by process.exit), alternate-screen escape (TTY guard), and PTY race suppression could all silently regress. The SIGHUP and render-error echo paths added in the same PR are tested; this function is not.
Suggested tests: (a) PTY race error is suppressed; (b) debug log is written synchronously with correct format; (c) alternate-screen escape sequences are written when stdout.isTTY; (d) escape sequences are skipped when stdout is not a TTY; (e) process.exit(1) is called; (f) previous handler is removed before new one is installed.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Good catch — setupUncaughtExceptionHandler does deserve dedicated coverage for the branches you list (PTY-race suppression, the synchronous debug-log write, the isTTY alternate-screen guard, process.exit(1), and removing the previous handler before installing the new one).
Deferring this one for now: the PR has completed five change-producing rounds and is in critical-only mode, so this round lands only the Critical that was dead-bundling the CLI (the entry↔lazy-module cycle, fixed by moving the helpers into utils/uncaught-exception-handler.ts). Per the repo's review policy, non-Critical suggestions past five rounds are deferred to a follow-up rather than widening the diff here. Leaving this thread open so the six test cases are tracked and not dropped — they'd make a good small follow-up PR.
中文说明
说得对——setupUncaughtExceptionHandler 确实值得为你列出的这些分支补专门的覆盖(PTY 竞态抑制、同步写调试日志、isTTY 备用屏守卫、process.exit(1),以及在安装新处理器前先移除旧处理器)。
先延后这一项:本 PR 已经完成五个产生改动的轮次、进入仅处理 Critical 的模式,因此本轮只落地那个让打包 CLI 失效的 Critical(entry↔懒加载模块成环,已通过将 helper 挪入 utils/uncaught-exception-handler.ts 修复)。按仓库的评审政策,超过五轮后的非 Critical 建议延后到后续处理,以免在此处扩大 diff。保持本线程开放,以便跟踪这六个测试用例、不被丢弃——它们很适合作为一个小的后续 PR。
|
The red Root causeThis line in import { handleUncaughtException, isExpectedPtyRaceError } from './cli.js';
That silently disables the bootstrap at the bottom of if (
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void runCliEntryPoint();
}Inside a chunk, That is exactly what the smoke test reports, three times over: Worth noting how much this breaks: the entire bundled CLI is dead, not just MeasurementsBuilt this PR's head (
Confirmed the mechanism separately on a minimal esbuild reproduction too: with the cycle plus FixMove the two helpers into a leaf module both sides can import. The Add --- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
-import { handleUncaughtException, isExpectedPtyRaceError } from './cli.js';
+import {
+ handleUncaughtException,
+ isExpectedPtyRaceError,
+} from './utils/uncaught-exception-handler.js';--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
+import {
+ handleUncaughtException,
+ isExpectedPtyRaceError,
+} from './utils/uncaught-exception-handler.js';
+
+// Re-exported for existing importers. gemini.tsx must import these from
+// ./utils/uncaught-exception-handler.js directly: a static import of this file
+// from a module the bundle loads lazily makes esbuild hoist this entry into a
+// shared chunk, which silently disables the bootstrap guard at the bottom.
+export { handleUncaughtException, isExpectedPtyRaceError };and delete the two definitions from Two things that will bite otherwise:
With this applied on top of Follow-upNo CI gate catches this class today, which is why it surfaced as an unrelated-looking daemon failure. I opened #8203 to add one: it asserts, from the esbuild metafile the startup closure checks already read, that 中文说明这里 根因
import { handleUncaughtException, isExpectedPtyRaceError } from './cli.js';
这会悄无声息地废掉 if (
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void runCliEntryPoint();
}在 chunk 里 这正是冒烟测试连报三次的内容: 值得强调影响面:整个打包 CLI 都是死的,不只是 实测数据用真实的
另外用一个最小 esbuild 复现单独验证了机制:加上环并开启 修法把这两个 helper 挪到一个两边都能引用的叶子模块。本 PR 新增的 新增 有两点不注意会踩:
在 后续目前 CI 没有任何门禁能拦住这一类问题,所以它才会以一个看起来毫不相干的 daemon 失败的形式暴露出来。我开了 #8203 来补这道门禁:它从启动闭包检查本来就在读的 esbuild metafile 出发,断言 Reviewed with Claude Code (Opus 5, 1M context) |
Round-2 real-environment validation at
|
| Baseline | main @ 702932cc7c (this PR's merge base) |
| Candidate | PR head 307442d6e9. The head has since moved to 2ea51c4e80, but that is a main merge only — the eight files this PR owns are byte-identical between the two, so everything below still applies. |
| Build | one worktree, one npm ci; both arms built with the same toolchain and the same node_modules. The two dist trees differ in exactly the six files the PR touches — nothing else. |
| Environment | Linux, Node v22.22.2, real pty 118×34 via pty.fork(), TERM=xterm-256color |
| Faults | a genuine synchronous uncaughtException raised from a timer callback via node --import (product code untouched). The two render faults are byte-identical injections applied to both arms (verified by md5sum) and removed afterwards. |
The two gaps from last round, now closed
1. kittyProtocolDetector.ts — removing the SIGTERM/SIGINT handlers
Last time the harness pty did not answer the Kitty capability query, so the protocol was never enabled and the removed handlers were never on the exit path. This round the harness answers CSI ? u itself, so ESC[>1u is genuinely pushed and the teardown path is real.
The flags are pushed once on the main screen at startup (offset 7) and once more on the alternate screen (offset 3278, pushKittyProtocolFlags). On main, the Kitty signal handler pops before Ink leaves the alternate screen, so the pop is spent on a buffer that is about to be discarded — and because disableProtocol() latches on protocolEnabled, the later disableKittyProtocol() in the cleanup chain becomes a no-op. Net result on baseline: zero pops reach the main screen, which is exactly #7779. The PR gets it right on both signals.
I also checked the obvious regression risk — is there a path where the protocol is enabled but installInteractiveSignalHandlers() is not installed? No: both are gated on config.isInteractive(), and the signal handlers are installed first (gemini.tsx:935 before the detection at :941). The process.on('exit') fallback is retained for process.exit() paths.
2. ThinkBody's ErrorBoundary
Driven with a mock provider streaming real reasoning_content, with a markdown render fault forced for the thought text only, then expanded with alt+t.
main: the fault reaches the top-level boundary → the entire TUI is replaced by "Something went wrong while rendering.",FATAL_RENDER_ERRORin the debug log, process exits 1. The session is gone.- PR:
THINK_RENDER_ERRORlogged, 0FATAL_RENDER_ERROR, only the thought block degrades to plain text, the session keeps running and/quitstill exits 0.
One precision note for the PR body: this boundary guards the expanded thought view. While a thought streams collapsed — the default — ThinkBody returns a plain <Text> tail window and never reaches MarkdownDisplay, so "partial markdown during thought streaming" only applies once the user has expanded the thought (alt+t / click / ctrl+o). Worth a one-line wording tweak; the code is right.
Core behaviour re-confirmed at this head
In the captured byte stream the ordering is unambiguous: baseline writes the trace at offset 18629 and only leaves the alternate screen at 18947 — trace inside the discarded buffer. The PR leaves at 18622 and writes the trace at 18695 — on the main screen.
Additional rows re-checked at this head:
mkdirSyncfollow-up — fixed and verified. WithQWEN_DEBUG_LOG_FILEunset and~/.qwen/debugabsent, the PR now creates the directory and writes 1UNCAUGHT_EXCEPTIONentry, so the "(logged to debug file)" wording is truthful on a fresh machine. Baseline: no directory, 0 entries.- Benign pty race — no regression. I made the injector record that it actually threw (
read EIO,code: EIO), then confirmed the session survived to a clean/quitexit 0 in both arms. The suppression genuinely fires rather than the fault silently not happening. - Clean
/quit— no regression. Exit 0 in both arms, no spuriousRendering errorline, so therecordForExitEchoscoping does not produce false positives. - Render-error echo works end-to-end. With a forced React render fault, the PR echoes
Rendering error (logged to debug file): …at offset 18973, after the alternate-screen exit at 18947; baseline emits nothing to the main screen. - Listener count measured live at fault time: 1 in both arms — the consolidation holds.
SIGHUP: baseline is killed by signal 1 (WIFSIGNALED, no exit code, no Kitty pop); the PR exits cleanly with 129 throughrunExitCleanup().
Also verified: the fix survives bundling
process.removeListener('uncaughtException', handleUncaughtException) only works if cli.ts and gemini.tsx resolve the same function object. That is obvious in the per-file tsc output, but not in the shipped esbuild bundle, where the two files land in different chunks — a duplicated copy there would leave two listeners, the basic one would win, and the whole fix would be silently dead in the published artifact.
Checked: handleUncaughtException is defined exactly once (dist/chunks/chunk-6KMALBFR.js) and the gemini chunk imports that binding. A live crash run against the bundle reports 1 listener, the trace on the main screen, and the debug entry written. Same behaviour as the per-file build.
New follow-up (non-blocking): ?1049l is emitted even when this process never entered the alternate screen
The handler gates the alternate-screen exit on process.stdout.isTTY, which is true in plenty of modes that never enter it: -p, --acp, serve, screen-reader mode, and ui.useTerminalBuffer: false. In a plain shell that is harmless — I verified that earlier output stays intact in both arms. But when an outer program owns the alternate screen and shells out to qwen -p, an uncaught exception now tears that screen down: #{alternate_on} goes 1 → 0, the outer program keeps running with its display destroyed, and the crash text lands on top of the user's shell scrollback. Baseline leaves the outer screen intact.
Suggested fix — gate on whether we entered it rather than on isTTY, e.g. have startInteractiveUI set a module-level flag when it renders with alternateScreen: true, and read that flag in the handler. The VP path this PR targets is unaffected either way.
Not covered
Stating these plainly so the coverage claim isn't overread:
- A real Kitty/Ghostty terminal. The harness answers the capability query on a real pty, which drives the same code path and gives byte-exact ordering, but it is not a real terminal's flag stack.
- Windows / macOS. Linux only this round.
- The original user-reported crash from the linked issues. The PR does not claim to fix it — it makes it visible — and I did not reproduce the underlying fault.
Static checks
gemini.test.tsx+ErrorBoundary.test.tsx— 66/66 pass- Full
npm cibuild of all packages, andnpm run bundle— both clean
🇨🇳 中文版本
第二轮真实环境验证(307442d6e9)✅
自上一轮验证以来 HEAD 已经前进(改为传入 Config 的处理器、recordForExitEcho 的收窄、mkdirSync 的后续修复,外加三次 base 合并)。我基于当前 HEAD 重新构建并跑了全部用例,并且补上了上一轮明确未覆盖的两块:kittyProtocolDetector.ts 与 ThinkBody 的 ErrorBoundary。这两块现在都有决定性的 A/B 证据。
**结论:仍然可以合并。**我上轮提出的 mkdirSync 后续问题已经实现并验证通过。文末有一个新的、不阻塞合并的后续问题。
环境
| 基线 | main @ 702932cc7c(即本 PR 的 merge base) |
| 候选 | PR head 307442d6e9。期间 HEAD 已前进到 2ea51c4e80,但那只是一次 main 合并 —— 本 PR 自身的 8 个文件在两者之间逐字节相同,因此下述结论仍然适用。 |
| 构建 | 同一个 worktree、同一次 npm ci;两个 arm 用同一套工具链、同一份 node_modules 构建。两棵 dist 树的差异恰好只有本 PR 改动的那 6 个文件,没有别的。 |
| 环境 | Linux,Node v22.22.2,pty.fork() 起的真实 pty 118×34,TERM=xterm-256color |
| 故障注入 | 通过 node --import 在定时器回调里抛出真正的同步 uncaughtException(未改动任何产品代码)。两处渲染故障是逐字节相同的注入,两个 arm 都打(md5sum 已核对),测完即还原。 |
上轮的两个空白,本轮补齐
1. kittyProtocolDetector.ts —— 移除 SIGTERM/SIGINT 处理器
上一轮的 pty 不会回应 kitty 能力查询,所以该协议从未启用,被移除的处理器也从未出现在退出路径上。本轮 harness 自己回应 CSI ? u,因此 ESC[>1u 是真的被推入了,退出路径是真实的。
标志位在启动时在主屏推入一次(偏移 7),进入备用屏后再推一次(偏移 3278,pushKittyProtocolFlags)。在 main 上,kitty 的信号处理器在 Ink 离开备用屏之前就 pop 了,这一次 pop 消耗在了一个即将被丢弃的缓冲区上;又因为 disableProtocol() 用 protocolEnabled 做了闩锁,清理链里后续的 disableKittyProtocol() 就变成了 no-op。基线的净结果是:主屏一次 pop 都没收到,这正是 #7779。本 PR 在 SIGTERM 和 SIGINT 两条路径上都改对了。
我也检查了最明显的回归风险——是否存在「协议已启用但没装 installInteractiveSignalHandlers()」的路径?没有:两者都由 config.isInteractive() 把关,且信号处理器先装(gemini.tsx:935 在探测的 :941 之前)。process.on('exit') 兜底也保留了。
2. ThinkBody 的 ErrorBoundary
用 mock provider 流式返回真实的 reasoning_content 驱动,仅对思考文本强制一个 markdown 渲染故障,然后用 alt+t 展开:
main:故障一路冒泡到顶层边界 → 整个 TUI 被替换成 "Something went wrong while rendering.",调试日志里是FATAL_RENDER_ERROR,进程退出码 1,会话彻底丢失。- 本 PR:记录
THINK_RENDER_ERROR,FATAL_RENDER_ERROR为 0,只有思考块降级成纯文本,会话继续存活,/quit仍然是 exit 0。
关于 PR 描述的一点精确性建议:这个边界保护的是已展开的思考视图。思考在折叠状态下流式输出时(也就是默认情况),ThinkBody 返回的是纯 <Text> 尾窗,根本不会走到 MarkdownDisplay。所以「思考流式输出过程中的半截 markdown」这一说法只在用户展开之后(alt+t / 点击 / ctrl+o)才成立。代码是对的,建议顺手改一下措辞。
在当前 HEAD 上复核的核心行为
捕获的字节流里顺序非常清楚:基线在偏移 18629 写堆栈,直到 18947 才离开备用屏 —— 堆栈落在了会被丢弃的缓冲区里;本 PR 在 18622 离开、18695 写堆栈 —— 落在主屏上。
其余复核项:
- **
mkdirSync后续修复 —— 已修复并验证。**在QWEN_DEBUG_LOG_FILE未设置、~/.qwen/debug不存在的情况下,本 PR 会创建目录并写入 1 条UNCAUGHT_EXCEPTION,因此 "(logged to debug file)" 这句提示在全新机器上是属实的。基线:目录不存在,0 条记录。 - **良性 pty 竞态 —— 无回归。**我让注入器在抛出时先落一条标记(
read EIO,code: EIO),确认异常确实抛了,然后两个 arm 都活到了/quit并 exit 0。也就是说抑制逻辑是真的生效,而不是故障压根没发生。 - **正常
/quit—— 无回归。**两个 arm 都是 exit 0,都没有多余的Rendering error,说明recordForExitEcho的收窄不会产生误报。 - **渲染错误回显端到端可用。**强制一个 React 渲染故障后,本 PR 在偏移 18973 输出
Rendering error (logged to debug file): …,位于 18947 的备用屏退出之后;基线在主屏什么也没有。 - 崩溃时刻实测监听器数量:两个 arm 都是 1 —— 合并为单一监听器的结论成立。
SIGHUP:基线被信号 1 杀死(WIFSIGNALED,没有退出码,也没有 kitty pop);本 PR 走完runExitCleanup()后干净地以 129 退出。
另外验证:修复在打包产物中依然有效
process.removeListener('uncaughtException', handleUncaughtException) 只有在 cli.ts 和 gemini.tsx 解析到同一个函数对象时才有效。在逐文件的 tsc 产物里这显然成立,但在实际发布的 esbuild bundle 里就不一定了——这两个文件会落到不同 chunk,一旦被复制成两份,就会有两个监听器,先注册的基础处理器胜出,整个修复在发布产物中会静默失效。
已核查:handleUncaughtException 在整个 bundle 中只定义了一次(dist/chunks/chunk-6KMALBFR.js),gemini 那个 chunk 导入的正是这个绑定。对 bundle 实跑一次崩溃:监听器数量 1,堆栈显示在主屏,调试记录已写入 —— 与逐文件构建表现一致。
新的后续问题(不阻塞合并):即使本进程从未进入备用屏,也会发出 ?1049l
处理器里退出备用屏的动作只由 process.stdout.isTTY 把关,而这个条件在很多从不进入备用屏的模式下都为真:-p、--acp、serve、读屏模式,以及 ui.useTerminalBuffer: false。在普通 shell 里这是无害的——我验证过两个 arm 之前的输出都完好。但如果外层程序占用着备用屏并 shell out 去跑 qwen -p,此时发生未捕获异常就会把外层的屏幕拆掉:#{alternate_on} 从 1 变成 0,外层程序还在跑但界面已被破坏,崩溃文本被倾泻到用户的 shell 回滚区上。基线则会保持外层屏幕完好。
建议的修法——按「是不是我们进入的」来判断,而不是按 isTTY:例如在 startInteractiveUI 以 alternateScreen: true 渲染时置一个模块级标志,处理器读这个标志。无论怎么改,本 PR 主攻的 VP 路径都不受影响。
本次未覆盖的部分
明确说明,避免高估覆盖范围:
- **真实的 Kitty/Ghostty 终端。**harness 在真实 pty 上回应了能力查询,走的是同一条代码路径,也给出了逐字节的顺序证据,但它毕竟不是真实终端的标志位栈。
- **Windows / macOS。**本轮只在 Linux 上跑。
- **用户最初上报的崩溃本身。**本 PR 并没有声称修掉它——只是让它可见——我也没有复现底层故障。
静态检查
gemini.test.tsx+ErrorBoundary.test.tsx—— 66/66 通过- 全量
npm ci构建(所有 package)与npm run bundle—— 均通过
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its E2E suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
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.
|
🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #8088Addressed the Critical that was killing the entire bundled CLI. One non-Critical suggestion is deferred under critical-only mode (details below). No base conflict ( Feedback triage
Critical fixed: entry↔lazy-module cycle dead-bundled the CLI (rc:3688709417)
Fix (matches the reviewer's recommended approach):
The Suggestion deferred: unit tests for
|
| # | 来源 | 类型 | 决定 |
|---|---|---|---|
| rc:3688709417 | @qwen-code-ci-bot | Critical | 已修复 — 在代码中解决 |
| rc:3688233617 | @doudouOUC | Suggestion | 延后(仅处理 Critical 模式)— 线程保持开放并附回复 |
| issue 评论 | @wenshao | 根因分析 | 印证了该 Critical;已由下方修复解决 |
| issue 评论 | @wenshao | 第二轮验证 ✅ + 不阻塞的 ?1049l 后续问题 |
验证结论已知悉;不阻塞的后续问题予以延后(本轮范围之外) |
| 失败检查 | Test (ubuntu-latest Node 22.x) |
CI 失败 | 根因即下方 Critical;已修复并通过静态验证 |
已修复的 Critical:entry↔懒加载模块成环导致打包 CLI 失效(rc:3688709417)
gemini.tsx 此前从 ./cli.js 静态导入了 handleUncaughtException 和 isExpectedPtyRaceError。cli.ts 是 esbuild 的 entry,而 gemini.tsx 只会经由 await import('./gemini.js') 懒加载到达。这条反向导入构成了 entry↔懒加载的环;在 splitting: true 下,esbuild 把 entry 的模块体搬进共享 chunk,dist/cli.js 只剩一个 re-export 空壳。于是 cli.ts 底部的 bootstrap 守卫(import.meta.url === pathToFileURL(process.argv[1]).href)在 chunk 内求值、永远不相等,runCliEntryPoint() 一次都不会被调用——打包后的 CLI 启动、什么都不做、exit 0。这正是冒烟测试 daemon exited with 0 before listening 的失败原因;tsc、eslint 和所有基于 src 的单测都是绿的,因为它们跑的是 src/。
修法(与评审建议的方案一致):
- 新增叶子模块
packages/cli/src/utils/uncaught-exception-handler.ts,原样收纳getErrnoCode、isExpectedPtyRaceError和handleUncaughtException。它只从已有的叶子模块./utils/stdioHelpers.js导入writeStderrLine(与cli.ts旧的私有 helper 语义逐字节相同),因此既不引入环,也不产生重复的 helper。 gemini.tsx改为从./utils/uncaught-exception-handler.js导入这两个 helper,不再从./cli.js导入,从而打破环。cli.ts从该叶子模块导入这两个 helper(它仍然通过process.on('uncaughtException', …)注册handleUncaughtException),并将它们 re-export 出去,同时附注释说明为什么gemini.tsx不能导入这个 entry 文件。该 re-export 让cli.test.ts无需改动即可继续工作。cli.ts中的私有writeStderrLine保留,供其余调用方(handleCriticalError、runCliEntryPoint)使用。
本 PR 新增的 uncaughtException 行为完全不变——变的只是这些 helper 放在哪儿。
延后的建议:为 setupUncaughtExceptionHandler 补单测(rc:3688233617)
这是一条真实且有价值的建议(针对 PTY 竞态抑制、同步写调试日志、备用屏转义守卫、process.exit(1) 以及处理器替换分支所列的六个测试用例)。它属于非 Critical,而本 PR 已经完成五个产生改动的轮次,因此在仅处理 Critical 的模式下(AGENTS.md:「只落地 Critical 修复……其余 Suggestion 延后到后续处理」),本轮不予新增,以便专注于打断打包的 Critical。线程保持开放并附回复记录此次延后,避免被静默丢弃;这些测试用例已记录,留待后续补齐。
已知悉的不阻塞后续问题(@wenshao,第二轮)
第二轮真实环境验证结论已知悉(仍可合并;mkdirSync 后续问题已验证)。新提出的不阻塞观察——?1049l 只由 process.stdout.isTTY 把关、而非按本进程是否真的进入过备用屏来判断,因此 qwen -p 内的崩溃可能会拆掉外层程序的备用屏——是真实存在的,但明确不阻塞合并,也不影响本 PR 主攻的 VP 路径。予以延后到后续处理(例如由 startInteractiveUI 置一个模块级「我们已进入备用屏」标志),不在本轮仅处理 Critical 的范围内。
冲突说明
无。--conflict false;未合并 origin/main。
验证
本轮实际运行的命令(除非特别说明,均在仓库根目录):
npm run build— 通过(所有 package 编译成功)。npm run typecheck— 通过(tsc --noEmit干净)。npm run lint— 通过(eslint . --ext .ts,.tsx && eslint integration-tests,干净;新的 kebab-case 文件名通过文件命名规则)。npx vitest run src/cli.test.ts src/gemini.test.tsx(在packages/cli内)— 111 通过(cli.test.ts 53 个,gemini.test.tsx 58 个)。cli.test.ts仍从./cli.js导入isExpectedPtyRaceError,经由 re-export 通过。npm run bundle— 通过;dist/cli.js为 12,470 字节,重新成为一个真正的 entry,而在损坏的 head 上测得的是约 629–719 字节的 re-export 空壳。- 静态 bundle 验证(作为只会执行
dist/cli.js的 CI 冒烟测试的替代手段;未直接运行 CLI):- bootstrap 守卫(
argv[1])存在于dist/cli.jsentry 中,因此runCliEntryPoint()重新可达; handleUncaughtException在整个 bundle 中恰好定义一次(dist/chunks/chunk-QL4TN4HS.js);- entry 和 gemini chunk 都导入同一个绑定(
import { handleUncaughtException, isExpectedPtyRaceError } from "./chunk-QL4TN4HS.js"),因此gemini.tsx中的process.removeListener('uncaughtException', handleUncaughtException)移除的正是 entry 注册的那个函数——单一监听器的合并结论在发布产物中成立(即 @wenshao 指出的不变量)。bundle 中 PTY 竞态字符串的第二处出现是 core 里无关的shellExecutionService.ts,并非重复的 handler。
- bootstrap 守卫(
工作流独立的 CI(包括 Test (ubuntu-latest Node 22.x) 集成冒烟测试)仍是最终的验证门禁。
Deferred non-Critical feedback
Critical-only mode is active after 5 change-producing rounds: the automated reviewer's non-Critical suggestions below are deferred and stay open for human follow-up — do not modify code, resolve threads, or reply on their behalf. Maintainer feedback defers only once its author has already had 2 regular feedback batches addressed in this window's Critical-only tail — an account can host an automated reviewer loop, so the brake keys on measured regeneration, not identity; authors at their budget, if any, are named below. (A maintainer can lift the mode itself: @qwen-code /retry starts a fresh counting window.)
中文说明
完成 5 个产生改动的轮次后进入仅处理 Critical 的模式:以上为自动评审的非 Critical 建议,予以延后、保持开放并留待人工跟进——不要为其修改代码、解决线程或代为回复。维护者的反馈仅在其本人于本窗口 Critical-only 阶段已被处理 2 批常规反馈之后才会被延后——账号可能挂着自动评审循环,因此刹车依据实测的再生频度而非身份;达到预算的作者(如有)在下方点名。(如需解除该模式,评论 @qwen-code /retry 即可开启新的计数窗口。)
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。
Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。
🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview
|
@qwen-code /triage |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
|
Released in v0.21.3. |








What this PR does
Adds a
process.on('uncaughtException')handler and strengthens error visibility in VP (alternate-screen) mode. Related to #7971 #7972 #7779 #7781.This PR does not claim to fix the crashes reported in those issues — it ensures that the next time a crash occurs, the error is captured in the debug log and visible on the terminal, so the actual root cause can be identified and fixed.
Why it's needed
Users report the CLI exiting silently during model streaming with VP mode enabled (
ui.useTerminalBuffer: true) — no error message, no debug log entry, empty stderr.Investigation of a real crash session confirmed:
[ERROR],[FATAL], or exception entry in the debug loguncaughtExceptionhandler exists anywhere inpackages/cli/srcunhandledRejectionhandler are present but only cover React render errors and promise rejections — synchronous exceptions bypass bothThe silence mechanism: in VP mode, Node's default uncaught-exception stack trace goes to stderr → alternate screen buffer → discarded when teardown writes
?1049l. The user sees their shell prompt with no trace of what happened.Changes (6 files, +117/-15)
Core fix:
uncaughtExceptionhandler (gemini.tsx)setupUncaughtExceptionHandler(sessionId)with synchronousfs.appendFileSyncto the debug log (asyncdebugLogger.error()would be abandoned byprocess.exit)?1049l) before writing to stderr, so the error is visible on the main screenVP main-screen error echo (
ErrorBoundary.tsx+startInteractiveUI.tsx)consumeLastRenderError()stores the last caught render error at module levelinstance.unmount()leaves the alternate screen in the cleanup chain, the error is echoed to stderr on the main screenKitty protocol signal handler fix (
kittyProtocolDetector.ts)SIGTERM/SIGINThandlers that raced with the main signal handlers, causing the Kitty pop to land on the wrong screen buffer (bug(cli): VP teardown can leave Kitty keyboard flags enabled on the main screen #7779)SIGHUP handler (
gemini.tsx)SIGHUPalongsideSIGTERM/SIGINTwith exit code 129 (bug(cli): SIGTERM and SIGHUP can leave VP terminal modes active #7781)MarkdownDisplay defense (
ConversationMessages.tsx)ThinkBody'sMarkdownDisplaywrapped withErrorBoundary— partial markdown during thought streaming degrades to plain text instead of crashing the VP treeHow to verify
After this PR, any crash will leave a trace:
[UNCAUGHT_EXCEPTION][FATAL_RENDER_ERROR]Unhandled Promise RejectionNo
--debugflag needed — the debug log file is written by default.