Skip to content

fix(core): parse agent & workflow integer env vars strictly - #5679

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/agent-workflow-env-int-parsing
Jun 23, 2026
Merged

fix(core): parse agent & workflow integer env vars strictly#5679
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/agent-workflow-env-int-parsing

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

What this PR does

Routes the four agent/workflow integer env overrides — QWEN_CODE_MAX_BACKGROUND_AGENTS, QWEN_CODE_MAX_TOKENS_PER_WORKFLOW, QWEN_CODE_MAX_WORKFLOW_AGENTS, and QWEN_CODE_MAX_WORKFLOW_CONCURRENCY — through the existing parsePositiveIntegerEnv helper instead of a bare Number(raw) + Number.isInteger check. Each function keeps its existing fallback, debug warning, and clamping behavior; only the validity check is tightened.

Why it's needed

Number(raw) accepts hex, scientific, and trailing-zero-float literals, and Number.isInteger only checks the result. So QWEN_CODE_MAX_WORKFLOW_AGENTS=0x10 was silently honored as 16, =1e3 as 1000, and =1.0 as 1 — none of which a user typing a count would expect, and a fat-fingered 1e9 could push the cap all the way to the hard ceiling. The rest of the codebase already parses positive-integer env vars with parsePositiveIntegerEnv (/^\d+$/ + Number.isSafeInteger) — e.g. coreToolScheduler, modelConfigResolver, serve.ts. These four agent/workflow caps were the remaining sites still on the loose pattern; this aligns them with the rest, and as a bonus rejects unsafe-integer strings that Number.isInteger let through.

Reviewer Test Plan

How to verify

Each resolver should accept only plain decimal integers and fall back otherwise (to its default, to null for the token cap, or to the cpu-derived value for the concurrency limit). New unit tests cover the previously-accepted 0x10 / 1e2 / 1e3 / 1e6 / 1.0 / 5.0 inputs; the existing decimal, zero/negative, non-numeric, and over-ceiling-clamp cases are unchanged and still pass.

npx vitest run \
  packages/core/src/agents/background-tasks.test.ts \
  packages/core/src/agents/runtime/workflow-budget.test.ts \
  packages/core/src/agents/runtime/workflow-orchestrator.test.ts
# Test Files  3 passed (3)
#      Tests  223 passed (223)

Evidence (Before & After)

N/A — non-user-visible env-parsing fix.

Tested on

OS Status
🍏 macOS N/A
🪟 Windows
🐧 Linux N/A

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR @he-yufeng!

Template looks good ✓

On direction: this is a clear, low-risk correctness fix. Number(raw) letting 0x10 / 1e3 / 1.0 slip through as valid integer overrides is genuinely surprising behavior, and the rest of the codebase (coreToolScheduler, modelConfigResolver, serve.ts, Session.ts) already uses parsePositiveIntegerEnv for this exact purpose. Aligning the last four agent/workflow sites closes a real consistency gap. Not in CHANGELOG but the pattern is well-established.

On approach: scope is tight — four call sites, one helper swap, new tests for the previously-accepted edge cases. Prettier reformatting on the touched test file is noise but unavoidable. No scope creep, no drive-by refactors.

Moving on to code review + test run. 🔍

中文说明

感谢贡献 @he-yufeng

模板完整 ✓

方向:这是低风险的正确性修复。Number(raw) 会默默接受 0x10 / 1e3 / 1.0 作为整数覆盖值,这是用户意料之外的行为;代码库其他地方(coreToolSchedulermodelConfigResolverserve.tsSession.ts)已经统一使用 parsePositiveIntegerEnv。把这四个 agent/workflow 的调用点对齐到同一套解析逻辑,弥补了一致性缺口。CHANGELOG 没有直接参考,但模式已经在多处验证过。

方案:范围紧凑 —— 四个调用点、一次 helper 替换、针对之前被接受的边界情况补充测试。测试文件的 prettier 重排是附带噪音但不可避免。没有范围蔓延,也没有顺手重构。

进入代码审查和测试阶段 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal before reading the diff: the four outlier resolvers (resolveMaxConcurrentBackgroundAgents, resolveMaxTokensPerWorkflow, resolveMaxAgentsPerRun, resolveConcurrencyLimit) should swap Number(raw) + Number.isInteger for the existing parsePositiveIntegerEnv helper from packages/core/src/utils/env.ts, which the rest of the codebase already uses for this exact purpose (coreToolScheduler, modelConfigResolver, serve.ts, Session.ts, tokenLimits.ts). Each call site must preserve its original fallback — DEFAULT_* / null / cpu-derived default — and the existing clamp-to-hard-ceiling logic on the concurrency resolver must remain intact. New unit tests should cover 0x.. / 1eN / N.0 inputs.

The PR's solution matches this proposal exactly. No correctness bugs, no regressions, no missed edge cases. The helper's contract (/^\d+$/ regex + Number.isSafeInteger + > 0) cleanly rejects every previously-accepted non-decimal input while preserving decimal parsing, and each call site's fallback/clamp behavior is preserved. A couple of prettier reformatting lines on the touched test file are collateral noise but consistent with the project's formatter. No scope creep, no drive-by refactors.

Testing

PR's own test suitepackages/core/src/agents/{background-tasks,runtime/workflow-budget,runtime/workflow-orchestrator}.test.ts:

 ✓ src/agents/runtime/workflow-budget.test.ts (20 tests) 17ms
 ✓ src/agents/background-tasks.test.ts (88 tests) 86ms
 ✓ src/agents/runtime/workflow-orchestrator.test.ts (115 tests) 460ms

 Test Files  3 passed (3)
      Tests  223 passed (223)

Before/after behavioral reproduction (ran resolvers directly against 0x10 / 1e3 / 1.0 / 0x2BF20 / 1e6 / 5.0 inputs):

Before (main — Number(raw) + Number.isInteger)

raw=0x10       Number(raw)=16 Number.isInteger=true
raw=1e3        Number(raw)=1000 Number.isInteger=true
raw=1.0        Number(raw)=1 Number.isInteger=true
raw=5.0        Number(raw)=5 Number.isInteger=true
raw=0x2BF20    Number(raw)=180000 Number.isInteger=true
raw=1e6        Number(raw)=1000000 Number.isInteger=true

End-to-end resolver test on main: resolveMaxConcurrentBackgroundAgents('0x10') returned 16, resolveMaxTokensPerWorkflow('0x2BF20') returned 180000, resolveMaxAgentsPerRun('1e3') returned 1000, resolveConcurrencyLimit('0x10') returned 16. Bug reproduced on all four sites.

After (this PR — parsePositiveIntegerEnv)

raw=0x10       parsePositiveIntegerEnv=FALLBACK
raw=1e3        parsePositiveIntegerEnv=FALLBACK
raw=1.0        parsePositiveIntegerEnv=FALLBACK
raw=5.0        parsePositiveIntegerEnv=FALLBACK
raw=0x2BF20    parsePositiveIntegerEnv=FALLBACK
raw=1e6        parsePositiveIntegerEnv=FALLBACK

Resolver-level test on the PR branch: all four resolvers now fall back to their documented defaults (DEFAULT_MAX_CONCURRENT_BACKGROUND_AGENTS, null, DEFAULT_MAX_AGENTS_PER_RUN, cpu-derived concurrency default) on the same inputs — 11/11 targeted assertions pass.

Evidence (Before & After): this is a non-user-visible env-parsing fix, so the behavioral delta above is the evidence. No TUI change.

中文说明

代码审查

读 diff 之前的独立方案:四个异常调用点(resolveMaxConcurrentBackgroundAgentsresolveMaxTokensPerWorkflowresolveMaxAgentsPerRunresolveConcurrencyLimit)应该把 Number(raw) + Number.isInteger 替换为 packages/core/src/utils/env.ts 中已有的 parsePositiveIntegerEnv,这个 helper 代码库其他地方已经在用(coreToolSchedulermodelConfigResolverserve.tsSession.tstokenLimits.ts)。每个调用点必须保留原来的 fallback(DEFAULT_* / null / cpu 派生值),并且 concurrency resolver 上的 hard-ceiling clamp 必须保留。新单元测试需要覆盖 0x.. / 1eN / N.0 输入。

PR 的方案与这个独立提案完全一致。没有正确性 bug,没有回退,没有遗漏的边界情况。Helper 的契约(/^\d+$/ 正则 + Number.isSafeInteger + > 0)干净地拒绝所有之前被接受的非十进制输入,同时保留十进制解析;每个调用点的 fallback/clamp 行为都保留。测试文件中有一些 prettier 重排,是被触碰文件的附带噪音,但与项目 formatter 一致。没有范围蔓延,没有顺手重构。

测试

PR 自带测试 —— packages/core/src/agents/{background-tasks,runtime/workflow-budget,runtime/workflow-orchestrator}.test.ts

 ✓ src/agents/runtime/workflow-budget.test.ts (20 tests) 17ms
 ✓ src/agents/background-tasks.test.ts (88 tests) 86ms
 ✓ src/agents/runtime/workflow-orchestrator.test.ts (115 tests) 460ms

 Test Files  3 passed (3)
      Tests  223 passed (223)

修复前后的行为复现(用 0x10 / 1e3 / 1.0 / 0x2BF20 / 1e6 / 5.0 直接调用 resolver):

修复前(main —— Number(raw) + Number.isInteger

raw=0x10       Number(raw)=16 Number.isInteger=true
raw=1e3        Number(raw)=1000 Number.isInteger=true
raw=1.0        Number(raw)=1 Number.isInteger=true
raw=5.0        Number(raw)=5 Number.isInteger=true
raw=0x2BF20    Number(raw)=180000 Number.isInteger=true
raw=1e6        Number(raw)=1000000 Number.isInteger=true

main 上做端到端 resolver 测试:resolveMaxConcurrentBackgroundAgents('0x10') 返回 16resolveMaxTokensPerWorkflow('0x2BF20') 返回 180000resolveMaxAgentsPerRun('1e3') 返回 1000resolveConcurrencyLimit('0x10') 返回 16。四个调用点都复现了 bug。

修复后(本 PR —— parsePositiveIntegerEnv

raw=0x10       parsePositiveIntegerEnv=FALLBACK
raw=1e3        parsePositiveIntegerEnv=FALLBACK
raw=1.0        parsePositiveIntegerEnv=FALLBACK
raw=5.0        parsePositiveIntegerEnv=FALLBACK
raw=0x2BF20    parsePositiveIntegerEnv=FALLBACK
raw=1e6        parsePositiveIntegerEnv=FALLBACK

在 PR 分支上做 resolver 级别测试:四个 resolver 现在在相同输入下都会回退到文档中定义的默认值(DEFAULT_MAX_CONCURRENT_BACKGROUND_AGENTSnullDEFAULT_MAX_AGENTS_PER_RUN、cpu 派生的并发默认值)—— 11/11 个目标断言通过。

证据(修复前/修复后): 这是不可见的环境变量解析修复,上面的行为差异数据就是证据。没有 TUI 变化。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection: this is the kind of fix that quietly makes the codebase better. The PR's approach matches my independent proposal exactly — reuse the existing parsePositiveIntegerEnv helper rather than reinventing the regex at each call site. The before/after is unambiguous: the old path accepted 0x10 as 16 and 1e3 as 1000 for integer overrides, which no user setting a count-style env var would expect; the new path cleanly rejects them and falls back to the documented default.

The fix is genuinely minimal — four Number(raw)parsePositiveIntegerEnv(raw, 0) swaps with each site's existing fallback/clamp branch preserved, plus test coverage for the previously-accepted edge cases. No scope creep, no speculative refactoring. Every change in the diff is required for the stated goal (modulo the unavoidable prettier reformatting on the touched test file).

223 PR tests pass, 11/11 targeted behavioral assertions confirm the fix across all four resolvers. Non-user-visible, so the behavioral delta is the evidence — and it's clean.

If I had to maintain this in six months, I'd thank the author: one helper, four identical-looking call sites, one mental model to keep in sync. Approving. ✅

中文说明

反思:这是那种 quietly 让代码变好的修复。PR 的方案与我的独立提案完全一致 —— 复用已有的 parsePositiveIntegerEnv helper,而不是在每个调用点重写正则。修复前/修复后的对比很明确:老路径会把 0x10 当作 16、把 1e3 当作 1000 作为整数覆盖值,这是设置"计数类"环境变量的用户完全不会预料到的行为;新路径干净地拒绝并回退到文档中定义的默认值。

修复是真的最小化 —— 四次 Number(raw)parsePositiveIntegerEnv(raw, 0) 替换,每个调用点原来的 fallback/clamp 分支都保留了;加上新测试覆盖之前被接受的边界情况。没有范围蔓延,没有投机性重构。diff 里的每一处改动都是为了达成目标所必需的(测试文件中不可避免的 prettier 重排除外)。

PR 223 个测试通过,11/11 个目标行为断言在四个 resolver 上确认了修复。这是不可见的改动,所以行为差异本身就是证据 —— 而且很干净。

如果六个月后我要维护这段代码,我会感谢作者:一个 helper、四个看起来完全一致的调用点、一个要维护的心智模型。批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — real qwen binary A/B

Verdict: PASS — safe to merge. I built both branches locally and drove the real CLI (not unit tests / not import-and-call). The fix changes observable runtime behaviour on all four resolvers exactly as the PR claims, and a valid plain-decimal value still works (no regression).

Method

One CLI binary; only core dist/ was swapped between this PR (19de577) and its merge-base (01d28a1) by rebuilding core — each side grep-confirmed in the built dist before the run. Real TUI, qwen3.7-max, --approval-mode yolo. Each env var was set to a value that Number() accepts but a strict decimal parse must reject: 0x22, 0x2BF20180000, 0x1016.

  • Workflow caps exercised via a saved /captest workflow = three sequential agent() calls.
  • Background cap exercised via three concurrent /forks (kept alive with sleep).

A/B results

Resolver env var (value) BASE — Number() + isInteger This PR — parsePositiveIntegerEnv
resolveMaxAgentsPerRun MAX_WORKFLOW_AGENTS=0x2 honored = 2 → 3rd agent() rejected → workflow failed: exceeded the maximum of 2 agent() calls per run rejected → default 10003/3 agents, completed
resolveMaxTokensPerWorkflow MAX_TOKENS_PER_WORKFLOW=0x2BF20 honored = 180000/workflows detail cap: 180k rejected → nullcap: (no cap)
resolveMaxConcurrentBackgroundAgents MAX_BACKGROUND_AGENTS=0x2 honored = 2 → 3rd /fork rejected: maximum concurrent background agents (2) reached rejected → default 10all 3 forks accepted
resolveConcurrencyLimit MAX_WORKFLOW_CONCURRENCY=0x10 honored = 16 (silent) rejected → cpu-derived default + debug warning

The single /workflows listing shows both workflow runs side by side:

wf_805c…  failed     captest · 2/2 agents · 52/180kt — Workflow exceeded the maximum of 2 agent() calls per run   ← BASE (0x2 honored)
wf_f150…  completed  captest · 3/3 agents · 91t                                                                   ← this PR (0x2 rejected)

Debug-log A/B (QWEN_DEBUG_LOG_FILE=1): this PR's session logs 3 × Invalid QWEN_CODE_MAX_… warnings; the BASE session logs 0 — i.e. it silently accepted the hex/scientific values, which is the bug.

No-regression probe: FIXED binary with a plain decimal MAX_WORKFLOW_AGENTS=2 still honors it → cap 2 → exceeded the maximum of 2. So the fix rejects only the non-decimal forms; valid decimal integers pass through unchanged.

Notes (non-blocking)

  • resolveConcurrencyLimit is the only one without a behavioural divergence here (with sequential agents both caps are ≥ 3), so it's confirmed via the debug warning rather than a visible effect — the other three are confirmed behaviourally at a user-visible surface.
  • Minor UX (pre-existing, not introduced by this PR): when a value like 0x2BF20 is rejected, the TUI shows the generic "Workflows have no per-run token cap…" notice; the "this value was invalid" detail only lands in the debug log. Correct and safe, just a little opaque for someone who fat-fingered hex. Possible future polish, not a merge blocker.

Behaviour matches the diff and the PR description; aligning these four sites with the rest of the codebase is the right call. 👍

🇨🇳 中文版

✅ 维护者本地验证 —— 真实 qwen 二进制 A/B

结论:通过 —— 可以合并。 我在本地构建了两个分支并驱动真实 CLI(不是单元测试、也不是 import 直接调函数)。修复在全部四个 resolver 上都改变了可观测的运行时行为,与 PR 描述完全一致;同时合法的纯十进制值仍然生效(无回归)。

方法

同一个 CLI 二进制,只把 core 的 dist/ 在本 PR(19de577)与其 merge-base(01d28a1)之间通过重新构建来回切换 —— 每一侧运行前都先在构建产物 dist 里 grep 确认。真实 TUI,qwen3.7-max--approval-mode yolo。每个环境变量都设成「Number() 接受、但严格十进制解析必须拒绝」的值:0x220x2BF201800000x1016

  • workflow 的两个上限:用保存的 /captest 工作流(三次顺序 agent() 调用)触发。
  • 后台 agent 上限:用三个并发 /fork(用 sleep 让它们保持运行)触发。

A/B 结果

Resolver 环境变量(值) BASE —— Number() + isInteger 本 PR —— parsePositiveIntegerEnv
resolveMaxAgentsPerRun MAX_WORKFLOW_AGENTS=0x2 被当作 2 → 第 3 个 agent() 被拒 → 工作流失败exceeded the maximum of 2 agent() calls per run 被拒绝 → 默认 10003/3 agents,完成
resolveMaxTokensPerWorkflow MAX_TOKENS_PER_WORKFLOW=0x2BF20 被当作 180000/workflows 详情 cap: 180k 被拒绝 → nullcap: (no cap)
resolveMaxConcurrentBackgroundAgents MAX_BACKGROUND_AGENTS=0x2 被当作 2 → 第 3 个 /fork 被拒maximum concurrent background agents (2) reached 被拒绝 → 默认 10三个 fork 全部接受
resolveConcurrencyLimit MAX_WORKFLOW_CONCURRENCY=0x10 被当作 16(静默) 被拒绝 → cpu 派生默认值 + debug 警告

同一个 /workflows 列表同时显示两次运行:

wf_805c…  failed     captest · 2/2 agents · 52/180kt — Workflow exceeded the maximum of 2 agent() calls per run   ← BASE(0x2 被采纳)
wf_f150…  completed  captest · 3/3 agents · 91t                                                                   ← 本 PR(0x2 被拒绝)

Debug 日志 A/BQWEN_DEBUG_LOG_FILE=1):本 PR 的会话打印了 3 条 Invalid QWEN_CODE_MAX_… 警告;BASE 会话打印 0 条 —— 也就是说它静默接受了 hex/科学计数值,这正是 bug 本身。

无回归探针: FIXED 二进制配上纯十进制 MAX_WORKFLOW_AGENTS=2 仍然生效 → 上限 2 → exceeded the maximum of 2。所以这个修复拒绝非十进制形式;合法的十进制整数原样通过。

备注(不阻塞合并)

  • resolveConcurrencyLimit 是唯一在本场景下没有行为差异的(顺序 agent 时两边的上限都 ≥ 3),所以它靠 debug 警告确认,而非可见效果 —— 另外三个都在用户可见的界面上得到了行为级确认。
  • 轻微 UX(既有行为,非本 PR 引入):当 0x2BF20 这类值被拒绝时,TUI 显示的是泛化的「Workflows have no per-run token cap…」提示;「这个值无效」的细节只落在 debug 日志里。行为是正确且安全的,只是对手滑打错 hex 的用户略不直观。可作为后续打磨,不是合并阻塞项。

行为与 diff、PR 描述一致;把这四处与代码库其余部分对齐是正确的做法。👍

Local real-binary verification on macOS (worktree build + tmux A/B). Workflow/background features enabled via QWEN_CODE_ENABLE_WORKFLOWS=1.

@wenshao
wenshao merged commit 1f26cdb into QwenLM:main Jun 23, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants