Skip to content

feat(serve): page large text files by byte cursor - #8002

Merged
wenshao merged 11 commits into
QwenLM:mainfrom
doudouOUC:pr-b-byte-cursor-paging
Jul 30, 2026
Merged

wenshao merged 11 commits into
QwenLM:mainfrom
doudouOUC:pr-b-byte-cursor-paging

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds bounded byte-cursor paging to workspace text reads across the HTTP, ACP, TypeScript SDK, and daemon MCP surfaces. An ordinary line / limit / maxBytes request can now return hasMore and an opaque nextCursor; passing that cursor back seeks directly to the next line start instead of scanning from byte 0 again.

Large reads remain bounded to 256 KiB of returned UTF-8 and 8 MiB of line-offset scanning. Reads stay bound to the opened file descriptor, reject binary or unsupported streamed encodings, preserve BOM and CRLF metadata, avoid splitting UTF-8 characters, and keep append-only log cursors valid while rejecting detectable replacement or truncation.

The branch has also been reconciled with current main, including the handle-bound snapshot contract and the SDK bundle-budget update. Conflict resolution retained the stricter file-size snapshot, workspace-runtime isolation, symlink checks, and mutation-error precedence while removing duplicate implementations.

Why it's needed

Line-number paging scans from the beginning on every request, so walking a large log page by page is O(n²). A deep line request is also refused after the 8 MiB scan budget. The only prior O(1) alternative was raw byte reads, which force clients to reimplement line boundaries, encoding handling, multibyte safety, and binary-file refusal.

Reviewer Test Plan

How to verify

  1. Read a UTF-8 log larger than 256 KiB with a finite limit; confirm the response is bounded and returns hasMore: true plus nextCursor.
  2. Follow nextCursor with the same path and limit until it is absent; join page contents with \n and confirm they reproduce the source text.
  3. Append lines after the first page and confirm the outstanding cursor remains valid. Replace or truncate the file and confirm the cursor returns hash_mismatch.
  4. Confirm cursor plus line and malformed cursors return parse_error, a deep line offset beyond 8 MiB returns file_too_large, and large non-UTF-8 or binary content returns binary_file.
  5. Confirm a line longer than the output cap returns a valid UTF-8 prefix, reports hasMore: true, and never emits an unsafe mid-line cursor; an unterminated final line must end paging without an error.

Evidence (Before & After)

Before: each text page identified by line number rescanned the prefix, deep pages failed after 8 MiB, and the original CI run stopped at the TypeScript SDK bundle budget (180318 > 180224).

After: daemon-issued byte cursors resume at line starts in O(1), all conflict paths are reconciled with current main, and the SDK bundle succeeds under the 178 KiB budget that accommodates the combined cursor and current-main API surfaces.

Verification Result
npm run lint Passed
npm run build Passed, including the TypeScript SDK bundle
npm run typecheck Passed across all workspaces
Core targeted tests 95 passed
CLI filesystem / route / bridge tests 177 passed
TypeScript SDK targeted tests 405 passed
Prettier and staged-diff checks Passed

Tested on

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS 26.4.1 (25E253), Node.js 22.22.3, local non-sandboxed workspace.

Risk & Scope

  • Main risk or tradeoff: Byte offsets, UTF-8 boundaries, and concurrent mutation can create page seams. The implementation mints only line-start cursors, uses fatal UTF-8 decoding and descriptor-bound reads, caps returned and scanned bytes separately, and rechecks inode, size, mtime, ctime, and symlink state. Allowing append-only growth means metadata cannot distinguish a pure append from a same-inode rewrite that keeps or grows the file; validating the full prefix would make every page O(n) and defeat the cursor.
  • Not validated / out of scope: Windows and Linux were not tested locally; client-side adoption in WebUI is not included. The change touches core infrastructure and adds roughly 1.2k production-logic lines, so it requires maintainer sign-off under the repository triage rules.
  • Breaking changes / migration notes: Existing line-number reads whose starting point lies beyond the new 8 MiB scan budget now return 413 file_too_large instead of content. Large non-UTF-8 or binary reads may now return 422 binary_file instead of 413 file_too_large. Clients that branch on these status codes must handle both responses; deep offsets should use nextCursor from a shallower read or readBytes. The cursor request parameter and response fields are otherwise additive, and clients can preflight the new workspace_file_read_cursor capability.

Linked Issues

Related: #7967 and #7947. Addresses the O(n²) paging gap left by #7946.

中文说明

此 PR 的改动

此 PR 为工作区文本读取增加了有界的字节游标分页,并贯通 HTTP、ACP、TypeScript SDK 与 daemon MCP 接口。普通的 line / limit / maxBytes 请求现在可以返回 hasMore 和不透明的 nextCursor;再次传入该游标时会直接定位到下一行行首,而不再从字节 0 重新扫描。

大文件读取仍限制为最多返回 256 KiB UTF-8 内容,并最多扫描 8 MiB 来解析行偏移。读取始终绑定到已打开的文件描述符,拒绝二进制内容或流式路径不支持的编码,保留 BOM 与 CRLF 元数据,避免拆分 UTF-8 字符;仅追加日志的游标保持有效,同时可检测的文件替换或截断会被拒绝。

该分支也已与当前 main 完成整合,包括基于句柄的快照契约和 SDK bundle 预算更新。冲突解决保留了更严格的文件大小快照、工作区运行时隔离、符号链接检查以及文件变更错误优先级,并移除了重复实现。

为什么需要此改动

按行号分页时,每次请求都会从文件开头扫描,因此逐页读取大型日志的复杂度是 O(n²)。超过 8 MiB 扫描预算的深层行请求也会被拒绝。此前唯一的 O(1) 替代方案是读取原始字节,这会迫使客户端自行重新实现行边界、编码处理、多字节字符安全和二进制文件拒绝逻辑。

Reviewer 测试计划

如何验证

  1. 使用有限的 limit 读取一个大于 256 KiB 的 UTF-8 日志;确认响应大小有界,并返回 hasMore: true 和 nextCursor。
  2. 对同一路径和相同 limit 持续传入 nextCursor,直到游标消失;使用 \n 拼接各页内容,并确认结果还原源文本。
  3. 在第一页之后追加若干行,确认未使用的游标仍然有效。替换或截断文件后,确认游标返回 hash_mismatch。
  4. 确认同时传入 cursor 与 line 以及传入格式错误的游标会返回 parse_error;超过 8 MiB 的深层行偏移返回 file_too_large;大型非 UTF-8 或二进制内容返回 binary_file。
  5. 确认超过输出上限的单行会返回合法的 UTF-8 前缀、报告 hasMore: true,且绝不产生不安全的行中游标;没有结尾换行符的最后一行必须正常结束分页而不能报错。

证据(改动前与改动后)

改动前:每个按行号定位的文本页都会重新扫描文件前缀,深层页面在扫描 8 MiB 后失败,原始 CI 还会在 TypeScript SDK bundle 预算检查处停止(180318 > 180224)。

改动后:daemon 签发的字节游标能够以 O(1) 从行首继续读取,所有冲突路径均已与当前 main 完成整合,SDK bundle 也在为游标 API 与当前 main API 合并体积设置的 178 KiB 预算内成功构建。

验证项 结果
npm run lint 通过
npm run build 通过,包括 TypeScript SDK bundle
npm run typecheck 所有 workspace 均通过
Core 定向测试 95 项通过
CLI 文件系统 / 路由 / bridge 测试 177 项通过
TypeScript SDK 定向测试 405 项通过
Prettier 与暂存区 diff 检查 通过

测试平台

操作系统 状态
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 26.4.1(25E253)、Node.js 22.22.3、本地非沙箱 workspace。

风险与范围

  • 主要风险或权衡:字节偏移、UTF-8 边界和并发文件变更可能产生分页接缝。实现只签发行首游标,使用严格 UTF-8 解码和绑定文件描述符的读取,分别限制返回字节数与扫描字节数,并重新检查 inode、大小、mtime、ctime 和符号链接状态。为了允许仅追加增长,元数据无法区分纯追加与保持同一 inode 且大小不缩小的原地改写;验证完整前缀会使每页重新变为 O(n),违背游标设计目标。
  • 未验证 / 不在范围内:没有在本地测试 Windows 和 Linux;不包含 WebUI 客户端接入。此改动涉及 core 基础设施,并新增约 1.2k 行生产逻辑,因此根据仓库分诊规则需要 maintainer 确认。
  • 破坏性变更 / 迁移说明:既有的按行号读取若起点超过新增的 8 MiB 扫描预算,现在会从返回内容变为 413 file_too_large;大型非 UTF-8 或二进制读取也可能从 413 file_too_large 改为 422 binary_file。依赖这些状态码分支的客户端需同时处理两种响应;深层偏移应从较浅读取返回的 nextCursor 继续分页,或使用 readBytes。游标请求参数和响应字段本身仍是增量兼容,客户端可以预检新的 workspace_file_read_cursor capability。

关联问题

相关:#7967 和 #7947。处理 #7946 遗留的 O(n²) 分页缺口。

doudouOUC and others added 5 commits July 28, 2026 23:47
…s set

Follow-up to the bounded large-text read path. Three changes:

Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.

Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).

Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.

Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.

Co-Authored-By: Claude Opus 5 <[email protected]>
…lpers

PR QwenLM#7947 pinned large-text reads to one inode by threading a caller-owned
FileHandle into readTextRange as an optional field, plus a second field,
forceStreaming, to suppress the buffering fast path. Two optional fields
produced four combinations: one meaningful, one used by a single test, one
unreachable, and — in readFileWithLineAndLimit — one that silently fell
through to a by-path read, defeating the reason the caller opened a handle.

Unify the two encoding detectors. detectFileEncoding now takes a path or a
borrowed handle, so detectFileHandleEncoding is deleted along with the
message discrepancy between them: an encoding iconv-lite cannot load now
raises LargeNonUtf8TextError naming that encoding rather than deferring to
the decoder's generic invalid-utf8 variant. Both still refuse the file, and
the Serve boundary maps both to binary_file.

Split the reader into readTextRange (path) and readTextRangeFromHandle
(always streams, both byte bounds required). The unreachable combination and
its untested readFileHandleBuffer are gone, and with no fileHandle parameter
left for readFileWithLineAndLimit to ignore, the RangeError guarding that
fallthrough is deleted too — the trap can no longer be expressed.

CoreReadTextFileHandleRequest drops its required stats field. Nothing
downstream read it, and because the ACP request type it extends permits
extra properties, TypeScript accepted the dead argument silently.

readFileHandleChunks becomes chunksFromHandle(fh, from) — the one seam
byte-cursor text paging needs.

No observable change at the Serve boundary: its 222 tests pass unmodified.
Two fileSystemService tests were deleted rather than repaired; they asserted
the arguments readFileWithLineAndLimit received, which is nothing once the
handle path stops calling it. Their coverage lives in read-text-range.test.ts
against real files and in workspace-file-system.test.ts at the real boundary.

258 production lines in core, net -71 overall.

Co-Authored-By: Claude Opus 5 <[email protected]>
Self-audit follow-up to f55c867. Two fields survived the reshape that the
handle path never reads:

- `stats` was documented as required ("must pass the Stats captured from that
  handle") and nothing downstream read it. The handle path always streams, so
  it never needs a size to choose a strategy, and the encoding probe does its
  own fstat.
- `path` became dead once readTextRangeFromHandle replaced the path-plus-handle
  call. Errors are labelled with the path by the Serve boundary that owns it.

Neither was caught by the compiler: the ACP ReadTextFileRequest the type
derived from permits extra properties, so the CLI kept passing both silently.
That is the argument for declaring the type standalone rather than Omit-ing
four of six inherited fields and quietly re-admitting the rest.

Also record the second behaviour delta of the detector merge in the design
doc: detectFileEncoding catches I/O errors and falls back to 'utf-8', where
detectFileHandleEncoding let them propagate. The failure is not lost — a handle
that fails the 8 KiB probe fails the streaming read immediately after — but a
different call now reports it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Line offsets address a byte stream, so `readText` resolves them by scanning
from byte 0. Paging a large log that way is O(n^2) across pages, and past
MAX_TEXT_SCAN_BYTES (8 MiB) a deep page is refused outright — agents had no
O(1) path short of dropping to GET /file/bytes and splitting lines themselves,
losing encoding handling, multibyte safety, and the binary_file refusal.

A response that leaves content behind now returns `hasMore`, and where a file
byte offset is derivable, an opaque `nextCursor`. Passing it back as `cursor`
resumes in O(1). Page 1 is an ordinary `limit` read, so clients never compute
byte offsets themselves, and a paging loop does not break when a file happens
to be small.

The cursor is unsigned base64url JSON carrying {off, size, dev, ino}, matching
encodeOrganizedCursor rather than the HMAC-signed transcript codec: the path is
re-resolved through the workspace boundary on every request, so a forged cursor
can only move the offset within a file the caller may already read — what
GET /file/bytes?offset= allows today. What the payload is for is staleness:
a replaced or truncated file yields hash_mismatch instead of bytes from the
wrong place, while an append leaves an outstanding cursor valid — the case the
feature exists for.

Every minted cursor points at the start of a line. When a single line exceeds
maxOutputBytes the reader emits a truncated prefix and skips to the next line
rather than resuming mid-line, because a mid-line cursor makes the following
page snap forward and silently drop the rest of that line at the seam. Windows
cut mid-line by a byte cap therefore report hasMore with no cursor, as do
non-UTF-8 snapshot reads whose decoded text is a UTF-8 re-encoding with no
mapping back to file offsets. That is why hasMore is a field rather than a
restatement of nextCursor.

Cursor reads branch before the size check, not by widening the window gate:
a cursor read of a file under MAX_READ_BYTES would otherwise land on the
snapshot path, which knows only line/limit, and silently return line 0.

Adds the workspace_file_read_cursor capability, per the convention that new
behavior gets a new tag, and retargets the scan-budget hint at cursor paging.

Co-Authored-By: Claude Opus 5 <[email protected]>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 29, 2026
wenshao pushed a commit that referenced this pull request Jul 29, 2026
The engage ack rode a pull_request:labeled round-trip: takeover-command
applies the label, the labeled event routes, and the takeover-ack job
posts the confirmation. That event has now been observed to simply not
fire twice in one day (#7999 — the author read the silence as failure
and removed the label; #8002 — an engaged fork PR with no ack for
hours), and fork label events can never ack at all since they carry no
secrets: a fork /takeover stayed silent until the next scan picked the
PR up (2h41m on #7993).

takeover-command now posts the engage ack directly after applying the
label — every admission gate has already passed at that point, so
'engaged' is truthful for in-repo and fork PRs alike; the fork variant
adds the expectation that the first round comes from the next scheduled
scan. The route side suppresses the label-path ack when the label
sender is the bot (only the ack: the immediate scan still routes), and
the review-scan's existing first-pickup ack dedups against the
command's comment and heals it if the post failed.

Two more silent paths become audible while here: a /takeover on a
stacked (non-main-base) PR now refuses out loud instead of dropping
with only a log line, and a /takeover stop on a non-main PR now
proceeds to remove the label instead of leaving it stuck.
@doudouOUC

doudouOUC commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Resolved the current review/triage items and refreshed the branch in c8794114d4:

  • Merged current main and reconciled all 11 conflicts across the handle-bound reader, workspace boundary, HTTP/ACP surfaces, SDK, tests, and docs.
  • Confirmed the prior red checks all stopped at the same TypeScript SDK bundle budget. The bundle now passes after incorporating the main-branch budget update from fix(sdk): raise daemon browser bundle budget to 177KB #8024.
  • Replaced the PR description with the current repository template, including an explicit Why it's needed, reviewer-oriented verification steps, current evidence, risk/scope, and a complete Chinese translation.
  • Audited cursor boundaries, scan/output caps, file mutation and symlink error precedence, workspace-runtime ownership, SDK compatibility, and all new option read sites. The audit also fixed two uncovered edge cases: truncating an unterminated final line no longer errors at EOF, and byte-truncated cursor pages now report hasMore: true even when no safe next cursor exists.
  • Answering the triage question about sliceDecodedText: its Buffer.byteLength walk only runs on the full-snapshot path, which is capped at 256 KiB, so it cannot turn into an unbounded scan.
  • No unresolved inline review threads were present.

Final local verification on macOS 26.4.1 / Node.js 22.22.3:

Verification Result
npm run lint Passed
npm run build Passed, including SDK bundle
npm run typecheck Passed
Core targeted tests 95 passed
CLI filesystem / route / bridge tests 177 passed
TypeScript SDK targeted tests 405 passed
Prettier and staged-diff checks Passed

The earlier automated Qwen review run timed out; I did not enqueue another long-running review because the repository requires Qwen Review to be requested explicitly by name. Maintainer sign-off is still required for the core-infrastructure scope.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 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. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

✅ AutoFix round 3 finished — view run. See this round's report below.

中文说明

✅ AutoFix 第 3 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no action taken

Triaged the feedback newer than the last evaluation on PR #8002. Nothing this
round requires a code change:

  • Reviews: none.
  • Inline comments: none.
  • Issue-level comments: all informational —
    • the automated review timeout notice (a CI/retry hint, not a change request);
    • the PR author's own status report describing the merge of main, conflict
      resolution, PR-description refresh, and self-audit already completed in
      c8794114d (work already done, no request directed at this bot);
    • the serve-daemon A/B report, which confirms the PR adds the
      workspace_file_read_cursor capability as intended (a positive signal, not
      a defect).
  • Failed checks: the only non-green check is label: CANCELLED. This is a
    superseded duplicate run of the "PR self-report label" job — a second label
    run from the same workflow completed SUCCESS. It is not a real failure and
    needs no code change. All substantive CI checks (Test, Serve A/B, SDK Java,
    web-shell E2E Smoke) are green; the automated review-pr check is still
    IN_PROGRESS.

No code was modified, so no verification commands were run and there are no
inline threads to resolve or reply to.

中文说明

Autofix 审查轮次 —— 未采取任何行动

已对 PR #8002 中自上次评估以来的新反馈进行分类。本轮没有任何需要修改代码的内容:

  • 审查(Reviews): 无。
  • 行内评论(Inline comments): 无。
  • Issue 级评论: 全部为信息性内容 ——
    • 自动审查超时通知(属于 CI / 重试提示,并非修改请求);
    • PR 作者自行提交的状态报告,描述了已在 c8794114d 中完成的合并 main、解决冲突、刷新 PR 描述以及自我审计等工作(属于已完成的工作,并未向本机器人提出请求);
    • serve-daemon A/B 报告,确认本 PR 按预期新增了 workspace_file_read_cursor 能力(是正向信号,并非缺陷)。
  • 失败的检查: 唯一非绿的检查是 label: CANCELLED。这是 "PR self-report label" 作业被取代的重复运行 —— 同一工作流的第二次 label 运行已 SUCCESS 完成。它不是真正的失败,无需修改代码。所有实质性 CI 检查(Test、Serve A/B、SDK Java、web-shell E2E Smoke)均为绿色;自动 review-pr 检查仍在 IN_PROGRESS 中。

由于未修改任何代码,因此未运行任何验证命令,也没有需要解决或回复的行内讨论串。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

wenshao pushed a commit that referenced this pull request Jul 29, 2026
- Corrected the comment's cost model: idle candidates hit 'continue'
  before the TARGETS append, so they never contend for the 10-target
  cap — the real win is the shared inspection budget plus the serial
  scan-walk latency (the walk is what delayed #8002's pickup by ~6
  minutes), and the comment now says exactly that.
- Slot quantum changed from the hour to the scan tick (600s, the same
  quantum as ROT_OFF): an hourly slot against the */10 cron meant 6
  back-to-back inspections then a ~3h blind window per PR — same 25%
  average, terrible shape. The gap is now bounded at ~30 minutes, which
  is what the operator-facing strings promise ('gap ≤30m').
- The two scan-only signals updatedAt cannot see (a base conflict
  appearing when main moves; still-red checks awaiting the redcheck
  marker) are named in the comment instead of papered over.
- The per-candidate jq fork became a single precomputed set + a bash
  substring test, matching the busy skip's idiom and the 'free' claim.
- Tests: the skip predicate and set builder got a behavioral replay
  (idle+out-of-slot defers, idle+in-slot inspects, fresh inspects,
  missing-from-lookup inspects); the two byte-distance assertions
  became a loop-head slice (comment growth cannot red-light CI, and
  budget-consuming code between the skips and the increment fails);
  the --json field pin is order-independent; the 3600 quantum is
  pinned OUT.
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Addressed the latest review round in 80b296cbbc.

Feedback Action
Empty UTF-8 truncation could mint a non-advancing cursor Fixed: the internal line snap advances by at least one byte; added a multibyte one-byte-budget regression test.
Cursor-bound argument validation lacked tests Fixed: added parameterized coverage for output/snap budgets, offsets, file size, and limit.
Mid-line cursor error mapping lacked boundary coverage Fixed: added a workspace-file-system test asserting parse_error.
Capability integration test collection was uncertain Confirmed: the root integration script collects the file and merge-queue/release CI invoke it; the targeted file passed locally.
Same-size overwrite delays lacked rationale Fixed: restored concise ctime/mtime rationale comments.
Exact final page minted an EOF cursor Fixed: only emit nextByteOffset before sourceSize; added regression coverage.
Internal oversized-line snap bypasses the scan budget Deferred: directly restoring request.maxSnapBytes would treat budget exhaustion as EOF and silently drop later lines when the newline is beyond the cap. This needs a separate error/cursor policy to bound work without data loss.

Verification on 80b296cbbc:

  • npm run build
  • npm run typecheck
  • npm run bundle
  • npm run lint
  • Core targeted tests: 108 passed
  • CLI workspace filesystem tests: 117 passed
  • qwen-serve-routes integration tests: 35 passed
  • Independent post-fix probes confirmed the multibyte cursor advances and the EOF cursor is gone; the deferred long-line behavior remains data-preserving.

The cancelled duplicate label job on the previous SHA was not retried because this push starts a fresh CI cycle.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action needed this round

All seven inline findings from the automated reviewer were already addressed in commit 80b296c (current HEAD), and @doudouOUC posted replies on each thread:

Finding Status
[Critical] Empty UTF-8 truncation mints a non-advancing cursor (rc:3674446611) Fixed: Math.max(consumedBytes, 1) at read-text-range.ts:381; regression test covers multibyte first character with one-byte budget
[Suggestion] Cursor-bound validation branches lack unit tests (rc:3674446637) Fixed: parameterized tests for maxOutputBytes, maxSnapBytes, startOffset/fileSize, and limit in fileSystemService.test.ts
[Suggestion] CursorNotAtLineBoundaryError → parse_error mapping lacks workspace-level test (rc:3674446642) Fixed: test at workspace-file-system.test.ts:402 asserts parse_error for a mid-line cursor
[Suggestion] Integration test unreachable by npm test (rc:3674446648) Confirmed: collected by test:integration:cli:sandbox:none and test:integration:no-ak:sandbox:none; merge-queue and release CI invoke those scripts; 35/35 tests passed locally
[Suggestion] Deleted comments explaining 50 ms sleep (rc:3674446651) Fixed: concise ctime/mtime rationale comments restored at lines 709 and 775
[Suggestion] Exact final page mints an EOF cursor (rc:3674446661) Fixed: nextByteOffset emitted only when consumedBytes < sourceSize (read-text-range.ts:680); regression test added
[Suggestion] Internal snap bypasses MAX_TEXT_SCAN_BYTES (rc:3674446673) Deferred with recorded reason: restoring request.maxSnapBytes would treat budget exhaustion as EOF and silently drop later lines; needs a separate error/cursor policy

The only new review this round is the automated reviewer's CHANGES_REQUESTED noting that "Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally." This is a known design choice — the ordinary PR job skips integration tests by design; merge-queue and release CI run them via the scripts above. The author ran the suite locally (35/35 passed) and documented this in the PR thread. No failed checks or still-red checks are listed.

No code changes are warranted.

中文说明

本轮无需操作

自动审查器提出的全部七条行内意见已在提交 80b296c(当前 HEAD)中处理,@doudouOUC 已在各线程回复:

意见 状态
[Critical] 空 UTF-8 截断会生成不前进的游标 (rc:3674446611) 已修复:read-text-range.ts:381 使用 Math.max(consumedBytes, 1);回归测试覆盖了多字节首字符 + 一字节预算的场景
[Suggestion] 游标边界验证分支缺少单元测试 (rc:3674446637) 已修复:fileSystemService.test.ts 中为 maxOutputBytes、maxSnapBytes、startOffset/fileSize 和 limit 添加了参数化测试
[Suggestion] CursorNotAtLineBoundaryError → parse_error 映射缺少 workspace 层测试 (rc:3674446642) 已修复:workspace-file-system.test.ts:402 断言行中游标返回 parse_error
[Suggestion] 集成测试无法被 npm test 收集 (rc:3674446648) 已确认:被 test:integration:cli:sandbox:none 和 test:integration:no-ak:sandbox:none 收集;merge-queue 和 release CI 调用这些脚本;本地 35/35 测试通过
[Suggestion] 删除了解释 50 ms 睡眠的注释 (rc:3674446651) 已修复:在第 709 行和第 775 行恢复了简洁的 ctime/mtime 原理注释
[Suggestion] 精确最后一页生成 EOF 游标 (rc:3674446661) 已修复:仅当 consumedBytes < sourceSize 时发出 nextByteOffset(read-text-range.ts:680);已添加回归测试
[Suggestion] 内部 snap 绕过 MAX_TEXT_SCAN_BYTES (rc:3674446673) 已记录原因并推迟:直接恢复 request.maxSnapBytes 会将预算耗尽视为 EOF 并静默丢弃后续行;需要单独的错误/游标策略

本轮唯一的新审查是自动审查器的 CHANGES_REQUESTED,指出"Integration Tests (CLI, No Sandbox) 在 CI 中被跳过且未在本地运行"。这是已知的设计选择——普通 PR 任务按设计跳过集成测试;merge-queue 和 release CI 通过上述脚本运行。作者已在本地运行该套件(35/35 通过)并在 PR 线程中记录。没有列出失败或持续红色的检查项。

无需代码变更。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer local verification — real daemon, base vs head

I built both arms from source in isolated worktrees and drove two real qwen serve daemons against the same on-disk workspace, so every result below comes from actual HTTP traffic rather than mocks.

  • head 80b296cbbcfd8c1ba213b6bab6c392f1ed4cd9ef → 127.0.0.1:8802
  • base cc617e6707 (merge-base with main) → 127.0.0.1:8803
  • macOS 15.7.7 (Darwin 24.6.0), Node 22.23.1. Fixtures: 1.6 MB LF log with CJK/emoji, CRLF, UTF-8 BOM, 13 MB deep log, 420 KB single-line file, unterminated-final-line, 484 B/line JSONL, GBK, binary, small control.
  • A/B isolation checked before measuring: workspace_file_read_cursor present in the head bundle and absent in base, neither dist/ references the other's worktree, and the two cli.js hashes differ.

Automated suites — 1848 passed, 0 failed

Suite Result
packages/core — read-text-range (40) + fileSystemService (68) 108 passed
packages/cli — workspace-file-system (117), workspace-file-read (40), transport (286), bridge-file-system-adapter (21), server (836) 1300 passed
packages/sdk-typescript — DaemonClient (302), acpRouteTable (103) 405 passed
integration-tests/cli/qwen-serve-routes.test.ts (real spawned daemon) 35 passed

Live end-to-end — 32 checks against the running daemon

live E2E

All five items of your Reviewer Test Plan reproduce, plus adversarial probes. Highlights:

  • Round-trip fidelity. Walking cursors to exhaustion reproduces LF / CRLF / BOM / no-trailing-newline / small-file sources, and returns content byte-identical to the pre-existing line walk on every fixture — the new path changes cost, not content.
  • No UTF-8 damage at seams. 36 pages across a CJK/emoji corpus, zero replacement characters.
  • Concurrency. Appending after a cursor is minted keeps it valid and the new line is reachable; truncate and inode-replace both yield 409 hash_mismatch.
  • Forged cursors stay inside the file. Seven malformed shapes (garbage, empty, over-long, valid-base64 wrong shape, wrong version, negative offset, cursor+line) all return 400 parse_error. A hand-written past-EOF offset returns an empty final page; a hand-written mid-line offset snaps forward to a line start and never emits a partial line. A cursor cannot bypass the non-UTF-8 refusal.

The performance claim holds

latency by depth

Per-page latency across 36 pages of the 1.6 MB log: cursor paging is flat (0.85× from first-8 to last-8 average) while line paging climbs (base 1.55×, head 1.80×). Full-file walk, median of 3: base·line 221 ms → head·cursor 140 ms.

Base vs head differential — 140 requests per arm

A/B differential

102 identical, 38 different. The differences are overwhelmingly loosening: 21 requests that base refused with 413 file_too_large now return content (maxBytes windows into large files), and 16 refusals move from a misleading 413 file_too_large to an accurate 422 binary_file. One difference goes the other way, and it is the item worth your attention.


Findings

1 — MAX_TEXT_SCAN_BYTES is introduced by this PR and is a wire-visible breaking change; the PR body says "Breaking changes: None."

MAX_TEXT_SCAN_BYTES is absent from current origin/main (566d2c1279) and is added by this branch's own commits 748c888a66 / e784e6d43d. Measured on a 13 MB log:

GET /file?path=deep.log&line=130000&limit=5
  base cc617e67 → 200, 376 B of content
  head 80b296cb → 413 file_too_large

Bisected threshold: line 90 552 still works, line 90 553 is refused — its byte offset is 8 388 604, i.e. exactly the new 8 MiB budget. Any existing client reading by line past 8 MiB into a file gets a 413 where it previously got content. Files that large are not exotic in a working tree (a coverage report this repo generates, packages/cli/coverage/coverage-final.json, is 21 MB after a test run).

I think the tightening itself is defensible — an unbounded scan behind a security boundary is a real cost, the qwen-serve-protocol.md change documents the new limit, the error hint names the cursor, and I confirmed the same region is reachable by paging (66 requests, 421 ms). But it should be declared: please update Risk & Scope → Breaking changes rather than leaving "None".

2 — the same applies to the 413 → 422 move on large non-UTF-8 / binary content. Sixteen of the 38 diffs are file_too_large → binary_file. More accurate, but a client branching on the 413 status will now miss them. Worth one line in the same section.

3 — a first page cut by the byte cap returns hasMore: true with nextCursor: null and no documented way forward.

Whether the entry page mints a cursor depends on which cap binds first. On the 484 B/line JSONL fixture:

limit=2000 → 262144 B  hasMore=true  nextCursor=NULL
limit=1000 → 262144 B  hasMore=true  nextCursor=NULL
limit= 600 → 262144 B  hasMore=true  nextCursor=NULL
limit= 500 → 241389 B  hasMore=true  nextCursor=yes

The client has to discover experimentally that it must lower limit until the page becomes line-bound. Good news, and I want to be clear about it: this is bootstrap-only — a mid-walk page never dead-ends. I walked the whole JSONL file at limit=400 and every resumed page minted an advancing cursor, because the cursor reader emits whole lines only. The docs currently give exactly one example of nextCursor: null (a small non-UTF-8 file with a limit), which is the rarest case; the two common ones are byte-cap truncation and finding 4.

4 — from a cold start, the tail after a line longer than the output cap is unreachable by paging.

On the 420 KB single-line fixture, the entry page returns a valid 262 143 B UTF-8 prefix with hasMore: true and no cursor, so a cursor walk terminates after one request having never reached the 50 ordinary lines that follow. The reader itself handles this correctly — seeded with a cursor at offset 0 it skips the giant line and mints offset 420001, exactly the next line start. It is only the entry point that cannot produce that first cursor. The workaround is line=2, which is not documented.

Findings 3 and 4 are the same root cause (hasMore && !byteTruncated && decodedBytesMatchSource gates cursor minting on the line path) and are correctness-safe by design — I'd treat them as a doc addition now, and optionally a follow-up that lets the line path drop its partial trailing line and mint the next line-start offset, which is what the cursor path already does.

Verdict

Correctness is solid — I found nothing that returns wrong bytes, loses content mid-walk, or lets a forged cursor escape the file. The TOCTOU discipline, the line-start invariant, the append-tolerant staleness check, and the UTF-8 handling all hold up under adversarial probing, and the O(1) claim is measurably true.

My ask before merge is documentation, not code: correct the "Breaking changes: None" line to name the deep-line 413 and the 413 → 422 reclassification, and add a sentence to qwen-serve-protocol.md listing the byte-truncation and oversized-line cases where nextCursor is null and how a client bootstraps past them. With that, this is good to land from my side.

中文说明

Maintainer 本地验证 —— 真实 daemon,base 与 head 对比

我在隔离的 worktree 中分别从源码构建了两个分支,并启动两个真实的 qwen serve daemon 指向同一个磁盘工作区,因此以下所有结论都来自真实 HTTP 流量,而非 mock。

  • head 80b296cbbcfd8c1ba213b6bab6c392f1ed4cd9ef → 127.0.0.1:8802
  • base cc617e6707(与 main 的 merge-base)→ 127.0.0.1:8803
  • macOS 15.7.7(Darwin 24.6.0)、Node 22.23.1。测试文件:含中日韩文字与 emoji 的 1.6 MB LF 日志、CRLF、UTF-8 BOM、13 MB 深层日志、420 KB 单行文件、无结尾换行文件、每行 484 B 的 JSONL、GBK、二进制、以及小文件对照组。
  • 测量前已确认 A/B 隔离:head 产物包含 workspace_file_read_cursor 而 base 不含,两侧 dist/ 均未引用对方 worktree,两个 cli.js 哈希不同。

自动化测试 —— 1848 项通过,0 项失败

测试套件 结果
packages/core —— read-text-range(40)+ fileSystemService(68) 108 通过
packages/cli —— workspace-file-system(117)、workspace-file-read(40)、transport(286)、bridge-file-system-adapter(21)、server(836) 1300 通过
packages/sdk-typescript —— DaemonClient(302)、acpRouteTable(103) 405 通过
integration-tests/cli/qwen-serve-routes.test.ts(真实启动 daemon) 35 通过

真实端到端验证 —— 针对运行中的 daemon 执行 32 项检查

你的 Reviewer 测试计划中的五项全部复现,另加对抗性探测。要点:

  • 往返保真度。 持续跟随游标直到耗尽,可还原 LF / CRLF / BOM / 无结尾换行 / 小文件各源文件,并且在每个测试文件上返回的内容与既有 line 分页逐字节一致 —— 新路径改变的是开销,而不是内容。
  • 接缝处无 UTF-8 损坏。 在中日韩与 emoji 语料上跨越 36 页,替换字符数为 0。
  • 并发。 游标签发后追加内容,游标仍然有效且新增行可被读到;截断与更换 inode 均返回 409 hash_mismatch。
  • 伪造游标无法越出文件。 七种畸形形态(乱码、空、超长、合法 base64 但结构错误、版本错误、负偏移、cursor 与 line 同时传入)全部返回 400 parse_error。手写的越过 EOF 的偏移返回空的末页;手写的行中偏移会向前吸附到行首,绝不返回半行。游标也无法绕过非 UTF-8 拒绝逻辑。

性能结论成立

1.6 MB 日志共 36 页的单页延迟:游标分页保持平坦(首 8 页与末 8 页均值之比为 0.85×),而按行分页持续上升(base 1.55×、head 1.80×)。整文件遍历(3 次取中位数):base·line 221 ms → head·cursor 140 ms。

base 与 head 差异对比 —— 每侧 140 个请求

102 项相同,38 项不同。差异绝大多数是放宽:21 个此前被 base 以 413 file_too_large 拒绝的请求现在能返回内容(对大文件的 maxBytes 窗口读取),另有 16 个拒绝从含义误导的 413 file_too_large 改为准确的 422 binary_file。只有一项差异方向相反,也正是需要你留意的地方。


发现的问题

1 —— MAX_TEXT_SCAN_BYTES 由本 PR 引入,属于对外可见的破坏性变更;但 PR 描述写的是「Breaking changes: None」。

MAX_TEXT_SCAN_BYTES 在当前 origin/main(566d2c1279)中并不存在,是由本分支自身的 748c888a66 / e784e6d43d 两个提交引入的。在 13 MB 日志上实测:

GET /file?path=deep.log&line=130000&limit=5
  base cc617e67 → 200,返回 376 B 内容
  head 80b296cb → 413 file_too_large

二分定位阈值:第 90 552 行仍可读取,第 90 553 行被拒绝 —— 其字节偏移为 8 388 604,正好是新的 8 MiB 预算。任何既有客户端按 line 读取文件中 8 MiB 之后的位置,都会从「返回内容」变成 413。这种大小的文件在工作区中并不罕见(本仓库跑完测试后生成的 packages/cli/coverage/coverage-final.json 就有 21 MB)。

我认为收紧本身是合理的 —— 安全边界后的无界扫描确实有实际代价,qwen-serve-protocol.md 的改动已记录了新上限,错误提示也指向了游标,而且我确认了同一区域确实可以通过分页到达(66 次请求、421 ms)。但这一点应当声明:请更新 Risk & Scope → Breaking changes,不要再写「None」。

2 —— 大型非 UTF-8 / 二进制内容的 413 → 422 变化同理。 38 项差异中有 16 项是 file_too_large → binary_file。这更准确,但依据 413 状态码分支的客户端会漏掉它们。建议在同一节补一句说明。

3 —— 被字节上限截断的首页会返回 hasMore: true 且 nextCursor: null,且没有文档说明后续该怎么做。

入口页是否签发游标,取决于哪个上限先生效。在每行 484 B 的 JSONL 测试文件上:

limit=2000 → 262144 B  hasMore=true  nextCursor=NULL
limit=1000 → 262144 B  hasMore=true  nextCursor=NULL
limit= 600 → 262144 B  hasMore=true  nextCursor=NULL
limit= 500 → 241389 B  hasMore=true  nextCursor=yes

客户端只能靠试探发现自己必须不断调小 limit,直到该页变为按行受限。这里有个好消息,我想明确说明:该问题仅存在于起步阶段 —— 分页过程中的页面绝不会走进死路。 我以 limit=400 遍历了整个 JSONL 文件,每个续读页都签发了向前推进的游标,因为游标读取器只输出完整行。文档目前只给了 nextCursor: null 的一个例子(带 limit 读取的小型非 UTF-8 文件),而那恰恰是最罕见的情形;常见的两种是字节上限截断和下面的第 4 点。

4 —— 从冷启动开始,超过输出上限的单行之后的内容无法通过分页读到。

在 420 KB 单行测试文件上,入口页返回了合法的 262 143 B UTF-8 前缀,hasMore: true 且无游标,因此游标遍历在一次请求后即终止,始终读不到其后的 50 行普通内容。读取器本身处理是正确的 —— 用偏移为 0 的游标驱动时,它会跳过该巨型行并签发偏移 420001,正是下一行行首。问题只在于入口点无法产出第一个游标。绕过方法是 line=2,但这一点没有文档说明。

第 3、4 点根因相同(hasMore && !byteTruncated && decodedBytesMatchSource 限制了按行路径签发游标),且在设计上是内容安全的 —— 我建议当前先补充文档,后续可选地让按行路径丢弃末尾的半行并签发下一行行首偏移,也就是游标路径已经在做的事。

结论

正确性扎实 —— 我没有发现任何返回错误字节、分页途中丢失内容、或让伪造游标越出文件的问题。 TOCTOU 处理、行首不变量、容忍追加的陈旧性检查以及 UTF-8 处理在对抗性探测下都站得住,O(1) 的结论也确实可测量地成立。

合并前我的请求是文档层面的,而非代码:修正「Breaking changes: None」这一行,写明深层 line 的 413 与 413 → 422 的重新归类;并在 qwen-serve-protocol.md 中补一句,列出 nextCursor 为 null 的字节截断与超长行两种情形,以及客户端该如何越过它们。做到这些,从我这边看就可以合入了。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@wenshao Addressed in 524ef96a74.

  • Updated the bilingual Risk & Scope section to disclose the deep-line 413 file_too_large behavior and the large non-UTF-8/binary 413 → 422 binary_file reclassification.
  • Documented the byte-cap and oversized-line cases where hasMore: true can have nextCursor: null, including lowering limit, advancing with an explicit line, and using /file/bytes when the complete oversized line is needed.

Thank you for the thorough real-daemon A/B verification.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review feedback update — 524ef96a74

Feedback Decision Action
Deep line reads past 8 MiB and large binary/non-UTF-8 status changes were missing from breaking-change notes Agree Updated the PR body in English and Chinese with the 413 and 413 → 422 compatibility impact and migration guidance.
Byte-capped and oversized-line entry pages can return hasMore: true with nextCursor: null without bootstrap guidance Agree Updated the protocol docs with safe recovery paths: lower limit, advance via explicit line, or use /file/bytes for the full oversized line.

Validation: Prettier check and repository pre-commit hook passed. Review threads resolved: 0/0 (the feedback was a top-level PR comment, not an inline review thread).

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Review feedback — no code change required

All four findings from @wenshao's maintainer verification were already addressed in commit 524ef96a74 (the current HEAD):

Finding Status
1 — MAX_TEXT_SCAN_BYTES deep-line 413 not declared as breaking Addressed: PR body updated with 413 compatibility impact and migration guidance
2 — Large binary/non-UTF-8 413 → 422 reclassification Addressed: PR body updated with 413 → 422 disclosure
3 — Byte-capped entry page returns hasMore: true / nextCursor: null without guidance Addressed: qwen-serve-protocol.md documents the byte-cap case and recovery paths (lower limit, explicit line, /file/bytes)
4 — Oversized line makes tail unreachable from cold start Addressed: same docs addition covers the oversized-line bootstrap (line=2, then cursors)

Failed check: Real daemon E2E / Java 11

This check runs scripts/run-java-daemon-sdk-e2e.ts, which starts a real daemon and drives it with the Java SDK (DaemonServeE2ETest). The test exercises prompt/tool/permission, deadline, cancel, and teardown flows — it does not call any file-read endpoint or exercise cursor paging.

Investigation:

  • The daemon starts correctly from the bundled CLI (verified locally: node dist/cli.js serve binds and logs listening on).
  • All 1 562 unit tests pass locally (108 core + 1 014 CLI + 405 SDK-TypeScript).
  • All 35 integration tests in cli/qwen-serve-routes.test.ts pass locally (real spawned daemon).
  • The PR does not modify packages/sdk-java/ or any code path the Java E2E test exercises.
  • Java and Maven are unavailable in this sandbox, so the exact CI failure cannot be reproduced here. The local harness fails at spawn mvn ENOENT, confirming the daemon side is healthy up to the Maven step.

Without CI log access or a Java toolchain, the root cause of the Java 11 failure cannot be determined from this environment. The evidence above shows the PR's changed code paths are not exercised by that test.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run bundle — passed
  • vitest packages/core (read-text-range.test.ts, fileSystemService.test.ts) — 108 passed
  • vitest packages/cli (workspace-file-system.test.ts, workspace-file-read.test.ts, bridge-file-system-adapter.test.ts, server.test.ts) — 1 014 passed
  • vitest packages/sdk-typescript (DaemonClient.test.ts, acpRouteTable.test.ts) — 405 passed
  • integration cli/qwen-serve-routes.test.ts (real spawned daemon) — 35 passed
  • daemon startup smoke test (node dist/cli.js serve) — started and listened successfully
中文说明

审查反馈 — 无需代码变更

@wenshao 的 maintainer 验证中提出的四项发现均已在提交 524ef96a74(当前 HEAD)中处理:

发现 状态
1 — MAX_TEXT_SCAN_BYTES 导致的深层 line 读取 413 未声明为破坏性变更 已处理:PR 描述已更新,说明 413 兼容性影响及迁移指引
2 — 大型二进制/非 UTF-8 文件 413 → 422 重新归类 已处理:PR 描述已更新,披露 413 → 422 变化
3 — 被字节上限截断的入口页返回 hasMore: true / nextCursor: null 但无后续指引 已处理:qwen-serve-protocol.md 已记录字节上限情形及恢复路径(降低 limit、显式 line、/file/bytes)
4 — 超长行导致冷启动时后续内容不可达 已处理:同一文档补充覆盖了超长行的引导方式(line=2,然后使用游标)

失败检查:Real daemon E2E / Java 11

该检查运行 scripts/run-java-daemon-sdk-e2e.ts,启动真实 daemon 并使用 Java SDK(DaemonServeE2ETest)驱动。测试覆盖 prompt/tool/permission、deadline、cancel 和 teardown 流程,不调用任何文件读取端点,也不涉及游标分页。

调查:

  • daemon 可从打包后的 CLI 正常启动(本地验证:node dist/cli.js serve 成功绑定并输出 listening on)。
  • 全部 1 562 项单元测试在本地通过(core 108 + CLI 1 014 + SDK-TypeScript 405)。
  • cli/qwen-serve-routes.test.ts 的全部 35 项集成测试在本地通过(真实启动 daemon)。
  • 本 PR 未修改 packages/sdk-java/ 或 Java E2E 测试所执行的任何代码路径。
  • 本沙箱中无 Java 和 Maven,因此无法在此复现确切的 CI 失败。本地 harness 在 spawn mvn ENOENT 处失败,确认 daemon 侧在 Maven 步骤之前运行正常。

由于无法访问 CI 日志且缺少 Java 工具链,无法从当前环境确定 Java 11 失败的根本原因。以上证据表明该 PR 变更的代码路径未被该测试执行。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run bundle — 通过
  • vitest packages/core(read-text-range.test.ts、fileSystemService.test.ts)— 108 项通过
  • vitest packages/cli(workspace-file-system.test.ts、workspace-file-read.test.ts、bridge-file-system-adapter.test.ts、server.test.ts)— 1 014 项通过
  • vitest packages/sdk-typescript(DaemonClient.test.ts、acpRouteTable.test.ts)— 405 项通过
  • 集成测试 cli/qwen-serve-routes.test.ts(真实启动 daemon)— 35 项通过
  • daemon 启动冒烟测试(node dist/cli.js serve)— 成功启动并监听

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

CI fix update — 5b514150cb

Failure Classification Fix
Java daemon E2E stopped during prepare: merged browser daemon SDK bundle was 181,252 bytes, 4 bytes above the 177 KiB budget Branch-related, deterministic merge-integration failure; not a flake Raised the established bundle budget by 1 KiB to 178 KiB, leaving 1,020 bytes of headroom for the current merged API surface.

Verification:

  • Exact commit: full npm run build and npm run typecheck passed.
  • Independent latest-main synthesis: no merge conflict; SDK build passed with 181,252 / 182,272 bytes.
  • PR body evidence updated in English and Chinese.

Flaky retries used: 0. Review threads resolved: 0/0 (this was a CI failure, not review feedback).

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action required this round

There is no actionable review feedback on PR #8002:

  • Reviews: none submitted.
  • Inline comments: none.
  • Issue-level comments: one informational CI-fix update from @doudouOUC describing the bundle-budget raise already committed as 5b514150c (current HEAD). No change requested.
  • Failed checks: label: CANCELLED — a cancelled label workflow, not a code or test failure.
  • Still-red checks: none.

No code changes were made.

中文说明

本轮无需操作

PR #8002 当前没有需要处理的评审反馈:

  • 评审(Reviews):无。
  • 行内评论(Inline comments):无。
  • Issue 级别评论:@doudouOUC 发布了一条信息性评论,说明了已在 5b514150c(当前 HEAD)中提交的 bundle 预算调整。未提出任何修改要求。
  • 失败的检查:label: CANCELLED —— 这是一个被取消的标签工作流,并非代码或测试失败。
  • 持续失败的检查:无。

本轮未做任何代码变更。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed (PR #8002)

Both findings from the automated reviewer were Suggestion-level. Both are addressed with focused test coverage that mirrors existing patterns; no production code changed.

[rc:3677562635] ACP dispatch cursor validation has no dedicated test coverage — RESOLVED

Decision: Implement. The ACP _qwen/file/read surface validates cursor independently of the HTTP route, and only cursor: undefined forwarding was asserted. A future divergence (e.g. a different MAX_TEXT_CURSOR_CHARS on one surface) would go uncaught.

Changes (packages/cli/src/serve/acp-http/transport.test.ts):

  • Added _qwen/file/read forwards a valid cursor and returns paged content, mirroring the HTTP route paging test: it forwards a cursor, asserts the paged reply (content, truncated, nextCursor), and asserts readText receives the cursor.
  • Extended the rejects invalid window params it.each table with { cursor: '' }, { cursor: 123 }, and { cursor: 'x'.repeat(MAX_TEXT_CURSOR_CHARS + 1) }, mirroring the HTTP route's malformed-cursor → 400 test (here RPC.INVALID_PARAMS / -32602, with readText not called). Imported MAX_TEXT_CURSOR_CHARS from ../fs/index.js.

[rc:3677562651] Cursor path LargeNonUtf8TextError → binary_file mapping is never exercised — RESOLVED

Decision: Implement. The only cursor + non-UTF-8 test used placeholder dev: '0', ino: '0', which trip the staleness gate (hash_mismatch) before decoding begins, leaving the binary_file reclassification dead from the test's perspective.

Changes (packages/cli/src/serve/fs/workspace-file-system.test.ts):

  • Added maps a cursor read of oversized non-UTF-8 text to binary_file. It reuses the UTF-16-with-BOM fixture (the BOM makes looksBinary return false, so the binary probe does not fire first) but supplies real dev/ino/size from fsp.stat, so the staleness gate passes and decoding reaches readTextCursorFromHandle → LargeNonUtf8TextError → binary_file. It asserts kind === 'binary_file' and the convert … UTF-8 hint, which distinguishes this mapping from the binary-probe path (whose hint is use readBytes for binary content). A mutation to file_too_large now turns this test red.

Conflict notes

--conflict false; no merge performed.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/cli src/serve/fs/workspace-file-system.test.ts -t "cursor" — 9 passed (109 skipped)
  • vitest packages/cli src/serve/acp-http/transport.test.ts -t "file/read" — 35 passed (255 skipped)

No settings source changed, so no settings-schema regeneration was required. Only test files were touched, so no bundled-CLI/integration run was required.

中文说明

已处理的审查反馈(PR #8002)

自动审查器给出的两条发现均为 Suggestion(建议)级别。两者都通过聚焦的测试覆盖予以解决,这些测试沿用了既有模式;未改动任何生产代码。

[rc:3677562635] ACP dispatch 的游标验证缺少专门测试覆盖 — 已解决

决定: 实施。ACP 的 _qwen/file/read 接口独立于 HTTP 路由对 cursor 做验证,而此前仅断言了 cursor: undefined 的转发。未来若出现分歧(例如两侧使用不同的 MAX_TEXT_CURSOR_CHARS),将没有任何测试能发现。

改动(packages/cli/src/serve/acp-http/transport.test.ts):

  • 新增 _qwen/file/read forwards a valid cursor and returns paged content,对应 HTTP 路由的分页测试:转发一个游标,断言分页返回结果(content、truncated、nextCursor),并断言 readText 收到了该游标。
  • 在 rejects invalid window params 的 it.each 表格中补充 { cursor: '' }、{ cursor: 123 } 和 { cursor: 'x'.repeat(MAX_TEXT_CURSOR_CHARS + 1) },对应 HTTP 路由的畸形游标 → 400 测试(此处为 RPC.INVALID_PARAMS / -32602,且 readText 不被调用)。并从 ../fs/index.js 导入了 MAX_TEXT_CURSOR_CHARS。

[rc:3677562651] 游标路径的 LargeNonUtf8TextError → binary_file 映射从未被测试覆盖 — 已解决

决定: 实施。唯一的游标 + 非 UTF-8 测试使用了占位的 dev: '0', ino: '0',会在解码开始前就触发过期校验门(hash_mismatch),使得 binary_file 重分类从测试角度看是死代码。

改动(packages/cli/src/serve/fs/workspace-file-system.test.ts):

  • 新增 maps a cursor read of oversized non-UTF-8 text to binary_file。它复用了带 BOM 的 UTF-16 固件(BOM 使 looksBinary 返回 false,因此二进制探针不会先触发),但提供来自 fsp.stat 的真实 dev/ino/size,从而通过过期校验门,使解码进入 readTextCursorFromHandle → LargeNonUtf8TextError → binary_file。它断言 kind === 'binary_file' 以及 convert … UTF-8 提示语,从而将此映射与二进制探针路径(其提示语为 use readBytes for binary content)区分开来。若将其突变为 file_too_large,此测试会立即变红。

冲突说明

--conflict false;未执行合并。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/cli src/serve/fs/workspace-file-system.test.ts -t "cursor" — 9 通过(109 跳过)
  • vitest packages/cli src/serve/acp-http/transport.test.ts -t "file/read" — 35 通过(255 跳过)

未改动任何 settings 源,因此无需重新生成 settings schema。仅改动了测试文件,因此无需运行打包 CLI / 集成测试。

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-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Round summary

Addressed both inline findings from the automated review.

Feedback points

  1. [Critical] rc:3678434597 — sawCrlf set before budget guard → Fixed

    Moved the sawCrlf assignment from before the output-budget guard to after it, adjacent to each lines.push(...) call. A CRLF line that is seen but excluded by the budget no longer flips the page's lineEnding metadata. The first-line truncation branch (which does push) retains its own sawCrlf update. Added a regression test using the reviewer's exact scenario ("aaa\nbbb\nccc\r\n" with maxOutputBytes = 8).

  2. [Suggestion] rc:3678434602 — sequence diagram branch order → Fixed

    Reordered the alt branches in the readText sequence diagram so cursor supplied is the first branch, before file <= 256 KiB, matching the code's actual evaluation order in readTextFromResolvedFile.

  3. Review-level CHANGES_REQUESTED — integration tests skipped in CI → Not actionable in code

    This is a CI infrastructure observation (the Integration Tests suite was skipped), not a code defect. No code change applies.

Conflict notes

No conflicts (--conflict false).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/core src/utils/read-text-range.test.ts — 41 passed (including the new regression test)
中文说明

轮次总结

处理了自动审查中的两个行内发现。

反馈要点

  1. [Critical] rc:3678434597 — sawCrlf 在预算判断之前赋值 → 已修复

    将 sawCrlf 赋值从输出预算判断之前移至其后,紧邻每个 lines.push(...) 调用。被预算排除的 CRLF 行不再翻转该页的 lineEnding 元数据。首行截断分支(确实会 push)保留了自己的 sawCrlf 更新。使用审查者的精确场景("aaa\nbbb\nccc\r\n" 配合 maxOutputBytes = 8)添加了回归测试。

  2. [Suggestion] rc:3678434602 — 时序图分支顺序 → 已修复

    重新排列了 readText 时序图中的 alt 分支,使 cursor supplied 成为第一个分支,排在 file <= 256 KiB 之前,与 readTextFromResolvedFile 中代码的实际判断顺序一致。

  3. 审查级别 CHANGES_REQUESTED — 集成测试在 CI 中被跳过 → 代码层面无法处理

    这是 CI 基础设施的观察(集成测试套件被跳过),而非代码缺陷。无适用的代码变更。

冲突说明

无冲突(--conflict false)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/core src/utils/read-text-range.test.ts — 41 个测试通过(包含新增的回归测试)

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-dev-bot pushed a commit that referenced this pull request Jul 30, 2026
…#8049)

Round-2 verification showed the scheduled scan lands every ~40-70 min on
this repo, not every 10 min, so the (epoch/600)%4 slot is an independent
~25% draw per scan, not a deterministic 1-in-4 rotation. The gap is
geometric (measured median ~2h, p90 ~6h), not bounded at ~40m.

Reword the operator-facing strings (skip echo, fleet row) and the workflow
comment to state the behaviour probabilistically and drop the false
"bounded" / "no PR is unlucky forever" claims; correct the misattributed
#8002 latency (queue/startup, not the serial walk). Relax the tests so a
truthful number is not a CI failure: pin the mod-4 time-quantum shape
instead of the exact 600s constant, and drop the /3600 exclusion that
forbade the better-tailed quantum. Mechanism logic is unchanged.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action taken

Both inline suggestions on this PR have already been explicitly deferred by maintainer @doudouOUC with recorded reasoning on their respective threads:

  1. Duplicate chunk.slice in readLargeUtf8Range (rc:3678961373) — deferred as a non-correctness micro-optimization. The duplicate slice occurs only for selected lines whose returned content is output-budget bounded; a deep scan over unselected lines does not incur the claimed duplicate. Revisitable in a focused follow-up.

  2. Missing dedicated decodeTextCursor field-validation tests (rc:3678961376) — deferred as missing-test coverage rather than an existing defect. The current validation is correct; the additional test matrix belongs in a focused follow-up.

Both deferrals are consistent with the repository's review policy: after roughly five review rounds, only Critical correctness, security, data-loss, or regression fixes are landed, and remaining Suggestions are deferred to follow-up work.

No failed checks, no still-red checks, no conflicts, and no CHANGES_REQUESTED reviews are present. No code change is warranted this round.

中文说明

未采取任何操作

本 PR 上的两条行内建议均已由维护者 @doudouOUC 在各自的主题中明确记录了推迟理由:

  1. readLargeUtf8Range 中重复的 chunk.slice(rc:3678961373)——作为非正确性的微优化被推迟。重复的 slice 仅发生在被选中的行上,而这些行的返回内容受输出预算约束;对未选中行的深层扫描不会产生所声称的重复开销。可在后续的专项跟进中重新审视。

  2. 缺少专门的 decodeTextCursor 字段校验测试(rc:3678961376)——作为缺失的测试覆盖而非现有缺陷被推迟。当前的校验逻辑是正确的;额外的测试矩阵应放在后续的专项跟进中。

两项推迟均符合仓库的审查策略:在大约五轮审查之后,仅接受 Critical 级别的正确性、安全性、数据丢失或回归修复,其余建议推迟到后续工作中处理。

当前没有失败的检查、没有持续失败的检查、没有冲突,也没有 CHANGES_REQUESTED 审查。本轮无需进行代码变更。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification (round 2) — head aa32ed13a6

Follow-up to my round-1 report (head 80b296cbbc). Four commits have landed since: 524ef96a74 docs · 5b514150cb sdk budget · 2fafb448e0 tests · aa32ed13a6 the sawCrlf fix. The merge-base is still cc617e6707, so the base arm is unchanged and every round-1 number stays comparable.

I rebuilt the head arm from source and re-ran everything against two real qwen serve daemons on one workspace — head aa32ed13a6 → :8802, base cc617e6707 → :8803. A/B isolation re-checked at the wire: GET /capabilities reports workspace_file_read_cursor on head and not on base, and neither dist/ references the other worktree. macOS 15.7.7 (Darwin 24.6.0), Node 22.23.1.

Every round-1 finding, re-tested at this head

R1 findings status

All four round-1 findings are addressed, and none of them stand at this head. All four were documentation asks, so I re-ran the underlying behaviour and executed the newly written workarounds rather than just reading them:

# Round-1 ask Status
1 MAX_TEXT_SCAN_BYTES breaking change undeclared Addressed. Body now names the deep-line 413 explicitly. Behaviour re-measured unchanged: base 200 → head 413, boundary at byte 8 388 605 (≈ 8 MiB, measured at limit=1; the exact line depends on limit because the scan must also cover the requested lines). Region still reachable by cursors — 65 requests, 377 ms.
2 413 → 422 reclassification undeclared Addressed, same body line. Re-measured: 16 of 154 matrix requests move 413 file_too_large → 422 binary_file.
3 byte-cap first page: hasMore:true + nextCursor:null, no documented recovery Addressed by 524ef96a74. I executed the advice: halving limit from 2000 mints a cursor at 500 after 2 probes, then walks to EOF in 6 pages.
4 tail after an oversized line unreachable from a cold start Addressed by 524ef96a74, and both documented routes work: line=2 + cursors recovers all 50 tail lines; GET /file/bytes serves the 140 KB line (64 KiB per call, with truncated: true so a client can page it).

aa32ed13a6 is a real fix, and it is load-bearing

sawCrlf verified

This commit moves one line: sawCrlf = true used to run at the top of emit(), before the byte-budget early return — so a CRLF line deferred to the next page still flipped the current page's lineEnding. I imported the pre-fix and post-fix modules into one process (the only variable is that line's position) and ran nine real files through real FileHandles:

  • pre-fix wrong on 2 fixtures, aa32ed13a6 wrong on 0, with the other 7 fixtures agreeing across both arms — including pure-CRLF and "emitted CRLF + deferred LF", so the fix does not weaken genuine CRLF detection.
  • content and nextOffset are identical on all 9 — this is a metadata-only fix, no content risk.
  • Confirmed on the wire too: a page capped at 9 199 B over a mixed LF/CRLF file returns lineEnding: lf, and the CRLF block at offset 9 200 still returns crlf.

This is worth flagging as more than cosmetic: reads report lineEnding, and writeTextFile accepts a client-supplied lineEnding — so a client that paged, edited, and wrote back could have converted an LF region to CRLF.

The added test does defend it. A 3-cell matrix over the real vitest runner: the previous head's own suite passes 40/40 over the buggy code (a genuine escape), the new test set fails exactly one case over that same buggy code — does not let a budget-excluded CRLF line flip lineEnding, the assertion the commit adds — and 41/41 pass at the real head.

Automated suites — 1854 passed, 0 failed

Suite Result
packages/core — read-text-range (41) + fileSystemService (68) 109 passed
packages/cli — workspace-file-system (118), workspace-file-read (40), acp-http/transport (290), bridge-file-system-adapter (21), server (836) 1305 passed
packages/sdk-typescript — DaemonClient (302), acpRouteTable (103) 405 passed
integration-tests/cli/qwen-serve-routes (real spawned daemon) 35 passed

That is +6 versus round 1's 1848, which is exactly the six tests the delta adds (core +1, workspace-file-system +1, transport +4). CI on the PR is also clean — 25 success, 43 skipped, 0 failing.

Base vs head differential — 154 requests per arm

A/B differential

I widened the matrix for this round: added a mixed LF/CRLF fixture and started comparing lineEnding as well as status, errorKind and content, so a line-ending regression on the pre-existing line path would surface rather than be assumed absent.

116 identical, 38 different — the same three classes and the same counts as round 1 (21 loosening, 16 reclassification, 1 tightening). The raw run flags 22 lineEnding differences, but every one of them is just the base arm returning 413 with no lineEnding field at all; restricted to requests where both arms answered 200, there are 0 lineEnding differences. And the cursor walk still reproduces the line walk byte for byte on all six text fixtures.

I also spot-checked the 07-workspace-filesystem.md branch reorder in aa32ed13a6: it is accurate — the cursor branch really is evaluated before the ≤ 256 KiB snapshot path (dispatch at workspace-file-system.ts:1435), and a cursor on an 18 KB file is honoured rather than falling into the snapshot reader.


One new finding — minor, and the only thing I'd change

5b514150cb raises the SDK browser-bundle budget from 177 KB to 178 KB, but the gated artifact does not need it.

The gate measures packages/sdk-typescript/dist/daemon/index.js (build.js:183). Built at this head:

measured            180 396 B  = 176.17 KB
budget 177 KB       181 248 B  → fits, 852 B of headroom
this PR's delta        101 B   (base cc617e6707 = 180 295 B)

I reverted only that constant to 177 * 1024 and re-ran the build: the gate passes. As a positive control I clamped it to 170 KB and the gate does fire (Browser daemon SDK bundle is 180396 bytes; expected <= 174080), so it is not silently broken. Both arms bundled with the lockfile-pinned [email protected] (resolved from packages/sdk-typescript/node_modules, not the root 0.25.6), so this is not a toolchain-drift artifact.

The comment attributes the bump to "workspace file byte-cursor paging after merging the workspace pairing approval SDK surface", which suggests it was needed at some intermediate state of the branch — but at the current head this PR only adds 101 bytes and there is still 852 bytes of room. Since main also just moved this constant in #8024, I'd rather not loosen a deliberate guard that isn't binding: please drop 5b514150cb and keep the budget at 177 KB. If CI disagrees on a different runner I'm happy to be shown otherwise — that would be worth knowing, since it would mean the artifact is platform-sensitive.

Verdict

All four of my round-1 asks are satisfied, and the one new code commit is a genuine bug fix that I confirmed is load-bearing, correctly scoped, and properly tested. Correctness at this head is where it was in round 1 — nothing returns wrong bytes, loses content mid-walk, or lets a forged cursor escape the file — and the differential against the merge-base is still overwhelmingly loosening, with the one tightening case now disclosed in the body.

This is good to merge from my side. The bundle-budget bump is the only outstanding item and it is a one-line revert, not a blocker — land it either way, but I'd prefer the guard stay at 177 KB.

中文说明

Maintainer 复验(第 2 轮)—— head aa32ed13a6

这是对我第 1 轮报告(head 80b296cbbc)的后续。此后新增了四个提交:524ef96a74 文档 · 5b514150cb SDK 体积预算 · 2fafb448e0 测试 · aa32ed13a6 sawCrlf 修复。merge-base 仍为 cc617e6707,因此 base 侧未变,第 1 轮的所有数据仍可直接对比。

我从源码重新构建了 head 侧,并针对两个真实的 qwen serve daemon(同一个工作区)重跑了全部验证 —— head aa32ed13a6 → :8802,base cc617e6707 → :8803。已在协议层重新确认 A/B 隔离:GET /capabilities 在 head 返回 workspace_file_read_cursor、base 不返回,且两侧 dist/ 均未引用对方 worktree。环境:macOS 15.7.7(Darwin 24.6.0)、Node 22.23.1。

第 1 轮每一项发现,均在本 head 重新测试

四项发现全部已处理,在本 head 上均不再成立。 四项原本都是文档层面的诉求,因此我不仅重跑了底层行为,还实际执行了新写入文档的绕过方案,而不是只读一遍:

# 第 1 轮诉求 状态
1 MAX_TEXT_SCAN_BYTES 破坏性变更未声明 已处理。 PR 描述现已明确写出深层 line 的 413。行为实测未变:base 200 → head 413,边界在字节 8 388 605(≈ 8 MiB,在 limit=1 下测得;具体行号取决于 limit,因为扫描还需覆盖所请求的行)。该区域仍可通过游标到达 —— 65 次请求、377 ms。
2 413 → 422 重新归类未声明 已处理,同一行描述。实测:154 个矩阵请求中有 16 个从 413 file_too_large 变为 422 binary_file。
3 被字节上限截断的首页返回 hasMore:true + nextCursor:null,且无文档说明如何继续 已由 524ef96a74 处理。 我实际执行了该建议:从 2000 开始折半,limit=500 时(第 2 次探测)签发出游标,随后 6 页走到 EOF。
4 冷启动时超长行之后的内容无法读到 已由 524ef96a74 处理,且文档给出的两条路径都可用:line=2 加游标可恢复全部 50 行尾部内容;GET /file/bytes 能取到这个 140 KB 的行(每次调用 64 KiB,并带 truncated: true 供客户端继续分页)。

aa32ed13a6 是真实修复,且确实起作用

该提交只移动了一行:sawCrlf = true 原先位于 emit() 开头、在字节预算的提前返回之前 —— 因此一个被推迟到下一页的 CRLF 行仍会改变当前页的 lineEnding。我把修复前与修复后的两个模块导入同一个进程(唯一变量就是这一行的位置),用九个真实文件经真实 FileHandle 运行:

  • 修复前有 2 个用例结论错误,aa32ed13a6 为 0 个,同时 7 个对照用例结论一致 —— 其中包括纯 CRLF 以及「已输出 CRLF + 被推迟的 LF」,说明修复并未削弱真正的 CRLF 识别能力。
  • 九个用例的 content 与 nextOffset 完全一致 —— 这是仅涉及元数据的修复,不存在内容风险。
  • 协议层同样确认:在混合 LF/CRLF 文件上以 9 199 B 为上限的页返回 lineEnding: lf,而偏移 9 200 处的 CRLF 区块仍返回 crlf。

这一点值得强调,它并不只是「显示问题」:读取会返回 lineEnding,而 writeTextFile 接受客户端传入的 lineEnding —— 因此一个「分页读取 → 编辑 → 写回」的客户端本可能把 LF 区域转换成 CRLF。

新增的测试确实守住了这个修复。 在真实 vitest runner 上做了三格矩阵:上一个 head 自带的测试套件在有 bug 的代码上通过 40/40(说明这是一次真实的逃逸),而新测试集在同样有 bug 的代码上恰好失败一项 —— does not let a budget-excluded CRLF line flip lineEnding,正是该提交新增的断言 —— 在真实 head 上则 41/41 全部通过。

自动化测试 —— 1854 项通过,0 项失败

测试套件 结果
packages/core —— read-text-range(41)+ fileSystemService(68) 109 通过
packages/cli —— workspace-file-system(118)、workspace-file-read(40)、acp-http/transport(290)、bridge-file-system-adapter(21)、server(836) 1305 通过
packages/sdk-typescript —— DaemonClient(302)、acpRouteTable(103) 405 通过
integration-tests/cli/qwen-serve-routes(真实启动 daemon) 35 通过

相比第 1 轮的 1848 项增加了 6 项,正好等于本次增量新增的六个测试(core +1、workspace-file-system +1、transport +4)。PR 上的 CI 同样是干净的 —— 25 项成功、43 项跳过、0 项失败。

base 与 head 差异对比 —— 每侧 154 个请求

本轮我扩大了矩阵:新增一个混合 LF/CRLF 的测试文件,并且除状态码、errorKind、内容之外,也开始比较 lineEnding,这样既有 line 路径上的行尾回归就会被暴露出来,而不是靠假设它不存在。

116 项相同、38 项不同 —— 与第 1 轮完全相同的三个类别、相同的数量(21 项放宽、16 项重新归类、1 项收紧)。原始输出标记了 22 处 lineEnding 差异,但其中每一处都只是 base 侧返回 413、根本没有 lineEnding 字段;如果只看两侧都返回 200 的请求,lineEnding 差异为 0。此外,游标遍历在全部六个文本测试文件上仍与 line 遍历逐字节一致。

我也抽查了 aa32ed13a6 中对 07-workspace-filesystem.md 分支顺序的调整:结论是准确的 —— 游标分支确实在 ≤ 256 KiB 快照路径之前判定(分发点在 workspace-file-system.ts:1435),并且对一个 18 KB 文件使用游标会走游标读取器,而不会落入快照读取器。


一处新发现 —— 轻微,也是我唯一希望改动的地方

5b514150cb 把 SDK 浏览器包体积预算从 177 KB 提到 178 KB,但被检查的产物并不需要这次上调。

该检查测量的是 packages/sdk-typescript/dist/daemon/index.js(build.js:183)。在本 head 构建后:

实测              180 396 B  = 176.17 KB
177 KB 预算       181 248 B  → 通过,余量 852 B
本 PR 自身增量        101 B   (base cc617e6707 = 180 295 B)

我只把该常量改回 177 * 1024 并重新构建:检查通过。 作为正向对照,我把它压到 170 KB,检查确实会报错(Browser daemon SDK bundle is 180396 bytes; expected <= 174080),说明这个门禁并没有静默失效。两侧构建都使用了 lockfile 锁定的 [email protected](从 packages/sdk-typescript/node_modules 解析,而非根目录的 0.25.6),因此这不是工具链版本漂移造成的假象。

注释把此次上调归因于「workspace file byte-cursor paging after merging the workspace pairing approval SDK surface」,这暗示它在分支的某个中间状态下确有必要 —— 但在当前 head 上,本 PR 只增加了 101 字节,仍有 852 字节余量。考虑到 main 也刚在 #8024 中调整过这个常量,我不倾向于放宽一个并未真正触及的门禁:建议撤掉 5b514150cb,把预算保持在 177 KB。 如果 CI 在其他 runner 上结论不同,我很乐意被纠正 —— 那反而是值得知道的信息,因为那意味着该产物对平台敏感。

结论

第 1 轮我提出的四项诉求全部得到满足,唯一的新增代码提交是一个真实的缺陷修复,我已确认它确实起作用、范围恰当、并且有测试守护。 本 head 的正确性与第 1 轮一致 —— 没有发现返回错误字节、分页途中丢失内容、或让伪造游标越出文件的问题;与 merge-base 的差异依然以放宽为主,而唯一收紧的那一项现已在 PR 描述中声明。

从我这边看可以合入。 体积预算的上调是唯一遗留项,且只是一行回退,并非阻塞项 —— 无论如何都可以合并,但我更希望这个门禁保持在 177 KB。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough re-verification. I am keeping the 178 KiB budget.

The 180,396-byte head-only build is reproducible here, but the failure that prompted 5b51415 was on GitHub's required merge ref, not the head-only tree. SDK Java run 30473276861 checked out c4806b7, which merged 524ef96 into the then-current base 97aaa38, and its clean Ubuntu/Node 22.23.1 prepare build produced 181,252 bytes against the 181,248-byte limit. That exact four-byte overflow is recorded in job 90648562382. The 856-byte difference from 180,396 matches the discrepancy under discussion: the merge ref included newer base SDK surface that the head-vs-merge-base build did not.

Because required CI validates PR integration with the current base, reverting to 177 KiB would risk restoring a demonstrated deterministic merge-ref failure. This is also explicitly non-blocking and the PR is beyond roughly five review rounds, so no further churn is warranted.

@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

pull Bot pushed a commit to Little-Star888/qwen-code that referenced this pull request Jul 30, 2026
…wenLM#8043)

* fix(autofix): post the takeover engage ack from the command itself

The engage ack rode a pull_request:labeled round-trip: takeover-command
applies the label, the labeled event routes, and the takeover-ack job
posts the confirmation. That event has now been observed to simply not
fire twice in one day (QwenLM#7999 — the author read the silence as failure
and removed the label; QwenLM#8002 — an engaged fork PR with no ack for
hours), and fork label events can never ack at all since they carry no
secrets: a fork /takeover stayed silent until the next scan picked the
PR up (2h41m on QwenLM#7993).

takeover-command now posts the engage ack directly after applying the
label — every admission gate has already passed at that point, so
'engaged' is truthful for in-repo and fork PRs alike; the fork variant
adds the expectation that the first round comes from the next scheduled
scan. The route side suppresses the label-path ack when the label
sender is the bot (only the ack: the immediate scan still routes), and
the review-scan's existing first-pickup ack dedups against the
command's comment and heals it if the post failed.

Two more silent paths become audible while here: a /takeover on a
stacked (non-main-base) PR now refuses out loud instead of dropping
with only a log line, and a /takeover stop on a non-main PR now
proceeds to remove the label instead of leaving it stuck.

* fix(autofix): ack command-driven releases directly and key the scan grace on the label actor

Review follow-up: the engage-side fix left the release side on the
fragile round-trip — a loud add next to a mute stop re-creates the
exact 'did it work or did the event get lost?' ambiguity this PR set
out to remove, now on release. And a fork or non-main release could
never ack at all (fork unlabeled events carry no secrets; the route
ignores non-main releases).

- takeover-command now posts the release ack directly after removing
  the label, mirroring the engage side, with the same three variants as
  the ack job (plain release / bot-authored / bot-authored+skip) chosen
  from the same PR_INFO the gates used. The route suppresses the
  unlabeled-path ack when the label sender is the bot.
- The scan's first-pickup grace is keyed on the label event's actor:
  a bot-applied label defers only 45s (the command's own write is
  seconds behind the label — fork or in-repo alike), so the immediate
  scan still heals a failed command ack instead of slipping to the next
  scheduled tick, and an ic.json snapshot taken between the label write
  and the command ack cannot double-post. A human in-repo label keeps
  the 3-minute ack-job grace; a human fork still posts right away.
- The fork note now says 'usually within minutes', matching the
  */10 cron instead of contradicting it by 6x.
- Tests kill the two surviving mutations from review: the EN/ZH
  fork-note swap (per-language-half assertions) and the warning
  fallback downgraded to '|| true' (both fallback strings pinned).

* test(autofix): behaviorally pin the LAST_LABELED_BY jq extraction (QwenLM#8043)

* fix(autofix): correct stale comments about scan heal timing and command blast radius (QwenLM#8043)

* test(autofix): pin release-ack body identity across the command and ack jobs (QwenLM#8043)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen Code Autofix <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review triage (round 3) — no code change

Head aa32ed13a6. No inline review comments this round; all feedback is
issue-level. After triage, no code change is warranted, so the branch is
left as-is.

Finding triaged

@wenshao — revert 5b514150cb, keep the SDK browser-bundle budget at
177 KB
(packages/sdk-typescript/scripts/build.js:80).

Disposition: declined (no change), agreeing with the PR author's recorded
decision. Reasons:

  • The finding is explicitly non-blocking — "@wenshao: land it either way … I'd
    prefer the guard stay at 177 KB," and "happy to be shown otherwise."
  • The author (@doudouOUC) has already shown otherwise, on the thread: the bump
    was prompted by a failure on GitHub's required merge ref, not the
    head-only tree. SDK Java run 30473276861 (job 90648562382) merged
    524ef96a74 into the then-current base and produced 181,252 bytes against
    the 181,248-byte (177 KB) limit
    — a deterministic four-byte overflow. The
    head-only 180,396-byte build @wenshao measured differs by exactly the newer
    base SDK surface that the merge ref includes.
  • Reverting to 177 KB would therefore risk restoring a demonstrated
    deterministic merge-ref CI failure, since required CI validates PR
    integration with the current base.
  • The PR is past roughly five review rounds, so per the repo's review policy
    only Critical fixes land at this stage; this is a non-blocking guard
    adjustment, not a correctness, security, build, or regression issue.

No other actionable items: @wenshao's four round-1 asks are confirmed
satisfied and the sawCrlf fix is confirmed load-bearing and tested; the
ci-bot comment is a maintainer scope sign-off gate (not code feedback) and
notes @wenshao has already approved.

中文说明

Autofix 审查分诊(第 3 轮)—— 无代码变更

Head aa32ed13a6。本轮没有行内审查评论,全部反馈均为 issue 级别。经分诊,无需任何代码变更,分支保持原样。

已分诊的发现

@wenshao —— 撤掉 5b514150cb,把 SDK 浏览器包体积预算保持在 177 KB(packages/sdk-typescript/scripts/build.js:80)。

处置:拒绝(不变更),与 PR 作者已记录的决定一致。理由:

  • 该发现明确是非阻塞的 —— “@wenshao:无论如何都可以合并……我更希望门禁保持在 177 KB”,并且“很乐意被纠正”。
  • 作者(@doudouOUC)已在该主题中给出了反证:此次上调是由 GitHub 必需合并引用(required merge ref) 上的一次失败促发的,而非仅 head 树。SDK Java 运行 30473276861(job 90648562382)将 524ef96a74 合入当时的 base,构建产物为 181,252 字节,超出 181,248 字节(177 KB)上限 —— 一次确定性的 4 字节溢出。@wenshao 测得的仅 head 构建 180,396 字节,与之相差的正是合并引用所包含的、更新的 base SDK 接口面。
  • 因此,回退到 177 KB 会有恢复一个已被证实的确定性合并引用 CI 失败的风险,因为必需 CI 验证的是 PR 与当前 base 的集成。
  • 该 PR 已经历约五轮审查,按仓库的审查策略,此阶段只合入 Critical 修复;而这是一项非阻塞的门禁调整,并非正确性、安全性、构建或回归问题。

没有其他可操作项:@wenshao 第 1 轮的四项诉求已确认全部满足,sawCrlf 修复已确认确实起作用且有测试守护;ci-bot 的评论是维护者对范围的签核门禁(非代码反馈),并指出 @wenshao 已经批准。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

kagura-chen pushed a commit to kagura-chen/qwen-code that referenced this pull request Jul 30, 2026
* feat(autofix): back off scan inspection of idle candidates

The scheduled scan inspects every candidate every tick. The takeover
pool doubled in two days (28 open takeover PRs, 8 of them idle in
'nothing new' state for 10+ hours), and idle candidates crowd the two
SHARED budgets: MAX_CANDIDATE_INSPECTIONS (60) and the 10-target cap.
Observed on QwenLM#8002: freshly engaged, admitted by the 09:03 scan, then
deferred by the target budget while long-idle PRs re-confirmed their
idleness yet again.

Candidates whose list-provided updatedAt (no extra API call) is older
than 24h are now inspected on roughly every 4th scan, on a
deterministic slot keyed by PR number and UTC hour so no PR waits
forever. The skip is free — it sits with the busy skip before the
inspection-budget increment.

Safe by construction: every real wake-up bumps updatedAt (reviews,
comments, labels, pushes) or routes in real time anyway, so the only
thing deferred is the scheduled re-confirmation of idleness plus
worst-case a few hours of base-conflict-detection latency for a PR
nobody touched in a day. The forced-dispatch path never builds the
list files, so a forced PR is always inspected.

* feat(autofix): idle-backoff review follow-ups

- Corrected the comment's cost model: idle candidates hit 'continue'
  before the TARGETS append, so they never contend for the 10-target
  cap — the real win is the shared inspection budget plus the serial
  scan-walk latency (the walk is what delayed QwenLM#8002's pickup by ~6
  minutes), and the comment now says exactly that.
- Slot quantum changed from the hour to the scan tick (600s, the same
  quantum as ROT_OFF): an hourly slot against the */10 cron meant 6
  back-to-back inspections then a ~3h blind window per PR — same 25%
  average, terrible shape. The gap is now bounded at ~30 minutes, which
  is what the operator-facing strings promise ('gap ≤30m').
- The two scan-only signals updatedAt cannot see (a base conflict
  appearing when main moves; still-red checks awaiting the redcheck
  marker) are named in the comment instead of papered over.
- The per-candidate jq fork became a single precomputed set + a bash
  substring test, matching the busy skip's idiom and the 'free' claim.
- Tests: the skip predicate and set builder got a behavioral replay
  (idle+out-of-slot defers, idle+in-slot inspects, fresh inspects,
  missing-from-lookup inspects); the two byte-distance assertions
  became a loop-head slice (comment growth cannot red-light CI, and
  budget-consuming code between the skips and the increment fails);
  the --json field pin is order-independent; the 3600 quantum is
  pinned OUT.

* test(autofix): pin the null-updatedAt defensive guard in idle-backoff replay (QwenLM#8049)

* fix(autofix): extract idle-backoff predicate from workflow, fix gap bound 30→40m (QwenLM#8049)

* test(autofix): pin takeover-prs.json in idle-backoff replay (QwenLM#8049)

* fix(autofix): state idle-backoff gap probabilistically, unpin quantum (QwenLM#8049)

Round-2 verification showed the scheduled scan lands every ~40-70 min on
this repo, not every 10 min, so the (epoch/600)%4 slot is an independent
~25% draw per scan, not a deterministic 1-in-4 rotation. The gap is
geometric (measured median ~2h, p90 ~6h), not bounded at ~40m.

Reword the operator-facing strings (skip echo, fleet row) and the workflow
comment to state the behaviour probabilistically and drop the false
"bounded" / "no PR is unlucky forever" claims; correct the misattributed
QwenLM#8002 latency (queue/startup, not the serial walk). Relax the tests so a
truthful number is not a CI failure: pin the mod-4 time-quantum shape
instead of the exact 600s constant, and drop the /3600 exclusion that
forbade the better-tailed quantum. Mechanism logic is unchanged.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <[email protected]>
@wenshao

wenshao commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@yiliang114 yiliang114 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 ✅

@wenshao
wenshao dismissed a stale review July 30, 2026 12:06

fixed

@wenshao
wenshao added this pull request to the merge queue Jul 30, 2026
Merged via the queue into QwenLM:main with commit f3ad4fc Jul 30, 2026
131 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants