Skip to content

fix(core): require integer ReadFile pagination params#6381

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
VectorPeak:codex/readfile-integer-pagination
Jul 6, 2026
Merged

fix(core): require integer ReadFile pagination params#6381
wenshao merged 1 commit into
QwenLM:mainfrom
VectorPeak:codex/readfile-integer-pagination

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR tightens ReadFileTool pagination parameters so offset and limit are treated as integer line controls instead of arbitrary numbers.

It changes the tool schema for both fields from number to integer, adds runtime integer validation in ReadFileTool.validateToolParamValues(), and adds regression tests for fractional pagination inputs.

Why it's needed

ReadFileTool describes offset as a 0-based line number and limit as the number of lines to read. Both values are discrete line controls, so they should be integers.

Before this change, the schema exposed both fields as number, and runtime validation only rejected negative offset values and non-positive limit values. Fractional inputs such as:

{
  "file_path": "/tmp/example.txt",
  "offset": 1.5,
  "limit": 1.5
}

could pass the ReadFile-specific validation path. That leaves downstream file-reading code and display text with ambiguous pagination semantics: JavaScript array slicing coerces fractional indexes, while user-facing descriptions can still compute fractional ranges such as lines 2.5-3.5.

This PR rejects those malformed pagination requests at the tool boundary instead of letting fractional line numbers reach the file-read pipeline.

Reviewer Test Plan

How to verify

Reviewers can confirm that:

  • offset and limit are now declared as integer in the ReadFile tool schema.
  • fractional offset values such as 1.5 are rejected.
  • fractional limit values such as 0.5 and 1.5 are rejected.
  • existing valid pagination behavior still works, including offset: 0 and positive integer limits.

Evidence (Before & After)

Before this change, the tool schema advertised:

offset: { type: 'number' }
limit: { type: 'number' }

and ReadFile-specific validation only checked:

params.offset < 0
params.limit <= 0

After this change, both schema and runtime validation require integer semantics:

offset: { type: 'integer' }
limit: { type: 'integer' }

and:

!Number.isInteger(params.offset) || params.offset < 0
!Number.isInteger(params.limit) || params.limit <= 0

The new tests cover the malformed fractional inputs directly.

Tested on

OS Status
🍎 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ✅ tested

Environment (optional)

Windows local validation with Node.js from D:\ZXY\Dev\nodejs.

Commands run on Windows and WSL/Linux:

npm ci --no-audit --no-fund
npm run test --workspace=packages/core -- read-file.test.ts
npm run typecheck --workspace=packages/core
git diff --check

Windows results:

read-file.test.ts: 68 passed, 2 skipped
typecheck: passed
git diff --check: passed

WSL/Linux results with temporary native Node.js v22.13.1:

read-file.test.ts: 70 passed
typecheck: passed
git diff --check: passed

Risk & Scope

  • Main risk or tradeoff: this is a small input-contract tightening. Any caller that was accidentally sending fractional line offsets or limits will now receive validation errors instead of ambiguous reads.
  • Not validated / out of scope: full repository test suite and macOS local validation were not run.
  • Breaking changes / migration notes: no intended breaking change for valid callers; integer pagination inputs continue to work.

Linked Issues

N/A

中文说明

这个 PR 做了什么

这个 PR 收紧了 ReadFileTool 的分页参数校验,让 offsetlimit 按“整数行号 / 整数行数”处理,而不是任意数字。

具体改动包括:将工具 schema 中的 offset / limitnumber 改为 integer,在 ReadFileTool.validateToolParamValues() 中增加运行时整数校验,并补充小数分页参数的回归测试。

为什么需要它

ReadFileTooloffset 表示 0-based 起始行号,limit 表示读取多少行。行号和行数都是离散概念,因此语义上应当是整数。

在本次修复之前,schema 将这两个字段声明为 number,运行时只拒绝负数 offset 和非正数 limit。因此类似下面的小数输入可能绕过 ReadFile 自身的校验:

{
  "file_path": "/tmp/example.txt",
  "offset": 1.5,
  "limit": 1.5
}

这会让下游文件读取逻辑和展示文案出现含混语义:JavaScript array slicing 会对小数索引做隐式整数化处理,而用户可见描述仍可能计算出 lines 2.5-3.5 这类没有实际意义的行号范围。

这个 PR 在工具边界直接拒绝小数分页参数,避免小数行号进入文件读取管线。

Reviewer 测试计划

Reviewers 可以确认:

  • ReadFile 工具 schema 中 offsetlimit 已声明为 integer
  • offset: 1.5 会被拒绝。
  • limit: 0.5limit: 1.5 会被拒绝。
  • 合法分页行为仍然可用,例如 offset: 0 和正整数 limit

证据

修复前 schema 使用:

offset: { type: 'number' }
limit: { type: 'number' }

并且 ReadFile 自身校验只检查:

params.offset < 0
params.limit <= 0

修复后 schema 和运行时校验都明确使用整数语义:

offset: { type: 'integer' }
limit: { type: 'integer' }

并且增加:

!Number.isInteger(params.offset) || params.offset < 0
!Number.isInteger(params.limit) || params.limit <= 0

新增测试直接覆盖了这些小数输入。

风险与范围

主要风险是输入契约被收紧:如果某些调用方之前意外传入小数分页参数,现在会得到校验错误。但这符合 offset / limit 作为行号和行数的实际语义。

本 PR 不改变 PDF pages、notebook 读取、文件内容读取、缓存逻辑或正常整数分页行为。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is input-validation hardening without a live reproduction — no linked issue or before/after showing a real tool call that emitted fractional offset/limit. That said, the validation gap is directly observable in the code: the current params.offset < 0 / params.limit <= 0 checks let 1.5 through, and the display string at line 87 (lines ${offset + 1}-${offset + limit}) would produce nonsense like "lines 2.5–3.5". So the bug is real even if nobody has tripped it yet.

Direction: Clearly aligned — tightening tool-boundary validation for a core tool is housekeeping, not feature creep. The schema change from number to integer is the main load-bearing fix (the LLM gets the JSON Schema and the framework enforces it at parse time); the runtime Number.isInteger() guard is defense-in-depth. No CHANGELOG precedent needed for this kind of fix.

Approach: Scope is tight — schema type change, runtime guard, updated error messages, and tests covering fractional offset, fractional limit, and valid zero-offset. Nothing extra. This is the minimal change.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是输入校验加固,没有提供实际复现——没有关联的 issue 或 before/after 展示真实工具调用产生小数 offset/limit。但校验缺口可以在代码中直接观测到:当前的 params.offset < 0 / params.limit <= 0 检查让 1.5 通过,而第 87 行的显示字符串(lines ${offset + 1}-${offset + limit})会产出 "lines 2.5–3.5" 这样的无意义输出。所以问题真实存在,只是没人触发过。

方向:明显对齐——收紧核心工具的输入校验是日常维护,不是功能膨胀。schema 从 number 改为 integer 是主要修复(LLM 收到 JSON Schema 并由框架在解析时强制执行);运行时 Number.isInteger() 守卫是纵深防御。此类修复不需要 CHANGELOG 先例。

方案:范围紧凑——schema 类型变更、运行时守卫、更新错误消息、覆盖小数 offset/小数 limit/合法零 offset 的测试。没有多余内容。这是最小改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

My independent proposal before reading the diff: change the JSON Schema type from number to integer for both fields, add Number.isInteger() runtime guards, update error messages, and add tests for fractional inputs.

The PR matches this exactly — same files, same approach, same test coverage. No simpler path was missed.

Correctness: Clean. The Number.isInteger() check correctly rejects fractional values while allowing 0 and positive integers. The combined conditions (!Number.isInteger(x) || x < 0 for offset, !Number.isInteger(x) || x <= 0 for limit) handle all edge cases including NaN and Infinity (both rejected by Number.isInteger()). Error messages updated from "number" to "integer" to match.

Tests: 65 → 70 tests, all passing. New coverage: fractional offset (1.5), fractional limit (0.5, 1.5), zero offset (valid), zero/negative limit (invalid). Existing offset-negative and limit-zero tests updated with new messages. Good coverage of the boundary conditions.

Reuse: No new abstractions or helpers — the validation is inline where it belongs. No issues.

No blockers found.

Real-Scenario Testing

Integer pagination with offset=4, limit=11 on a 50-line file — verifying the PR doesn't regress normal behavior.

Before (installed build)

$ qwen -p 'Read the file /tmp/triage-test-file.txt using offset 4 and limit 11' --max-session-turns 2 -y -o text
Warning: running headless with --yolo / approval-mode=yolo and no sandbox.
Lines 5–15 of the 51-line file — all containing `Lorem ipsum dolor sit amet`.
The offset/limit worked correctly: 0-based offset 4 starts at line 5, and 11 lines were returned.

After (this PR via dev build)

$ npm run dev -- -p 'Read the file /tmp/triage-test-file.txt using offset 4 and limit 11' --max-session-turns 2 -y -o text
Warning: running headless with --yolo / approval-mode=yolo and no sandbox.
Lines 5–15 of `/tmp/triage-test-file.txt` — all contain `Lorem ipsum dolor sit amet`.
The file has 51 total lines.

Both produce identical pagination results (lines 5–15, 11 lines from offset 4). Normal integer pagination is unaffected by the schema change.

Unit Tests

# Before (main): 65 passed
# After (this PR): 70 passed — 5 new tests for fractional/integer validation
# Core package typecheck: clean
中文说明

代码审查

我在看 diff 之前的独立方案:将 schema type 从 number 改为 integer,加 Number.isInteger() 运行时守卫,更新错误消息,补充小数输入测试。

PR 完全一致——同样的文件、同样的方案、同样的测试覆盖。没有遗漏更简路径。

正确性: 没有问题。Number.isInteger() 正确拒绝小数值,同时允许 0 和正整数。组合条件处理了所有边界情况,包括 NaNInfinity(都被 Number.isInteger() 拒绝)。错误消息从 "number" 更新为 "integer" 以匹配。

测试: 65 → 70 测试,全部通过。新覆盖:小数 offset (1.5)、小数 limit (0.5, 1.5)、零 offset(合法)、零/负 limit(非法)。已有的 offset 负数和 limit 零测试更新了新消息。边界条件覆盖良好。

复用: 没有新增抽象或辅助函数——校验就在该在的地方内联。没有问题。

未发现阻塞项。

真实场景测试

在 50 行文件上用 offset=4、limit=11 测试整数分页——验证 PR 不会回退正常行为。

修复前(已安装版本)和修复后(本 PR dev build) 均正确返回第 5–15 行(11 行),整数分页行为不受影响。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, minimal input-validation fix. The schema change from number to integer is the load-bearing improvement — it tells both the LLM and the framework that fractional pagination params are invalid, catching them at the JSON Schema validation layer before they reach runtime. The Number.isInteger() guard is defense-in-depth.

The PR matches what I would have written independently: same files, same approach, same test coverage. No scope creep, no unnecessary abstractions, no drive-by refactors. The diff is exactly what the goal needs and nothing more.

Real-scenario testing confirms integer pagination works identically before and after. Unit tests go from 65 → 70, all passing, with good boundary coverage (fractional offset/limit, zero offset, negative limit).

Approving. ✅

中文说明

这是一个干净、最小化的输入校验修复。schema 从 number 改为 integer 是核心改进——它同时告知 LLM 和框架:小数分页参数无效,在 JSON Schema 校验层就拦截。Number.isInteger() 守卫是纵深防御。

PR 和我独立构思的方案完全一致:同样的文件、同样的方案、同样的测试覆盖。没有范围膨胀、没有不必要的抽象、没有顺手重构。diff 恰好是目标所需,不多不少。

真实场景测试确认修复前后整数分页行为一致。单元测试从 65 → 70,全部通过,边界覆盖良好(小数 offset/limit、零 offset、负 limit)。

批准。✅

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. ✅

@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.

No issues found. Clean, focused validation tightening — schema and runtime layers are consistent, tests cover the meaningful boundaries. LGTM! ✅

Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real end-to-end run

I verified this PR locally by driving the real compiled ReadFileTool end-to-end (not just the unit tests), plus A/B toggle experiments that isolate exactly which change enforces what. Verdict: correct and safe to merge. One non-blocking observation below about the runtime guard being redundant/untested.

Environment: Linux (Debian 13, kernel 6.12), Node v22.22.2, fresh npm ci in a dedicated git worktree at PR head fd339c558 (parent 5c8af1a1f). True PR scope confirmed = the 2 files in the diff (50+/12−); the GitHub "files changed" count is accurate.

Methodology

  1. Compiled-artifact E2E — imported the built packages/core/dist/src/tools/read-file.js and called tool.build(params) against a real temp file, capturing the actual thrown message or, on success, the actual getDescription() output. Ran it against both the PR build and a base-main build (same file reverted to pre-PR, core rebuilt).
  2. A/B toggles — reverted each half of the fix independently and re-ran the PR's own read-file.test.ts (70 tests) to prove causation.
  3. Standard gates: read-file.test.ts, typecheck --workspace=packages/core, git diff --check.

Compiled-artifact E2E — real ReadFileTool.build()

Input PR build Base (pre-PR) build
offset: 1.5 params/offset must be integer (schema) ⚠️ accepted → from line 2.5
limit: 0.5 params/limit must be integer (schema) ⚠️ accepted → first 0.5 lines
limit: 1.5 params/limit must be integer (schema) ⚠️ accepted → first 1.5 lines
offset: 1.5, limit: 2 params/offset must be integer (schema) ⚠️ accepted → lines 2.5-3.5
offset: -1 Offset must be a non-negative integer (runtime) …non-negative number
limit: 0 / -1 Limit must be a positive integer (runtime) …positive number
offset: 0, limit: 10 ✅ accepted → lines 1-10 ✅ accepted → lines 1-10

The base column reproduces the exact bug described in the PR: fractional pagination is accepted and renders nonsensical ranges like lines 2.5-3.5. And because JS Array.slice() truncates fractional indexes (['l1','l2','l3'].slice(1.5)['l2','l3']), the displayed range (from line 2.5) and the actual content read (from line 2) diverge — the ambiguous semantics the PR set out to remove. Valid integer inputs (and integer-valued numbers, which JSON/JS treat as integers) are unaffected; offset: 0 still works.

A/B toggle proofs (vitest, read-file.test.ts, 70 tests)

State Result Proves
PR as-is ✅ 70 passed baseline (matches the PR's Linux "70 passed")
Revert schema integernumber only (keep runtime guard) 3 fractional tests fail — they expect params/offset must be integer but get Offset must be a non-negative integer the fractional rejection is driven by the schema change; those tests are pinned to the AJV message
Remove runtime !Number.isInteger() only (keep schema) still 70 passed the runtime integer-guard is shadowed by the schema and has zero test coverage

Why this happens (control flow)

BaseDeclarativeTool.validateToolParams() runs AJV schema validation first and returns on the first error, then calls validateToolParamValues(). So:

  • Fractional inputs are rejected by the schema (type: 'integer') → params/… must be integer. The four-pass coercion in SchemaValidator only fixes numeric strings, so a fractional number stays rejected.
  • The new runtime !Number.isInteger() clauses only ever run for values that already passed the schema (i.e. real integers), so they never fire for fractional inputs on the build() path.

Non-blocking observations (for the merge decision)

  1. The one-line schema change (numberinteger) is the real fix. The runtime !Number.isInteger() additions are harmless defense-in-depth (they'd only matter if the schema-then-runtime order ever changed), but Experiment 2 shows they are fully untested — deleting them keeps all 70 green. Consider either adding a direct validateToolParamValues unit test to lock the behavior in, dropping the clause, or keeping it with a // defense-in-depth comment.
  2. Two message styles for closely related failures: fractional → params/offset must be integer (JSON-pointer/AJV style); negative/zero → Offset must be a non-negative integer (friendly style). This split pre-exists for negatives and isn't a regression — just noting the model/user sees two shapes.

Gate results

  • read-file.test.ts: 70 passed
  • typecheck --workspace=packages/core: passed (tsc --noEmit, rc=0) ✅
  • git diff --check: clean ✅ (the only warning is a pre-existing CRLF note on an unrelated NOTICES.txt)

Recommendation: 👍 merge. The fix does what it claims, valid callers are unaffected, and the reproduction/regression are both confirmed. Optionally address observation #1 (untested runtime guard) either now or in a follow-up.

🀄 中文版(维护者本地真实验证报告)

✅ 维护者本地真实验证(端到端)

我在本地对本 PR 做了端到端真实验证:不仅跑了单测,还直接驱动编译后的真实 ReadFileTool,并通过 A/B 开关实验精确定位“到底是哪一处改动在生效”。结论:改动正确、可以合并。 下面有一条非阻塞的观察,关于运行时那段 Number.isInteger 校验是冗余且没有测试覆盖的。

环境: Linux(Debian 13,内核 6.12)、Node v22.22.2,在独立 git worktree 中对 PR 头 fd339c558(父提交 5c8af1a1f)执行了全新的 npm ci。已确认 PR 真实改动范围就是 diff 里的 2 个文件(50+/12−),GitHub 的 files-changed 数量准确。

验证方法

  1. 编译产物端到端:导入构建后的 packages/core/dist/src/tools/read-file.js,对真实临时文件调用 tool.build(params),抓取真实抛出的错误信息,或成功时抓取真实的 getDescription() 输出。分别在 PR 构建base(main) 构建(把该文件还原到改动前、重新构建 core)上各跑一遍。
  2. A/B 开关:分别单独回退修复的两半,重新运行 PR 自带的 read-file.test.ts(70 个用例),以此证明因果关系。
  3. 常规校验:read-file.test.tstypecheck --workspace=packages/coregit diff --check

编译产物端到端 —— 真实 ReadFileTool.build()

输入 PR 构建 base(改动前)构建
offset: 1.5 params/offset must be integer(schema 层) ⚠️ 通过 → from line 2.5
limit: 0.5 params/limit must be integer(schema 层) ⚠️ 通过 → first 0.5 lines
limit: 1.5 params/limit must be integer(schema 层) ⚠️ 通过 → first 1.5 lines
offset: 1.5, limit: 2 params/offset must be integer(schema 层) ⚠️ 通过 → lines 2.5-3.5
offset: -1 Offset must be a non-negative integer(运行时) …non-negative number
limit: 0 / -1 Limit must be a positive integer(运行时) …positive number
offset: 0, limit: 10 ✅ 通过 → lines 1-10 ✅ 通过 → lines 1-10

base 那一列完整复现了 PR 描述的 bug:小数分页会被接受,并渲染出 lines 2.5-3.5 这类无意义的行号范围。而且由于 JS 的 Array.slice() 会对小数下标做截断(['l1','l2','l3'].slice(1.5)['l2','l3']),展示的范围(from line 2.5)与实际读取的内容(从第 2 行开始)并不一致——这正是本 PR 想消除的含混语义。合法整数输入(以及 JSON/JS 视为整数的取值)不受影响,offset: 0 仍然可用。

A/B 开关证明(vitest,read-file.test.ts,70 用例)

状态 结果 说明
PR 原样 ✅ 70 通过 基线(与 PR 的 Linux “70 passed” 一致)
只回退 schema integernumber(保留运行时校验) 3 个小数用例失败——它们断言 params/offset must be integer,实际却拿到 Offset must be a non-negative integer 小数拒绝是由 schema 改动驱动的;这些用例绑定在 AJV 的报错文案上
只删掉运行时 !Number.isInteger()(保留 schema) 仍然 70 通过 运行时那段整数校验被 schema 完全遮蔽,且零测试覆盖

原因(控制流)

BaseDeclarativeTool.validateToolParams() 先跑 AJV schema 校验,遇到第一个错误就返回,之后才调用 validateToolParamValues()。因此:

  • 小数输入被 schematype: 'integer')拦截 → params/… must be integerSchemaValidator 里的四遍强转只修“数字字符串”,所以一个小数数字依旧会被拒。
  • 新增的运行时 !Number.isInteger() 只会对已经通过 schema(即已经是整数)的值执行,所以在 build() 路径上永远不会因为小数而触发。

非阻塞观察(供合并决策参考)

  1. 真正的修复是那一行 schema 改动(numberinteger)。 运行时新增的 !Number.isInteger() 属于无害的“纵深防御”(只有当 schema→运行时的先后顺序被改变时才有意义),但实验 2 表明它完全没有测试覆盖——删掉后 70 个用例仍全绿。建议二选一/三选一:为 validateToolParamValues 补一个直接的单测把行为固化下来、或删掉这段、或保留并加 // defense-in-depth 注释。
  2. 两种报错风格并存:小数 → params/offset must be integer(JSON-pointer/AJV 风格);负数/零 → Offset must be a non-negative integer(友好风格)。这个分裂在负数场景本就存在,不是本 PR 引入的回归,仅作提示。

校验结果

  • read-file.test.ts70 通过
  • typecheck --workspace=packages/core通过tsc --noEmit,rc=0)✅
  • git diff --check干净 ✅(唯一的告警是无关文件 NOTICES.txt 的既有 CRLF 提示)

建议:👍 合并。 修复达成了目标,合法调用方不受影响,bug 复现与回归防护均已确认。观察点 #1(未覆盖的运行时校验)可现在处理,也可留作后续。

@wenshao
wenshao added this pull request to the merge queue Jul 6, 2026
Merged via the queue into QwenLM:main with commit 4292102 Jul 6, 2026
39 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