fix(core): require integer stop hook cap#5667
Conversation
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ On direction: this is a clean correctness fix — On approach: minimal and focused — 5 files, +39/-7, no scope creep. The two-layer defense (JSON schema rejects fractional values in editors, env parser rejects fractional strings at runtime, Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 方向:这是一个干净的正确性修复—— 方案:最小化且聚焦——5 个文件,+39/-7,无范围蔓延。两层防御(JSON schema 在编辑器层面拒绝小数值,env 解析器在运行时拒绝小数串, 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): I'd add a Comparison: the PR matches this approach exactly. No critical blockers found. The diff is minimal and every change serves the stated goal. Test ResultsBefore (main branch —
|
|
Going back to my independent proposal — the PR matches it almost exactly. The approach is sound: reject fractional env vars at the parsing layer, reject fractional config values at the schema layer, keep The diff is minimal and every line earns its place. No drive-by refactors, no scope creep. Two well-targeted new tests. The only thing I'd flag is the PR description's claim that "fractional direct settings values fall back to This is a straightforward correctness fix that ships what it promises. Tests confirm it works. LGTM. ✅ 中文说明回到我的独立方案——PR 的方案几乎完全一致。方案合理:在解析层拒绝小数 env 值,在 schema 层拒绝小数配置值,保留 diff 最小化,每一行都有存在的理由。没有顺手重构,没有范围蔓延。两个有针对性的新测试。唯一想指出的是 PR 描述中"直接配置的小数值会回退到 这是一个直接的正确性修复,交付了承诺的功能。测试确认有效。LGTM。 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
|
||
| export function normalizeStopHookBlockingCap(value: unknown): number { | ||
| if (typeof value !== 'number' || !Number.isFinite(value)) { | ||
| if (typeof value !== 'number' || !Number.isInteger(value)) { |
There was a problem hiding this comment.
[Suggestion] When an existing user has stopHookBlockingCap: 3.7 in their config (which previously floored to 3), the value now silently resets to the default of 8 with no logging or migration notice. The user's effective cap nearly triples without any signal — a safety-valve setting changing behavior silently is risky.
Consider logging a warning when a finite non-integer value is encountered:
| if (typeof value !== 'number' || !Number.isInteger(value)) { | |
| if (typeof value !== 'number' || !Number.isInteger(value)) { | |
| if (typeof value === 'number' && Number.isFinite(value)) { | |
| debugLogger.warn( | |
| `stopHookBlockingCap must be an integer; got ${value}, using default ${DEFAULT_STOP_HOOK_BLOCK_CAP}`, | |
| ); | |
| } | |
| return DEFAULT_STOP_HOOK_BLOCK_CAP; | |
| } |
— Claude 3.5 Sonnet via Qwen Code /review
There was a problem hiding this comment.
good catch, updated this. Existing config values like 3.7 now keep the old floor-to-3 behavior, while the env override and JSON schema still require integers for new values.
✅ Real-binary verification report (local, tmux)This PR makes the stop-hook blocking cap reject non-integers (falling back to the default) instead of silently flooring them, and declares the setting as Verdict: correct, well-tested (non-vacuous tests), and consistent across the runtime guard + JSON schema + VSCode schema — safe to merge. One behavioral nuance is noted at the end. 1. Real-binary e2e (tmux) — the cap flows end-to-endSetup: an always-block
The hook logged each invocation to a counter file (1 vs 8, matching the warning). So a fractional cap is no longer silently floored to a small number — it is rejected and falls back to the safe default 8. 2. Function-level A/B (
|
| input | BEFORE | FIXED |
|---|---|---|
int 5 / 200 / 0 |
5 / 100 / 8 | 5 / 100 / 8 (unchanged) |
float 3.7 |
3 | 8 |
float 5.9 |
5 | 8 |
float 100.9 |
100 | 8 |
env 1.5 (cfg 12) |
1 | 8 |
env 5 (cfg 12) |
5 | 5 (unchanged) |
Integers behave identically; every non-integer now returns the default 8 instead of being floored. NaN / Infinity / strings already returned the default on both sides.
3. Mutation test — the added tests genuinely guard the change
stopHookCap.test.ts: reverting the source to base → 2 new assertions fail (expected 3 to be 8,expected 1 to be 8).settingsSchema.test.ts: removing the newjsonSchemaOverrideblock → fails (expected undefined to deeply equal { type: 'integer', minimum: 1, default: 8 }).
4. Schema consistency
settingsSchema.ts (jsonSchemaOverride) and the VSCode settings.schema.json mirror both move stopHookBlockingCap from type: "number" (no lower bound) to type: "integer", minimum: 1, default: 8. Consistent.
Behavioral nuance (not a blocker)
The effective cap for fractional inputs can move in either direction: 5.9 was floored to 5 → now 8; 2.9 was 2 → now 8. A fractional iteration count is user error anyway, and falling back to the default 8 is the right behavior — just flagging that the resolved value can go up or down versus the old flooring, for anyone who set a fractional cap on purpose.
Environment
Real qwen built from PR head (0b41eda) via npm ci && npm run build; tmux 3.6a, isolated HOME with an always-block Stop hook; macOS, Node v22. BEFORE built by reverting only stopHookCap.ts to base and rebuilding core (cli resolves @qwen-code/qwen-code-core from core/dist at runtime).
中文版(完整对应)
✅ 真实二进制验证报告(本地 tmux)
本 PR 让 stop-hook 阻塞上限拒绝非整数(回退到默认值),而不是静默向下取整,并在 JSON schema 里把该设置声明为 integer / minimum: 1。我在隔离 worktree 里用 PR head 构建了真实的 qwen 二进制,在 tmux 里驱动它,并补充了函数级与变异测试。
结论:正确、测试非空过、且在"运行时守卫 + JSON schema + VSCode schema"三处保持一致 —— 可以合并。 末尾有一条不阻塞合并的行为提示。
1. 真实二进制 e2e(tmux)—— 上限端到端生效
设置:一个"总是 block 的 Stop hook"(echo '{"decision":"block",...}')触发防失控的安全阀;QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.9(分数覆盖值);两个构建用相同 prompt。
| 构建 | 1.9 解析为 |
Stop hook 触发次数 | TUI 里的上限警告 |
|---|---|---|---|
BEFORE(Number.isFinite + Math.floor) |
floor(1.9) = 1 |
1 次 | Stop hook blocked continuation 1 consecutive time; … |
FIXED(Number.isInteger) |
非整数 → 默认 8 | 8 次 | Stop hook blocked continuation 8 consecutive times; … |
hook 每次调用都写入计数文件(1 vs 8,与警告一致)。所以分数上限不再被静默取整成一个小数字 —— 它被拒绝并回退到安全默认值 8。
2. 函数级 A/B(normalizeStopHookBlockingCap / resolveStopHookBlockingCap)
| 输入 | BEFORE | FIXED |
|---|---|---|
整数 5 / 200 / 0 |
5 / 100 / 8 | 5 / 100 / 8(不变) |
分数 3.7 |
3 | 8 |
分数 5.9 |
5 | 8 |
分数 100.9 |
100 | 8 |
env 1.5(cfg 12) |
1 | 8 |
env 5(cfg 12) |
5 | 5(不变) |
整数行为完全一致;所有非整数现在返回默认值 8 而不是取整。NaN / Infinity / 字符串两边本来就返回默认值。
3. 变异测试 —— 新增测试确实守护了改动
stopHookCap.test.ts:把源码回退到 base → 2 个新断言失败(expected 3 to be 8、expected 1 to be 8)。settingsSchema.test.ts:删掉新增的jsonSchemaOverride块 → 失败(expected undefined to deeply equal { type: 'integer', minimum: 1, default: 8 })。
4. Schema 一致性
settingsSchema.ts(jsonSchemaOverride)和 VSCode 的 settings.schema.json 镜像都把 stopHookBlockingCap 从 type: "number"(无下限)改为 type: "integer", minimum: 1, default: 8。一致。
行为提示(不阻塞合并)
对分数输入,生效上限可能向两个方向变化:5.9 原本取整为 5 → 现在 8;2.9 原本是 2 → 现在 8。分数迭代次数本身就是用户误用,回退到默认 8 是正确行为 —— 只是提醒一下:对于有意设置分数上限的人,解析值相比旧的取整可能变大或变小。
环境
真实 qwen 由 PR head(0b41eda)经 npm ci && npm run build 构建;tmux 3.6a、隔离 HOME 配置了"总是 block 的 Stop hook";macOS、Node v22。BEFORE 通过只把 stopHookCap.ts 回退到 base 并重建 core 得到(cli 运行时从 core/dist 解析 @qwen-code/qwen-code-core)。
✅ Local real-build verification (tmux A/B)Maintainer verification before merge. I built the actual Verdict: LGTM. The fix changes user-visible runtime behavior exactly as intended (fractional caps now fall back to the documented default instead of being silently floored), with zero regression on every integer / out-of-range / 1. Real-TUI A/B — the behavior issue #5664 describesHarness: a user-scope Before (pre-fix):
After (this PR):
(The 2. Built-function A/B (
|
| input | BASE (pre-fix) | FIXED (this PR) | |
|---|---|---|---|
norm(1.5) |
1 |
8 (DEFAULT) |
← changed |
norm(3.7) |
3 |
8 (DEFAULT) |
← changed |
norm(99.9) |
99 |
8 (DEFAULT) |
← changed |
norm(100.9) |
100 |
8 (DEFAULT) |
← changed |
env=1.5 |
1 |
8 (DEFAULT) |
← changed |
env=2.9 |
2 |
8 (DEFAULT) |
← changed |
norm(1) / norm(8) / norm(100) |
1 / 8 / 100 |
1 / 8 / 100 |
identical |
norm(99999) (clamp) |
100 |
100 |
identical |
norm(0.5) / norm(-1) / norm(0) |
8 |
8 |
identical |
norm(NaN) / norm(Infinity) |
8 |
8 |
identical |
env=5 / env=150 / env=abc |
5 / 100 / 8 |
5 / 100 / 8 |
identical |
Only fractional inputs change; every existing valid/invalid/clamp path is byte-for-byte identical → the change is correctly scoped.
3. Mutation tests (proving the new tests aren't vacuous)
Reverted each source file to base while keeping the PR's tests, ran the suite, expected failures:
packages/core—stopHookCap.test.ts: 2 tests fail against base —
normalizes invalid values to the default cap→expected 3 to be 8, and
rejects fractional environment overrides→expected 1 to be 8. Both pass on the PR. ✅packages/cli—settingsSchema.test.ts:should define stopHookBlockingCap schema override as a positive integerfails against base →expected undefined to deeply equal { type: 'integer', minimum: 1, default: 8 }. Passes on the PR. ✅
4. Schema generation is reproducible
npm run generate:settings-schema on the PR leaves git diff empty (committed settings.schema.json matches a fresh generation). Reverting settingsSchema.ts to base regenerates "type": "number" (no minimum), confirming the committed integer / minimum: 1 / default: 8 block is genuinely produced by the source change.
中文版(点击展开)
✅ 本地真实构建验证(tmux A/B 对比)
合并前的维护者验证。我在 PR HEAD(0b41eda)上构建了真实的 qwen 二进制,并与修复前的构建(PR 父提交 01d28a1,仅回退 stopHookCap.ts 并重新构建 core)做了 A/B 对比。环境:macOS arm64,Node v22.22.2。
结论:可以合并。 修复精确地改变了用户可见的运行时行为(小数 cap 现在回退到文档默认值,而不再被静默向下取整),且对所有整数 / 越界 / NaN / clamp 路径零回归。
1. 真实 TUI A/B —— 复现 issue #5664 描述的行为
测试装置:一个用户级 Stop hook,始终返回 {"decision":"block"}(并且每次触发往计数文件追加一行),在 tmux 中以 QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5 运行。生效的 cap 有两种可观测方式:override 提示里打印的数字,以及循环被覆盖前 hook 触发的次数。
修复前: 1.5 → Math.floor → cap 1 → loop protection 在单次阻塞后就触发:
✦ 你好!有什么我可以帮你的吗?
⎿ Stop says:
Stop hook blocked continuation 1 consecutive time; overriding and ending the turn.
计数文件 = 1 次触发
修复后(本 PR): 1.5 不是整数 → 回退到 DEFAULT_STOP_HOOK_BLOCK_CAP = 8 → hook 阻塞 8 次后才覆盖:
✦ ok
● Ran 1 stop hooks
⎿ Stop hook error: Reply with only the word: ok
… (共 8 个阻塞循环)…
✦ ok
⎿ Stop says:
Stop hook blocked continuation 8 consecutive times; overriding and ending the turn.
计数文件 = 8 次触发
(Stop hook error: … 这行只是我的测试 hook 把自己的 block reason 回显出来,并不是真的报错。)这正是 #5664 的意外之处:配置 1.5 让 loop protection 在 1 次阻塞后就触发,而不是预期的 8 次。注意提示文本本身就带着生效的 cap(1 time vs 8 times),所以这个错误的值是直接对用户可见的。
2. 内置函数 A/B(normalizeStopHookBlockingCap / resolveStopHookBlockingCap,从构建出的 core/dist 导入)
| 输入 | BASE(修复前) | FIXED(本 PR) | |
|---|---|---|---|
norm(1.5) |
1 |
8(默认) |
← 改变 |
norm(3.7) |
3 |
8(默认) |
← 改变 |
norm(99.9) |
99 |
8(默认) |
← 改变 |
norm(100.9) |
100 |
8(默认) |
← 改变 |
env=1.5 |
1 |
8(默认) |
← 改变 |
env=2.9 |
2 |
8(默认) |
← 改变 |
norm(1) / norm(8) / norm(100) |
1 / 8 / 100 |
1 / 8 / 100 |
一致 |
norm(99999)(clamp) |
100 |
100 |
一致 |
norm(0.5) / norm(-1) / norm(0) |
8 |
8 |
一致 |
norm(NaN) / norm(Infinity) |
8 |
8 |
一致 |
env=5 / env=150 / env=abc |
5 / 100 / 8 |
5 / 100 / 8 |
一致 |
只有小数输入会变化,所有既有的有效/无效/clamp 路径都逐位一致 → 改动范围精确。
3. 变异测试(证明新增测试不是空过)
把每个源文件回退到 base、保留 PR 的测试、跑测试套件,预期失败:
packages/core—stopHookCap.test.ts: 对 base 有 2 个测试失败 ——
normalizes invalid values to the default cap→expected 3 to be 8,以及
rejects fractional environment overrides→expected 1 to be 8。两者在 PR 上都通过。✅packages/cli—settingsSchema.test.ts:should define stopHookBlockingCap schema override as a positive integer对 base 失败 →expected undefined to deeply equal { type: 'integer', minimum: 1, default: 8 }。在 PR 上通过。✅
4. Schema 生成可复现
在 PR 上跑 npm run generate:settings-schema 后 git diff 为空(提交的 settings.schema.json 与重新生成的结果一致)。把 settingsSchema.ts 回退到 base 后重新生成会得到 "type": "number"(没有 minimum),确认提交里的 integer / minimum: 1 / default: 8 区块确实是由源码改动生成的。
✅ Local verification report (maintainer)Built and tested this PR locally to confirm the fix before merge. Environment
Results
Mutation test (the key evidence).
Correctness review
Minor / non-blocking observation. Verdict: LGTM. Correct, minimal, mutation-verified, schema in sync; merges cleanly, type/lint/format clean. Safe to merge from my side. 🇨🇳 中文版✅ 本地验证报告(维护者)合并前在本地构建并测试了本 PR 以确认修复。 环境
结果
变异测试(关键证据)。
正确性审查
次要 / 非阻塞观察。 结论:LGTM。 正确、最小化、经变异测试验证、schema 同步;可干净合并,类型/lint/格式干净。从我这边看可以安全合并。 |
0b41eda to
a123c82
Compare
| } | ||
|
|
||
| function parseStopHookBlockingCapEnv(value: string): number { | ||
| const parsed = Number(value); |
There was a problem hiding this comment.
[Suggestion] Number() accepts hex ("0x10"), scientific notation ("1e2"), and binary ("0b10") as valid integers — inconsistent with every other integer env-var parser in the codebase.
Consider using the shared parsePositiveIntegerEnv helper from packages/core/src/utils/env.ts (used by 7+ other parsers: workflow-orchestrator.ts, workflow-budget.ts, coreToolScheduler.ts, background-tasks.ts, serve.ts, Session.ts, modelConfigResolver.ts). It enforces plain decimal integers via ^\d+$ regex + Number.isSafeInteger and removes the need for the private parseStopHookBlockingCapEnv function entirely:
import { parsePositiveIntegerEnv } from '../utils/env.js';
export function resolveStopHookBlockingCap(configValue?: number): number {
const envValue = process.env[STOP_HOOK_BLOCK_CAP_ENV];
if (envValue !== undefined && envValue.trim() !== '') {
const parsed = parsePositiveIntegerEnv(envValue, 0);
return normalizeStopHookBlockingCap(parsed);
}
return normalizeStopHookBlockingCap(configValue);
}The fallback 0 works correctly because normalizeStopHookBlockingCap(0) maps to DEFAULT_STOP_HOOK_BLOCK_CAP (since 0 < 1).
— qwen3.7-max via Qwen Code /review
✅ Local verification —
|
| Input | Resolved cap | Note |
|---|---|---|
ENV=1.5 |
8 | the fix — rejected → default |
ENV=2 |
2 | used as-is |
ENV=99999 |
100 | capped to MAX |
ENV=0 / -3 / abc |
8 | invalid → default |
ENV=3.0 |
3 | Number("3.0") is an integer → used |
ENV="" (empty) + config 5 |
5 | empty env ignored → config |
config 3.7 (no env) |
3 | legacy floor preserved (back-compat) |
config 0 / undefined |
8 | default |
A/B proof (on the real build, under tmux)
ENV=1.5 → cap |
|
|---|---|
| this PR (new) | 8 |
| fix reverted (old) | 1 |
Every other case is byte-identical between old and new — the change is surgical. Reverting the env guard also flips stopHookCap.test.ts > rejects fractional environment overrides from green to AssertionError: expected 1 to be 8, confirming the test actually guards the behavior.
Notes (merge reference, not blockers)
- The env path is now strict-integer, while the config path still floors fractionals (
3.7 → 3) for backward compatibility — intentional and covered bypreserves legacy fractional config values. So the JSON-schemaintegerconstraint is advisory (editor-side) for config values, whereas the env override is enforced at runtime. Reasonable; just worth being aware of. - Consumption sites (
core/client.tsStop loop at ~2362,agent.tsSubagentStop loop at ~1329) read the resolved cap viaconfig.getStopHookBlockingCap()and are unchanged by this PR.
Verdict: ✅ Safe to merge. Behavior is correct on the compiled build, fully tested, back-compatible, and the A/B confirms the fix removes the silent-floor footgun (a typo like 1.5 no longer collapses the safety valve to a single iteration).
🇨🇳 中文版(合并参考)
✅ 本地验证 — fix(core): require integer stop hook cap
在隔离 worktree(commit a123c8201)上用真实 npm ci + 构建完成验证,并在 tmux 中驱动编译产物 packages/core/dist/src/hooks/stopHookCap.js(而非仅 TS 源码)。
这个修复做了什么: QWEN_CODE_STOP_HOOK_BLOCK_CAP 现在要求整数 —— 小数(如 1.5)会回退到默认值 8,而不是被静默向下取整为 1。配置文件中的值保留旧的取整行为(向后兼容),settings JSON schema 现在约束为 integer, minimum 1。
测试
packages/corestopHookCap.test.ts— 9/9 ✅packages/clisettingsSchema.test.ts— 27/27 ✅tsc --noEmit(core)0 错误;eslint(改动文件)0 问题 ✅
tmux 中真实编译产物(15/15 全部通过)
| 输入 | 解析出的 cap | 说明 |
|---|---|---|
ENV=1.5 |
8 | 本次修复 —— 拒绝小数 → 回退默认值 |
ENV=2 |
2 | 原值使用 |
ENV=99999 |
100 | 限制到 MAX |
ENV=0 / -3 / abc |
8 | 非法 → 默认值 |
ENV=3.0 |
3 | Number("3.0") 是整数 → 使用 |
ENV=""(空)+ 配置 5 |
5 | 空 env 忽略 → 用配置 |
配置 3.7(无 env) |
3 | 保留旧的取整(向后兼容) |
配置 0 / undefined |
8 | 默认值 |
A/B 证明(真实构建产物,tmux 中)
ENV=1.5 → cap |
|
|---|---|
| 本 PR(新) | 8 |
| 还原修复(旧) | 1 |
除该用例外,新旧产物结果完全一致 —— 改动是外科手术式的。还原修复后,stopHookCap.test.ts > rejects fractional environment overrides 由通过变为 AssertionError: expected 1 to be 8,证明测试确实守护了该行为。
说明(合并参考,非阻塞项)
- env 路径现在严格要求整数;而配置路径仍对小数取整(
3.7 → 3)以保持向后兼容 —— 这是有意为之且有测试覆盖。因此 JSON schema 的integer约束对配置值是编辑器层面的提示,env override 则在运行时强制执行。设计合理,知悉即可。 - 消费点(
core/client.ts的 Stop 循环约 2362 行、agent.ts的 SubagentStop 循环约 1329 行)通过config.getStopHookBlockingCap()读取解析后的 cap,本 PR 未改动。
结论:✅ 可以合并。 编译产物行为正确、测试完整、向后兼容,A/B 证明修复消除了静默取整的隐患(像 1.5 这样的笔误不会再把安全阀压缩到只剩 1 次迭代)。
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Rejects fractional values for the Stop/SubagentStop hook blocking cap.
stopHookBlockingCapvalues as invalid and falls back toDEFAULT_STOP_HOOK_BLOCK_CAP.QWEN_CODE_STOP_HOOK_BLOCK_CAP.minimum: 1.Why it's needed
stopHookBlockingCapis a count-like safety valve, but fractional values were previously accepted and floored. For example,QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5resolved to1, which can make Stop/SubagentStop hook loop protection fire earlier than the configured value suggests.Fractional values should be treated like other invalid values instead of silently changing the cap.
Reviewer Test Plan
How to verify
normalizeStopHookBlockingCap(3.7)QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5packages/vscode-ide-companion/schemas/settings.schema.jsonnow marksstopHookBlockingCapas an integer.Evidence (Before & After)
Before:
normalizeStopHookBlockingCap(3.7)returned3.QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5resolved to1."type": "number"forstopHookBlockingCap.After:
DEFAULT_STOP_HOOK_BLOCK_CAP.DEFAULT_STOP_HOOK_BLOCK_CAP."type": "integer","minimum": 1, and"default": 8.Commands run:
npm run generate:settings-schemanpm test --workspace=packages/core -- hooks/stopHookCap.test.tsnpm test --workspace=packages/cli -- src/config/settingsSchema.test.tsnpm run lint --workspace=packages/core --if-presentnpm run lint --workspace=packages/cli --if-presentnpm run typecheck --workspace=packages/core --if-presentnpx prettier --check packages/core/src/hooks/stopHookCap.ts packages/core/src/hooks/stopHookCap.test.ts packages/cli/src/config/settingsSchema.ts packages/cli/src/config/settingsSchema.test.ts packages/vscode-ide-companion/schemas/settings.schema.jsongit diff --checkTested on
Risk & Scope
Risk is low. The change only affects validation/normalization for fractional cap values.
Out of scope:
Breaking changes:
stopHookBlockingCapvalues are no longer accepted. They now fall back to the default, matching the invalid-value path.Linked Issues
Fixes #5664
AI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.
中文说明
这个 PR 做了什么
这个 PR 拒绝 Stop/SubagentStop hook blocking cap 的小数值。
stopHookBlockingCap会被视为无效值,并回退到DEFAULT_STOP_HOOK_BLOCK_CAP。QWEN_CODE_STOP_HOOK_BLOCK_CAP走同样的规则。minimum: 1的 integer。为什么需要
stopHookBlockingCap是计数类 safety valve,但之前小数会被接受并向下取整。比如QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5会解析成1,这会让 Stop/SubagentStop hook loop protection 比配置看起来更早触发。小数值应该像其他无效值一样回退,而不是被静默改写成另一个 cap。
Reviewer 测试计划
如何验证
normalizeStopHookBlockingCap(3.7)QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5packages/vscode-ide-companion/schemas/settings.schema.json现在把stopHookBlockingCap标记为 integer。前后对比证据
修改前:
normalizeStopHookBlockingCap(3.7)返回3。QWEN_CODE_STOP_HOOK_BLOCK_CAP=1.5解析为1。stopHookBlockingCap使用"type": "number"。修改后:
DEFAULT_STOP_HOOK_BLOCK_CAP。DEFAULT_STOP_HOOK_BLOCK_CAP。"type": "integer"、"minimum": 1和"default": 8。已运行命令:
npm run generate:settings-schemanpm test --workspace=packages/core -- hooks/stopHookCap.test.tsnpm test --workspace=packages/cli -- src/config/settingsSchema.test.tsnpm run lint --workspace=packages/core --if-presentnpm run lint --workspace=packages/cli --if-presentnpm run typecheck --workspace=packages/core --if-presentnpx prettier --check packages/core/src/hooks/stopHookCap.ts packages/core/src/hooks/stopHookCap.test.ts packages/cli/src/config/settingsSchema.ts packages/cli/src/config/settingsSchema.test.ts packages/vscode-ide-companion/schemas/settings.schema.jsongit diff --check测试环境
风险和范围
风险较低。这个改动只影响小数 cap 值的校验和归一化。
不在范围内:
破坏性变化:
stopHookBlockingCap不再被接受,现在会按无效值路径回退到默认值。关联 Issue
Fixes #5664
AI 辅助披露
我使用 Codex 审查改动、对照现有模式做 sanity check,并帮助发现潜在边界情况。