fix(core): Reclaim command hook process trees - #10100
Conversation
Own command-hook process groups and terminate descendants on timeout or cancellation while preserving bounded output draining. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
E2E test reportEnvironment: macOS 26.4.1 (25E253), Node.js v22.22.3, npm 10.9.8. Verified a real POSIX command-hook tree whose descendant records SIGTERM and ignores it. On cancellation, the root exited first, the descendant remained in the owned process group, group SIGKILL ran after the two-second grace period, and neither PID was running when the Hook result returned. Repeated real-process runs passed without residual PIDs or temporary files. Verified cancellation-time output draining with a real child that writes final stdout and stderr from its SIGTERM handler before closing. Both streams were preserved in the cancellation result. The bounded fallback for a child that never emits Focused verification: Not tested on real Windows or Linux hosts. The repository-wide build is currently blocked by unrelated existing Ink selection type errors under |
|
Re-run after the round-5 fix — the gate verdict stands, updated for the new head Template: complete — all required sections present, including the bilingual summary ✓ Problem: exists, verified in the current code. Direction: aligned. The claude-code CHANGELOG has shipped this exact class of fix (orphaned Bash-tool process trees, SIGTERM-before-SIGKILL teardown), and this repo's shell tool already spawns Size: touches Approach: still minimal. The round-5 commit is exactly what the round-5 Critical (R5-1) asked for — fire-and-forget hooks exempted from the parent-exit fallback, plus the requested process-level survival test — and nothing else: +9/−2 production lines, +92 test lines, no drive-by edits. Earlier deferrals (taskkill dedup, cross-package tree-kill consolidation, observability logging) remain genuinely out of scope for this bugfix. Risk: no match on the revert-correlated high-risk paths. The earlier tradeoff note still applies: with Moving on to code review. 🔍 中文说明复审(第 5 轮修复之后的重新运行)——门槛结论不变,按新 head 模板:完整 —— 所有必需章节齐全,包含双语说明 ✓ 问题:真实存在,已在当前代码中核实。 方向:对齐。claude-code 的 CHANGELOG 已上线过同类修复(Bash 工具进程树孤儿化、先 SIGTERM 后 SIGKILL 的清理顺序),本仓库 shell 工具也早已采用 规模:触及 方案:仍然是最小改动。第 5 轮提交恰好是第 5 轮 Critical(R5-1)所要求的内容——即发即忘 Hook 从父进程退出兜底中豁免,外加所要求的进程级存活测试——别无其他:生产代码 +9/−2 行、测试 +92 行,无夹带改动。此前的推迟项(taskkill 去重、跨包进程树回收整合、可观测性日志)仍然确实超出本 bug 修复的范围。 风险:未命中与 revert 相关的高风险路径。此前的权衡提示仍然适用: 进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewRe-run after the round-5 fix. The full-diff audit from earlier rounds (termination-flow race safety, TERM→KILL escalation gating, drain bound, Windows taskkill path, parent-exit registry lifecycle) still holds; this pass verified the incremental +9/−2 production lines in
One non-blocking nit, recorded per the round-6 convergence posture rather than requested as a change: the design doc's fallback paragraph still says the exit path kills "every active hook group" without mentioning that fire-and-forget hooks are now exempt from registration — worth a sentence in a follow-up. sequenceDiagram
participant P1 as Hook caller
participant P2 as HookRunner
participant P3 as Termination op
participant P4 as Process group
participant P5 as Parent exit path
P1->>P2: run command hook
P2->>P4: spawn detached (owns group)
alt not a fire-and-forget hook
P2->>P5: register group
end
alt timeout or abort
P1->>P2: abort or timeout
P2->>P3: start once, idempotent
P3->>P4: SIGTERM group
P3->>P3: poll liveness, up to 2 seconds
alt group exits
P3-->>P2: done
else group still alive
P3->>P4: SIGKILL group
P3-->>P2: done
end
P2->>P2: drain close, up to 1 second
P2-->>P1: cancelled or timeout result
else parent exits or unhandled signal
P5->>P4: SIGKILL every registered group
P5->>P5: re-raise signal when no app handler
end
P2->>P5: unregister when hook settles
TestingUnattended run — PR code is never executed here; the evidence below is the PR's own CI on the reviewed commit, read via the API. CI has settled: all checks that ran are green, including
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Evidence notes, same shape as earlier rounds: Sandboxed verification would settle the last general gap: 中文说明代码审查:本轮针对第 5 轮 Critical(R5-1)验证 测试:无人值守运行,此处不执行 PR 代码;以上证据为通过 API 读取的该提交自身 CI。CI 已收敛:实际运行的检查全部为绿,其中 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — the round-5 Critical is fixed the right way and pinned by a test that provably fails without it; docking the last point for the Windows path still resting on mocked coverage and a one-sentence design-doc lag. Stepping back: this is the sixth look at this PR, and the picture at Remaining reservations, none blocking: the Windows tree-kill has never run on a real Windows host (mocked unit tests only, and no lane here can change that), and the design doc's fallback paragraph hasn't caught up with the exemption — recorded as a follow-up, not a change request, per the convergence posture at this round count. The deferred Suggestions (taskkill dedup, cross-package tree-kill consolidation, observability logging, Windows parent-exit gap) stay deferred with their reasons in-thread. Verdict: approve. CI is settled green on the reviewed commit, so the approval is posted now, pinned to 中文说明置信度:4/5 —— 第 5 轮 Critical 以正确方式修复,并由一个缺少修复便必然失败的测试钉住;扣掉一分是因为 Windows 路径仍只有 mock 覆盖,且设计文档还差一句话。 退一步看:这是本 PR 的第六次审查, 剩余保留意见(均不阻塞):Windows 进程树回收从未在真实 Windows 主机运行过(仅 mock 单测,此处也没有任何通道能改变这一点);设计文档的兜底段落尚未跟上豁免改动——按当前轮次的收敛姿态记为后续跟进,不作为修改要求。被推迟的建议(taskkill 去重、跨包进程树回收整合、可观测性日志、Windows 父进程退出缺口)保持推迟,理由均在主题中。 结论:通过。该提交的 CI 已全绿收敛,批准即时发布,钉住 — Qwen Code · qwen3.8-max Reviewed at |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 844 passed · 0 failed · 844 total Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:844 通过 · 0 失败 · 844 总计 抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #10100 verification — fix(core): Reclaim command hook process treesVerdict: 中文摘要
Central claim and A/B proofCentral claim: on timeout or cancellation, a command hook's entire process tree is reclaimed (SIGTERM to the owned POSIX process group, escalation to SIGKILL after a 2 s grace) before the result returns; the base only signalled the direct child and let descendants survive. Harness
All 40 A/B assertions and 6 S7 assertions passed on both arms (head 22+3, base 18+3; three consecutive runs, deterministic). The base arm's expected-broken cells (leak, hang, ignored pre-abort) are encoded as expectations, so their reproduction counts as a pass. Secondary claims
Vacuity and attributionBase attribution: the PR's 49 hook tests run against the base build give 10 failed / 39 passed — the 10 reds are exactly the new cancellation tests, including the real-process test Mutation matrix (scratch worktree at head, one mutation at a time, file restored between runs; witness
No mutant regressed a killed-to-survived test relative to the control; the M5 survivor is real but bounded (F1). FindingsF1 — Suggestion (coverage gap, not blocking): the F2 — Nice to have: the non-ESRCH F3 — Note (design semantics, by construction): Not covered
MethodologyEnvironment: CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
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 explored to full depth (tool budget reached): "agent 4": none — no checks were cut short.; "agent 1a": none — no check was cut short. One caveat: I did not run the Windows-path behavior on a real Windows host (mocked-unit coverage only, which is what the PR provi…; "agent 6b": none — but disclosing two checks I did not finish for honesty: (1) I did not trace runExitCleanup 's registered callbacks to determine whether they abort in-fl….
中文说明
未探索到全部深度(达到工具调用预算):"agent 4":none — no checks were cut short.;"agent 1a":none — no check was cut short. One caveat: I did not run the Windows-path behavior on a real Windows host (mocked-unit coverage only, which is what the PR provi…;"agent 6b":none — but disclosing two checks I did not finish for honesty: (1) I did not trace runExitCleanup 's registered callbacks to determine whether they abort in-fl…。
— qwen3.8-max via Qwen Code /review (v0.22.0)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Addressed the latest review round in
Verification: 57 focused tests passed; core ESLint, typecheck, build, formatting, and diff checks passed. Independent process verification confirmed preserved exit semantics and full root/descendant cleanup. Replied to all 8 review threads; resolving those handled threads now. |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 911 passed · 0 failed · 911 total Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:911 通过 · 0 失败 · 911 总计 抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #10100 verification (round 2) — fix(core): Reclaim command hook process treesVerdict: This is a follow-up round. The PR gained a second commit ( 中文 — 判定:✅ 通过 · 可合入(agent 判定)· 第 2 轮
Previous-finding status (round 1 → this head)
Central claim and A/B proofCentral claim: on timeout or cancellation, a command hook's entire process tree is reclaimed (SIGTERM to the owned POSIX process group, escalation to SIGKILL after a 2 s grace) before the result returns; base signalled only the direct child and leaked descendants. Harnesses
A/B assertions: S1–S7 head 28 + base 24 = 52; S8–S9 head 15 + base 15 = 30. All base-arm expected-broken cells (leak, hang, ignored pre-abort) are encoded as expectations, so their reproduction counts as pass. Secondary claims
Vacuity and attributionBase attribution (PR's test files staged into the base worktree, run against base source; witness Mutation matrix at head (scratch worktree, one hunk per mutant, source restored between runs —
No mutant regressed a killed-to-survived test relative to the control. The M1/M2/M6 red sets extend beyond the minimally expected tests for mechanically verified reasons (noted in the table) — layered guards, not collateral damage: FindingsF1 — Suggestion (stands from round 1, coverage gap, not blocking): the F2 — Note (downgraded from round 1's Nice-to-have): the EPERM F3 — Note (design semantics, mitigated): No new findings this round. Not covered
MethodologyEnvironment: CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
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.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/hooks/hookRunner.ts:91 — [probe] Where group signalling is denied, every cancellation pays the full 2 s grace for nothing
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review round 2 handled at
Verification:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- WINDOWS_TASKKILL constant and taskkill-call duplication — already reported (comment 3861650968), author deferred the consolidation
- cross-package tree-kill consolidation — already reported (comment 3861650968), author deferred the consolidation
- exit/signal fallback observability logging — already reported as R2-3 (comment 3863811425), author declined
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1b": none.** All checks I planned completed within budget..
Convergence: round 3 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 7 (7 new). Findings keep coming back to the same files: packages/core/src/hooks/hookRunner.ts (findings in round 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1b":none.** All checks I planned completed within budget.。
收敛情况:第 3 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 7 条(其中 7 条首次提出)。发现反复回到同一批文件:packages/core/src/hooks/hookRunner.ts(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review feedback update for commit
No items were rejected or deferred in this batch. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- WINDOWS_TASKKILL constant and taskkill-call / POSIX tree-kill duplication — already reported (comment 3861650968), author deferred the consolidation
Not reviewed: build-and-test — the test harness aborted before its test phase on a pre-existing packages/sdk-typescript compile failure untouched by this PR; the hook suites (61 tests) ran green via direct agent execution, but the wider affected suite was not exercised by the harness.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1d": none — no check was cut short.; "agent 3c": running packages/core/src/hooks/hook-runner.process.test.ts — the review worktree has no node_modules installed and vitest cannot start, so the refactor was….
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/core/src/hooks/hookRunner.ts:906 — [probe] already-aborted signal fast path has no test witness
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — the test harness aborted before its test phase on a pre-existing packages/sdk-typescript compile failure untouched by this PR; the hook suites (61 tests) ran green via direct agent execution, but the wider affected suite was not exercised by the harness。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1d":none — no check was cut short.;"agent 3c":running packages/core/src/hooks/hook-runner.process.test.ts — the review worktree has no node_modules installed and vitest cannot start, so the refactor was…。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
@qwen-code /triage |
1 similar comment
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 130 passed · 0 failed · 130 total Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:130 通过 · 0 失败 · 130 总计 抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #10100 verification (round 3) — fix(core): Reclaim command hook process treesVerdict: This is a follow-up round. The PR gained two more feedback commits ( 中文 — 判定:✅ 通过 · 可合入(agent 判定)· 第 3 轮
Previous-finding status (round 2 → this head)
Central claim and A/B proofCentral claim: on timeout or cancellation, a command hook's entire process tree is reclaimed (SIGTERM to the owned POSIX process group, escalation to SIGKILL after a 2 s grace) before the result returns; base signalled only the direct child via Harnesses
A/B assertions: ab-main 74 (S1–S9 both arms incl. 12 S6 cross-arm field comparisons), ab-parent-fallback 14, dist-smoke 8. All base-arm expected-broken cells (leak, hang, ignored pre-abort) are encoded as expectations in the harness, so their reproduction counts as pass. Delta since round 2 (measured, not diffed)The two feedback commits are not individually reachable (depth-2 checkout; Vacuity, attribution, and the mutation matrixBase attribution (PR's test files staged into the base worktree, run against base source; witness Mutation matrix at head (scratch worktree
No mutant regressed a killed-to-survived test relative to the control. The M6/M7 red sets extend beyond the minimally expected tests for mechanically verified reasons (quoted failure messages in FindingsF1 — Suggestion (stands from rounds 1–2, coverage gap, not blocking): the F2 — Note (stays resolved): the EPERM F3 — Note (design semantics, still mitigated): No new findings this round. Not covered
MethodologyEnvironment: CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
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.
Partially reviewed — gaps disclosed.
4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- WINDOWS_TASKKILL constant and taskkill-call duplication — already reported (comment 3861650968), author deferred the consolidation
- cross-package tree-kill consolidation — already reported (comment 3861650968), author deferred the consolidation
- exit/signal fallback observability logging — already reported as R2-3 (comment 3863811425), author declined
- already-aborted signal fast path test witness — already reported in round 4 (deferred item in review 5033829402)
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/core/src/hooks/hookRunner.ts:150 — [review] No Windows parent-exit hook reclamation; the design doc's Non-goals does not record the gap
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Round 5 Critical addressed in fb0ed9f.
Verification: exact-head reproduction failed for both rows before the fix; after the fix, the 6-test process-tree suite, core build, core typecheck, targeted ESLint, and Prettier checks pass. Repository-wide build remains affected by the pre-existing unrelated CLI Ink typing mismatch, so this change does not modify that area. |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — the PR could not be built - workflow run The PR could not be built because 中文 — 判定:❌ 不通过 · PR 构建失败由于 Install/build log— Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed at the current head — spot-checked hookRunner.ts (the whole source delta) plus the design doc and test coverage:
- POSIX:
detachedmakes the hook child a process-group leader, cancellation signals the whole group (SIGTERM → 2s grace → SIGKILL), with ESRCH handled as "gone" and a direct-child fallback when group signaling fails. Windows path usestaskkill /f /twith its own timeout and direct-kill fallback. - Parent-exit leak closed: active hook children are registered in a module-level set and force-killed on parent exit / SIGHUP / SIGINT / SIGQUIT / SIGTERM (re-raising the signal when no other listener owns it); the registration is unregistered and the listeners removed again once the set drains. MessageDisplay / async hooks are correctly exempted as surviving the parent.
- The settle path is single-shot (
finish), cancellation waits for child close (1s bound) before destroying stdio so a cancelled hook cannot hang the caller, and the pre-aborted signal fast path is covered. - CI green (only the review-pr bot lane running). Aligns with the earlier review rounds' resolution.
wenshao
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the Windows process-tree path did not run locally.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
docs/design/hook-process-tree-cancellation.md:18 — [review] The design document says every POSIX command hook is registered for parent-exit cleanup, contradicting the intentional MessageDisplay and async-hook exemptions.packages/core/src/hooks/hookRunner.ts:127 — [review] The parent-exit direct-child fallback for a failed process-group SIGKILL has no regression test.
Convergence: round 6 posted 2 inline comment(s), 2 of them reported for the first time. Findings keep coming back to the same files: packages/core/src/hooks/hookRunner.ts (findings in round 5; 2 more now). (Evidence: the previous round was recovered from a marker this account did not post, so those rounds may not be this account's own.) A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the Windows process-tree path did not run locally。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 2 条行内评论,其中 2 条是首次提出。发现反复回到同一批文件:packages/core/src/hooks/hookRunner.ts(第 5 轮已出过发现,本轮又有 2 条)。(证据说明:上一轮的数据来自并非本账号发布的标记,上述轮次可能不属于本账号。)一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.2)
| const survivesParentExit = | ||
| eventName === HookEventName.MessageDisplay || | ||
| this.isAsyncHook(hookConfig); |
There was a problem hiding this comment.
[Critical] R5-1: (fix-induced) Fire-and-forget hooks exempted from parent-exit SIGKILL still die when they write to parent-owned stdout or stderr after the parent exits.
A MessageDisplay or async hook can outlive Qwen, but every hook is still spawned with parent-owned stdout/stderr pipes. Once Qwen exits, a later write receives SIGPIPE/EPIPE, terminating the hook before its audit, notification, or logging side effect completes.
Witness:
PR: both augmented survival tests failed with `Condition not met within 3000ms` (2 failed, 4 passed).
With parent-independent stdout/stderr: both passed (6 passed).
Give hooks intended to survive parent exit stdio that remains valid independently of the parent, such as spool files that HookRunner reads while alive, or explicitly revise the survival contract and clean them up. Extend both process-level survival fixtures to write stdout and stderr before creating their completion marker, and verify that removing the independent-stdio fix makes those tests fail.
中文说明
被排除在父进程退出 SIGKILL 清理之外的即发即忘 Hook,仍会在父进程退出后写入父进程持有的 stdout/stderr 管道时死亡。Qwen 退出会关闭管道读取端,Hook 随后的输出会触发 SIGPIPE/EPIPE,在审计、通知或日志副作用完成前终止。探针中,当前实现的两个增强生存测试均超时失败;将生存 Hook 的 stdout/stderr 改为独立于父进程后,6 个进程测试全部通过。请为需要存活的 Hook 提供独立 stdio,并让两个生存用例在创建完成标记前写 stdout/stderr;移除修复后测试必须失败。
— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.2)
| const survivesParentExit = | ||
| eventName === HookEventName.MessageDisplay || | ||
| this.isAsyncHook(hookConfig); |
There was a problem hiding this comment.
[Critical] R6-1: The parent-exit exemption omits synchronous StopFailure command hooks even though their documented contract and production callers launch them fire-and-forget.
docs/users/features/hooks.md explicitly defines StopFailure as fire-and-forget and documents rate-limit monitoring and authentication-failure logging as use cases. Core, CLI UI, and ACP callers start fireStopFailureEvent(...) without awaiting it. Because this predicate exempts only MessageDisplay and async: true, a headless error or loop-detection exit registers the synchronous StopFailure hook for parent-exit cleanup and SIGKILLs it before its alert or audit record completes.
Witness: not run — the review harness could not create the required isolated scratch tree for this verifier; the documented contract and complete production call chain are directly present in the reviewed commit.
Include HookEventName.StopFailure in the fire-and-forget parent-exit exemption and audit any other event whose production delivery is explicitly non-awaited, while retaining timeout and AbortSignal cancellation. Add a process-level synchronous StopFailure test whose parent exits immediately, and verify that removing the exemption makes the completion-marker assertion fail.
中文说明
父进程退出豁免遗漏了同步 StopFailure 命令 Hook,但其文档契约和生产调用点都明确采用即发即忘方式。用户文档将 StopFailure 定义为 fire-and-forget,并把限流告警、认证失败日志列为用途;Core、CLI UI 和 ACP 调用点都不会等待 fireStopFailureEvent(...)。当前判断只豁免 MessageDisplay 与 async: true,因此 headless 错误或循环检测退出时会把同步 StopFailure 注册到父退出清理并在告警/审计记录完成前 SIGKILL。请把 HookEventName.StopFailure 纳入豁免,并补一个父进程立即退出的同步 StopFailure 进程测试;移除豁免后完成标记断言必须失败。
— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.2)
|
Released in v0.22.3. |













What this PR does
This PR makes every command hook own a process group on POSIX and reclaims the group with a bounded SIGTERM-to-SIGKILL sequence when the hook times out or is cancelled. Windows cancellation invokes the absolute System32
taskkill.exepath asynchronously with/F /T, avoiding event-loop blocking while retaining a direct-child fallback. Cancellation and timeout races share one cleanup operation, final stdout and stderr are drained before the result is returned, and a one-second close boundary prevents stream shutdown from waiting indefinitely.Why it's needed
Command hooks can launch nested shells, package managers, and installers. The previous direct-child cleanup used
ChildProcess.killed, which records signal delivery rather than process exit, so escalation could be skipped and descendants could survive after the cancellation result. In session-start flows, those orphan processes amplify an initialization timeout into persistent resource leakage and make subsequent sessions less reliable.Reviewer Test Plan
How to verify
On POSIX, start a command hook whose descendant records SIGTERM and keeps running. Cancel or time out the hook and confirm the descendant receives SIGTERM, the process group is escalated after approximately two seconds, and neither the root nor descendant is running when the result returns. Also verify that final stdout and stderr written during cancellation are present in the result, while a child that never emits
closereturns after the one-second drain boundary. On Windows, confirm cancellation startstaskkill.exeasynchronously with/F /T /PID, does not block the abort call, and force-kills the direct child if taskkill reports an error.Evidence (Before & After)
N/A — non-UI process lifecycle change.
Tested on
Environment (optional)
macOS 26.4.1 (25E253), Node.js v22.22.3, npm 10.9.8. Focused core build, typecheck, lint, formatting, and 49 hook tests passed. The repository-wide build is currently blocked by unrelated existing Ink selection type errors under the CLI package.
Risk & Scope
Linked Issues
Closes #10099
中文说明
本 PR 做了什么
本 PR 在 POSIX 上让每个命令 Hook 拥有独立进程组,并在 Hook 超时或取消时通过有界的 SIGTERM 到 SIGKILL 流程回收整个进程组。Windows 取消路径异步调用 System32 下的绝对路径
taskkill.exe并传入/F /T,避免阻塞事件循环,同时保留直接子进程兜底。取消与超时竞态共享同一个清理操作,最终 stdout 和 stderr 会在结果返回前排空;如果流始终不关闭,则通过一秒的 close 边界避免无限等待。为什么需要
命令 Hook 可能启动多层 shell、包管理器和安装程序。原有的直接子进程清理使用
ChildProcess.killed,该属性只表示信号已经发送,并不表示进程已经退出,因此可能跳过强制升级并让后代进程在取消结果返回后继续运行。在 SessionStart 链路中,这类孤儿进程会把一次初始化超时放大成持续的资源泄漏,并降低后续会话的可靠性。Reviewer 测试计划
如何验证
在 POSIX 上启动一个命令 Hook,使其后代进程记录 SIGTERM 后继续运行。取消 Hook 或让其超时,确认后代收到 SIGTERM,进程组在约两秒后升级回收,并且结果返回时根进程和后代进程均不再运行。同时确认取消期间写出的最终 stdout 和 stderr 被保留,而永远不触发
close的子进程会在一秒排空边界后返回。在 Windows 上,确认取消会异步启动带/F /T /PID参数的taskkill.exe,不会阻塞 abort 调用,并在 taskkill 返回错误时强制终止直接子进程。证据(前后对比)
N/A — 非 UI 的进程生命周期变更。
测试平台
环境(可选)
macOS 26.4.1(25E253)、Node.js v22.22.3、npm 10.9.8。Core 聚焦 build、typecheck、lint、format 以及 49 个 Hook 测试均已通过。全仓库 build 当前被 CLI 包中与本 PR 无关的既有 Ink selection 类型错误阻断。
风险与范围
关联 Issue
Closes #10099