Skip to content

feat(core): bound image reads for reliable zoom - #7911

Merged
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/image-zoom-canonical-overview
Jul 29, 2026
Merged

feat(core): bound image reads for reliable zoom#7911
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/image-zoom-canonical-overview

Conversation

@qqqys

@qqqys qqqys commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Static PNG, JPEG, and WebP reads now return a canonical JPEG overview together with its oriented source dimensions and a short hint describing how to request a normalized zoom. The overview is auto-oriented, flattened onto white, and constrained by the same edge, visual-patch, source-byte, and output-byte budgets as image zooms. Small overviews are not upscaled, while zoomed crops may still be magnified within the shared budget.

The shared rendering path keeps direct single-file reads and zoomed crops aligned. Batch and directory reads preserve the overview text immediately before the image. GIF, BMP, audio, video, SVG, and PDF handling retain their existing byte and size-limit behavior.

The downstream paths reviewed for the new two-part image result include direct reads, batch reads, workspace path reads, interactive @ attachments, pasted temporary images, vision-bridge preservation, request token estimation, Gemini/Anthropic/OpenAI-compatible conversion, file-read telemetry, and compaction.

Why it's needed

Large source images can exceed the existing inline-media limit or be downscaled differently by providers, which can hide labels and other small details before the model sees them. Returning one deterministic bounded overview reduces first-request payload size, gives every provider the same coordinate frame, and tells the model how to reopen a full-resolution region through the zoom tool introduced in #7809.

Reviewer Test Plan

How to verify

  1. Read small and large static PNG, JPEG, and WebP files. Expect overview text followed immediately by a JPEG image; small images keep their oriented dimensions, while large images stay within the documented edge, patch, source, and output limits.
  2. Read an EXIF-rotated or transparent image. Expect reported dimensions to match the oriented image and transparent pixels to render on white.
  3. Request a normalized zoom from an overview. Expect the crop to map to the same full-resolution oriented coordinate space and remain within the shared output budget.
  4. Read GIF, BMP, supported audio/video, SVG, and PDF inputs. Expect their existing behavior and limits to remain unchanged.
  5. Exercise direct reads, @ attachments, batch reads, a text-only vision bridge, and image-capable provider conversion. Expect overview text to remain text, image bytes to remain image parts, and no raw base64 to appear as model-visible text.

Evidence (Before & After)

Before After
Static raster reads forwarded original bytes and rejected sources above the legacy inline-media limit. Provider-side resizing could discard small details without giving the model a stable zoom frame. Static PNG/JPEG/WebP reads return a bounded, oriented JPEG overview plus explicit normalized zoom guidance. Legacy media paths are unchanged.

Automated evidence after rebasing onto the latest upstream main: 1,084 focused Core tests passed across image rendering, zoom, direct and batch reads, workspace paths, registration, and scheduling. Full repository build, typecheck, and bundle also passed.

Tested on

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

Environment (optional)

macOS, Node.js 22-compatible workspace, native Sharp renderer, sandbox disabled for local build and tests.

Risk & Scope

  • Main risk or tradeoff: Static PNG/JPEG/WebP reads now decode and re-encode the source as JPEG, so exact original pixels and alpha are intentionally replaced by a bounded white-background overview.
  • Not validated / out of scope: Live answer-quality and latency comparison across the primary Qwen vision model and an OpenAI-compatible vision provider; pathless images that exist only in tool or MCP results; animated image, video-frame, PDF-page, remote-URL, and provider-specific budget support.
  • Breaking changes / migration notes: No tool-schema, configuration, provider, session-state, or migration changes. Existing zoom coordinates remain normalized from 0 to 1000.

Linked Issues

Follow-up to #7809.

中文说明

本 PR 做了什么

静态 PNG、JPEG 和 WebP 读取现在会返回统一的 JPEG 概览、按 EXIF 方向处理后的原图尺寸,以及如何使用归一化坐标请求局部放大的简短提示。概览会自动旋转、将透明背景铺成白色,并与图片放大共用最长边、视觉 patch、源文件字节和输出字节预算。小图不会被放大,局部裁剪仍可在统一预算内放大。

统一渲染路径保证直接读取与局部放大使用相同坐标空间。批量和目录读取会保持概览文字紧邻图片之前。GIF、BMP、音频、视频、SVG 和 PDF 继续沿用现有字节与大小限制行为。

本次复查的新双 Part 图片结果下游包括直接读取、批量读取、工作区路径读取、交互式 @ 附件、粘贴到临时目录的图片、视觉桥接保留、请求 token 估算、Gemini/Anthropic/OpenAI-compatible 转换、文件读取遥测和上下文压缩。

为什么需要

大尺寸源图可能超过现有内联媒体限制,也可能被不同 provider 以不同方式缩小,导致标签等小细节在模型看到之前丢失。返回确定且有界的概览可以降低首次请求负载、为所有 provider 提供一致坐标框架,并引导模型通过 #7809 引入的放大工具重新读取全分辨率局部。

Reviewer Test Plan

如何验证

  1. 读取小型和大型静态 PNG、JPEG、WebP。预期先出现概览文字,随后紧邻 JPEG 图片;小图保持按方向处理后的尺寸,大图满足最长边、patch、源文件和输出限制。
  2. 读取带 EXIF 方向或透明背景的图片。预期文字尺寸与旋转后的图片一致,透明像素显示为白色。
  3. 根据概览请求归一化局部放大。预期裁剪映射到同一套全分辨率方向坐标,并满足统一输出预算。
  4. 读取 GIF、BMP、受支持的音视频、SVG 和 PDF。预期保持现有行为和大小限制。
  5. 覆盖直接读取、@ 附件、批量读取、文本模型视觉桥接以及支持图片的 provider 转换。预期概览仍为文本、图片字节仍为图片 Part,原始 base64 不会作为模型可见文本泄漏。

证据(前后对比)

之前 之后
静态栅格图片读取直接传递原始字节,超过旧内联媒体限制的源图会被拒绝;provider 侧缩放可能丢失小细节,也没有稳定的放大坐标框架。 静态 PNG/JPEG/WebP 返回有界、按方向处理的 JPEG 概览和明确的归一化放大提示;旧媒体路径保持不变。

重放到最新 upstream main 后的自动化证据:图片渲染、放大、直接与批量读取、工作区路径、注册和调度共 1,084 个 Core 测试通过;全仓 build、typecheck 和 bundle 也全部通过。

测试平台

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

环境(可选)

macOS、兼容 Node.js 22 的工作区、原生 Sharp 渲染器;本地构建和测试未启用 sandbox。

风险与范围

  • 主要风险或权衡:静态 PNG/JPEG/WebP 现在会解码并重新编码为 JPEG,因此原始精确像素与透明通道会按设计替换为有界白底概览。
  • 未验证或范围外:主 Qwen 视觉模型与一个 OpenAI-compatible 视觉 provider 的真实回答质量和延迟对比;仅存在于工具或 MCP 结果中的无路径图片;动画图片、视频帧、PDF 页面、远程 URL 和 provider 专属预算支持。
  • 破坏性变更或迁移说明:没有工具 schema、配置、provider、会话状态或迁移变更;现有放大坐标继续使用 0 到 1000 的归一化范围。

关联项

#7809 的后续阶段。

@qqqys
qqqys marked this pull request as ready for review July 28, 2026 07:19
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Re-run after five autofix rounds and maintainer verification.

Template looks good ✓

Problem: this is a feature follow-up to #7809 (zoom tool), not a bug fix. The motivation is observed: large source images exceed the legacy inline-media limit or get downscaled inconsistently by different providers, hiding small details before the model sees them. Returning one deterministic bounded overview with zoom guidance is a natural next step.

Direction: aligned — reliable image ingestion is core to the coding agent experience, and this builds directly on the already-merged zoom tool. No CHANGELOG reference needed for an in-progress feature arc.

Size: 642 production lines (image-view.ts 314 new, zoom-image.ts 216 refactored, fileUtils.ts 112) + 550 test lines across 5 test files. Above the 500-line awareness threshold for core paths — flagged here for the record. Maintainer @wenshao has reviewed, verified locally, and approved.

Approach: the scope feels right. Extracting the shared rendering path into image-view.ts keeps overview and zoom aligned on the same coordinate frame and budget constants. The graceful fallback (non-size render failures fall through to legacy inline-bytes) preserves backward compatibility without adding a config switch. No unrelated changes or drive-by refactors in the diff.

Risk: no elevated risk signals — none of the changed files match the high-risk path patterns.

Moving on to code review. 🔍

中文说明

感谢贡献!这是经过五轮 autofix 和维护者验证后的重新审查。

模板完整 ✓

问题:这是 #7809(放大工具)的功能后续,不是 bug 修复。动机来自实际观察:大尺寸源图超过旧内联媒体限制,或被不同 provider 以不同方式缩小,导致小细节丢失。返回确定且有界的概览是自然的下一步。

方向:对齐——可靠的图片处理是编码代理体验的核心,且直接基于已合并的放大工具。

规模:642 行生产代码 + 550 行测试代码。超过核心路径 500 行关注阈值——在此记录。维护者 @wenshao 已审查、本地验证并批准。

方案:范围合理。提取共享渲染路径到 image-view.ts 使概览和放大使用相同坐标框架和预算常量。非尺寸渲染失败时优雅回退到旧的内联字节路径,保持向后兼容。diff 中无无关改动。

风险:无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 9351160d4d50e30174ed8081577454b7bb632d65 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal: I would extract the shared rendering constants and pipeline from zoom-image.ts into a reusable module, then hook it into processSingleFileContent for canonical image types (PNG/JPEG/WebP), returning a two-part result (overview text + bounded JPEG). Non-canonical formats stay on the legacy path. Errors from the renderer fall back to forwarding raw bytes rather than failing the read.

Comparison with the diff: the PR matches this approach closely. The extraction into image-view.ts is clean — prepareImage handles validation (stat, size, format, animation), renderImageView handles the sharp pipeline, and the two public entry points (renderImageOverview, renderNormalizedImageCrop) share the same budget constants and output format. The zoom tool now delegates entirely to renderNormalizedImageCrop, removing ~180 lines of duplicated logic.

No critical blockers found. A few observations:

  • prepareImage reads the file into a buffer before passing to sharp (the original zoom-image.ts passed the file path directly). This enables abort-signal support on the read and lets the same buffer serve both metadata and render passes. On the fallback path in fileUtils.ts, the file is read a second time — a minor inefficiency, not a bug.
  • The error-code mapping in zoom-image.ts (ImageViewErrorToolErrorType) is straightforward and preserves the original error semantics.
  • Tests now use real sharp-generated images instead of fake byte buffers — a meaningful improvement in test fidelity. Coverage spans small/large images, EXIF orientation, transparency flattening, corrupt/animated inputs, non-canonical formats, audio passthrough, and the zoom crop path.

CI test evidence

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
precheck-pr / precheck ✅ success
review-pr ✅ success

Ubuntu test suite passed. macOS and Windows tests were skipped (infra configuration, not failures). No red checks.

Sandboxed verification is unavailable for this fork PR (the author lacks write access). Maintainer @wenshao has verified locally with real builds and image reads (see comments below) and triggered @qwen-code /verify for isolated confirmation.

中文说明

代码审查

独立方案: 我会将共享渲染常量和管线从 zoom-image.ts 提取到可复用模块,然后在 processSingleFileContent 中为标准图片类型(PNG/JPEG/WebP)接入,返回双 Part 结果(概览文本 + 有界 JPEG)。非标准格式保持旧路径。渲染器错误回退到转发原始字节。

与 diff 对比: PR 与此方案高度一致。image-view.ts 的提取干净——prepareImage 处理验证,renderImageView 处理 sharp 管线,两个公共入口共享预算常量和输出格式。放大工具完全委托给 renderNormalizedImageCrop,移除约 180 行重复逻辑。

未发现关键阻塞问题。测试现在使用真实 sharp 生成的图片而非伪造字节缓冲区,测试保真度显著提升。

CI 测试证据

Ubuntu 测试套件通过。macOS 和 Windows 测试被跳过(基础设施配置,非失败)。无红色检查。

沙箱验证对此 fork PR 不可用(作者无写权限)。维护者 @wenshao 已使用真实构建和图片读取进行本地验证,并触发了 @qwen-code /verify 进行隔离确认。

Qwen Code · qwen3.8-max-preview

Reviewed at 9351160d4d50e30174ed8081577454b7bb632d65 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean extraction, comprehensive tests, green CI, maintainer-verified.

This is what a good follow-up PR looks like. The zoom tool landed in #7809 with its own rendering pipeline; this PR extracts that pipeline into a shared module and hooks it into the file-read path so every static PNG/JPEG/WebP read returns a bounded, oriented JPEG overview with zoom guidance — one deterministic coordinate frame for every provider. The fallback for non-renderable inputs (corrupt, animated, sharp missing) preserves the legacy forward-verbatim behavior, so nothing regresses. The tests are the strongest part: real sharp-generated images, EXIF rotation, transparency flattening, budget assertions, and format-passthrough checks. Five autofix rounds addressed earlier review feedback, and the maintainer verified the real product locally with before/after evidence.

No reservations. Shipping this.

中文说明

置信度:5/5 —— 干净的提取、全面的测试、绿色 CI、维护者已验证。

这是一个优秀的后续 PR。放大工具在 #7809 中落地时自带渲染管线;本 PR 将该管线提取为共享模块并接入文件读取路径,使每个静态 PNG/JPEG/WebP 读取都返回有界、按方向处理的 JPEG 概览和放大引导——为所有 provider 提供统一的坐标框架。不可渲染输入(损坏、动画、sharp 缺失)的回退保留了旧的转发原始字节行为,无回归。测试是最强的部分:真实 sharp 生成图片、EXIF 旋转、透明铺白、预算断言和格式透传检查。五轮 autofix 处理了早期评审反馈,维护者已使用真实产品进行本地 before/after 验证。

无保留意见。准备合并。

Qwen Code · qwen3.8-max-preview

Reviewed at 9351160d4d50e30174ed8081577454b7bb632d65 · re-run with @qwen-code /triage

@yiliang114

yiliang114 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

审查完成,但 GitHub 发布操作被取消,评论尚未发出。

结论:REQUEST_CHANGESimage-view.ts:166stat → readFile 存在竞态,可在完整读入后才发现超过 100 MB,导致内存上限失效甚至 OOM。其余主流程、测试覆盖和 Ubuntu CI 均正常。

现有机器人所称“未处理异常”不准确:外层 catch 会转换为 READ_CONTENT_FAILURE,批量读取也会展示该错误。


✅ completed

Comment thread packages/core/src/utils/fileUtils.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

1 similar comment
@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@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 28, 2026
Comment thread packages/core/src/utils/fileUtils.ts Outdated
@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 28, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 6 finishedview run. See this round's report below.

中文说明

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

@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 Round Summary

Feedback triage

Finding Reviewer Classification Decision
rc:3663782926 @qwen-code-ci-bot Critical (severity inaccurate — outer catch at fileUtils.ts:1863 already converts re-thrown errors to READ_CONTENT_FAILURE) Implemented — simplified version
rc:3666055649 @doudouOUC Critical (same issue, re-verified; severity also inaccurate per maintainer @yiliang114) Implemented — adopted this simpler suggestion

What changed

Both findings identified that the catch block around renderImageOverview in processSingleFileContent only handled source_too_large and output_too_large ImageViewError codes, letting other codes (e.g. unsupported_image, animated_image, decode_failed, renderer_unavailable) fall through to the generic outer catch. While the outer catch at line 1863 does convert these to READ_CONTENT_FAILURE error results (so they are NOT truly "unhandled exceptions" as claimed), the inner catch now handles all ImageViewError codes directly for:

  • Cleaner error messages: the ImageViewError's own message (e.g. "Unsupported image. Expected a static PNG, JPEG, or WebP file") instead of the outer catch's wrapped format ("Error reading file path: ...")
  • Intentional error handling: explicit rather than accidental fallthrough
  • Consistency with zoom-image.ts: which already maps all ImageViewError codes

The fix adopts @doudouOUC's simpler suggestion: catch all ImageViewError instances, map size codes to FILE_TOO_LARGE, everything else to READ_CONTENT_FAILURE. The more elaborate 9-way switch from rc:3663782926 was not adopted — file_not_found, target_is_directory, and target_not_regular_file are pre-checked by processSingleFileContent before the image overview path, so mapping them to distinct ToolErrorType values in this catch would be defense for cases that cannot reach it.

A regression test was added: a corrupt .png file (text content) now verifies READ_CONTENT_FAILURE is returned.

Conflict notes

No conflicts (--conflict false).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest run src/utils/fileUtils.test.ts (packages/core) — 169 passed
  • vitest run src/utils/image-view.test.ts src/utils/readManyFiles.test.ts src/utils/pathReader.test.ts src/tools/read-file.test.ts (packages/core) — 137 passed
中文说明

审查轮次总结

反馈分类

发现 审查者 分类 决定
rc:3663782926 @qwen-code-ci-bot Critical(严重性不准确 — fileUtils.ts:1863 的外层 catch 已将重新抛出的错误转换为 READ_CONTENT_FAILURE 已实现 — 采用简化版本
rc:3666055649 @doudouOUC Critical(同一问题,已重新验证;维护者 @yiliang114 确认严重性同样不准确) 已实现 — 采纳了此更简洁的建议

变更内容

两个发现都指出 processSingleFileContentrenderImageOverview 周围的 catch 块仅处理了 source_too_largeoutput_too_large 两种 ImageViewError 错误码,其他错误码(如 unsupported_imageanimated_imagedecode_failedrenderer_unavailable)会落入通用外层 catch。虽然 1863 行的外层 catch 确实会将这些错误转换为 READ_CONTENT_FAILURE 错误结果(因此并非如所称的"未处理异常"),但内部 catch 现在直接处理所有 ImageViewError 错误码,带来以下改进:

  • 更清晰的错误消息:使用 ImageViewError 自身的消息(如"Unsupported image. Expected a static PNG, JPEG, or WebP file"),而非外层 catch 的包装格式("Error reading file path: ...")
  • 有意识的错误处理:显式处理而非意外落入
  • zoom-image.ts 保持一致:该文件已映射所有 ImageViewError 错误码

修复采纳了 @doudouOUC 更简洁的建议:捕获所有 ImageViewError 实例,将尺寸相关错误码映射为 FILE_TOO_LARGE,其余映射为 READ_CONTENT_FAILURE。rc:3663782926 中更复杂的 9 路 switch 未被采纳 — file_not_foundtarget_is_directorytarget_not_regular_fileprocessSingleFileContent 进入图片概览路径之前已被前置检查处理,因此在此 catch 中将它们映射为不同的 ToolErrorType 值属于对不可能到达的情况的防御。

新增了回归测试:损坏的 .png 文件(文本内容)现在验证返回 READ_CONTENT_FAILURE

冲突说明

无冲突(--conflict false)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest run src/utils/fileUtils.test.ts(packages/core)— 169 个通过
  • vitest run src/utils/image-view.test.ts src/utils/readManyFiles.test.ts src/utils/pathReader.test.ts src/tools/read-file.test.ts(packages/core)— 137 个通过

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

Reviewed — no blockers. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

中文说明

已审查——无阻断问题。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/utils/readManyFiles.test.ts Outdated
Comment thread packages/core/src/utils/image-view.ts
@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 轮)。改动内容与我反驳保留之处如下:

Review Feedback Summary — PR #7911

Feedback points

1. [rc:3666858578] Misleading test name in readManyFiles.test.tsResolved (previous commit, re-verified)

Finding: The test name said "preserves unsupported images" but the test creates a valid PNG (a canonical image type) and verifies overview pipeline rendering — the opposite of "unsupported."

Status: Already renamed to 'renders canonical images through the overview pipeline even when the bridge handoff flag is set' in commit 54bc8f9. Re-verified the rename is in place and the test passes.

2. [rc:3666858583] Wrong error code for corrupt canonical images in image-view.tsResolved (previous commit + follow-up fix)

Finding: When sharp().metadata() fails on a corrupt canonical image, the error was reported as unsupported_image ("Unsupported image"). Since the MIME type was already confirmed canonical, the real problem is corruption, not format incompatibility. The decode_failed code existed but was only used in renderImageView.

Status: The previous commit (54bc8f9) changed the metadata catch block to use decode_failed. However, the new message ("Failed to decode image metadata (file may be corrupt)") broke zoom-image.test.ts, which asserts the error mentions /PNG, JPEG, or WebP/i — because prepareImage cannot distinguish "corrupt image" from "not an image at all" (sharp throws in both cases). The follow-up commit updates the message to "Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP)", which is accurate for both cases and satisfies the existing test assertion.

Conflict notes

No conflicts (--conflict false).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/core src/utils/image-view.test.ts — 4 passed
  • vitest packages/core src/tools/zoom-image.test.ts — 19 passed
  • vitest packages/core src/utils/readManyFiles.test.ts — 30 passed (included in batch)
  • vitest packages/core src/utils/fileUtils.test.ts — 169 passed (included in batch)
  • vitest packages/core src/tools/read-file.test.ts — 87 passed (included in batch)
  • vitest packages/core src/utils/pathReader.test.ts — 13 passed (included in batch)
中文说明

审查反馈总结 — PR #7911

反馈要点

1. [rc:3666858578] readManyFiles.test.ts 中测试名称具有误导性 — 已解决(前次提交,已重新验证)

发现: 测试名称写的是 "preserves unsupported images"(保留不支持的图片),但测试实际创建的是有效 PNG(标准图片类型),验证的是概览管线渲染——与 "不支持" 完全相反。

状态: 已在提交 54bc8f9 中重命名为 'renders canonical images through the overview pipeline even when the bridge handoff flag is set'(即使设置了 bridge handoff 标志,标准图片仍通过概览管线渲染)。已重新确认重命名到位且测试通过。

2. [rc:3666858583] image-view.ts 中损坏标准图片的错误码不正确 — 已解决(前次提交 + 后续修复)

发现:sharp().metadata() 在损坏的标准格式图片上失败时,错误被报告为 unsupported_image("Unsupported image")。由于 MIME 类型已确认为标准格式,真正的问题是损坏,而非格式不兼容。decode_failed 错误码已存在但仅在 renderImageView 中使用。

状态: 前次提交(54bc8f95e)将元数据 catch 块改为使用 decode_failed。但新消息("Failed to decode image metadata (file may be corrupt)")导致 zoom-image.test.ts 失败,因为该测试断言错误信息包含 /PNG, JPEG, or WebP/i——而 prepareImage 无法区分 "图片损坏" 和 "根本不是图片"(两种情况 sharp 都会抛出异常)。后续提交将消息更新为 "Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP)"(无法解码图片,文件可能已损坏或不是有效的 PNG、JPEG 或 WebP),对两种情况均准确,并满足现有测试断言。

冲突说明

无冲突(--conflict false)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/core src/utils/image-view.test.ts — 4 项通过
  • vitest packages/core src/tools/zoom-image.test.ts — 19 项通过
  • vitest packages/core src/utils/readManyFiles.test.ts — 30 项通过(批量运行)
  • vitest packages/core src/utils/fileUtils.test.ts — 169 项通过(批量运行)
  • vitest packages/core src/tools/read-file.test.ts — 87 项通过(批量运行)
  • vitest packages/core src/utils/pathReader.test.ts — 13 项通过(批量运行)

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

Reviewed. Suggestions are inline. Not reviewed: invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts, invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts, invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts — the agent returned no evidence of its walk twice. Not reviewed: the invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts, the invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts, the invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

中文说明

已审查。 建议见行内评论。 未审查:invariant check (state, timers, collections) on packages/core/src/tools/zoom-image.ts、invariant check (counters, return values, error taxonomies) on packages/core/src/tools/zoom-image.ts、invariant check (config fields, early returns) on packages/core/src/tools/zoom-image.ts——该 agent 连续两次未返回任何检查过程的证据。 未审查:不变量检查(状态、定时器、集合)(packages/core/src/tools/zoom-image.ts)、不变量检查(计数器、返回值、错误分类)(packages/core/src/tools/zoom-image.ts)、不变量检查(配置字段、提前返回)(packages/core/src/tools/zoom-image.ts)——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。 未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/utils/fileUtils.ts Outdated
Comment thread packages/core/src/utils/fileUtils.test.ts
@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Local verification — real build & real image reads, head 974b1c35a5

I built and ran this locally in an isolated worktree (merge-base b3873571aa), driving the real processSingleFileContent / readManyFiles / renderImageOverview over real on-disk images rather than fixtures-in-memory. The feature itself works and is well-tested. I'm blocking on three things that all share one small fix.

Verdict: fix-then-merge. The design is right, the shared budget module is a genuine improvement, and the 11 tests that discriminate against main really do pin it. But this PR moves image decoding onto the universal read path, and three previously-working inputs now hard-fail there — including, in a published build, every PNG/JPEG/WebP.


🔴 B1 — In the shipped npm package, sharp is never installed, so every canonical image read would fail

fig5

sharp is not declared by packages/core or packages/cli. It is in node_modules only as a workspace transitive (@qwen-code/mobile-mcp → mobilewright → @mobilewright/core → sharp@0.34.5), and scripts/prepare-package.js:305-322 publishes dependencies: {} with a fixed optionalDependencies list that omits it. I confirmed this against the actually-published tarball (npm pack @qwen-code/qwen-code@0.21.0): no sharp, no @img/*.

Controlled experiment — one bundle built with the repo's own esbuild options (including inject: scripts/esbuild-shims.js), placed in the published package's directory layout, only variable = whether @img/* is resolvable:

arm @img/* present renderImageOverview("small.png")
published layout (matches npm i -g) no renderer_unavailable
identical layout + @img/* copied in yes OK → 640×400, 3,268 B

On main this gap is bounded — zoom_image is an optional deferred tool, so it just reports itself unavailable (I flagged the same packaging gap during the #7809 review). With this PR the gap lands on read_file / @-attachments / read_many_files, and images stop being readable at all rather than merely un-zoomable.

🔴 B2 — Animated WebP reads now hard-fail

.webp is in CANONICAL_IMAGE_MIME_TYPES, so an animated WebP reaches prepareImage, trips (metadata.pages ?? 1) > 1, and returns READ_CONTENT_FAILURE — "Only static images are supported". On main the bytes were forwarded verbatim. The PR body lists GIF/BMP/audio/video/SVG/PDF as unchanged; animated WebP isn't in that list but is equally affected.

Rejecting animated input in zoom_image is correct — it's opt-in. Rejecting it in read_file removes a capability.

🔴 B3 — Non-canonical content behind a canonical extension now hard-fails

Measured precisely (real renderImageOverview):

file content before after
x.png JPEG bytes forwarded ✅ renders
x.png WebP bytes forwarded ✅ renders
x.jpg PNG bytes forwarded ✅ renders
x.png GIF bytes forwarded READ_CONTENT_FAILURE
x.png TIFF bytes forwarded READ_CONTENT_FAILURE
x.png AVIF bytes forwarded READ_CONTENT_FAILURE

Cross-canonical mismatches are fine; GIF/TIFF/AVIF/BMP behind a .png/.jpg/.webp name are not. A truncated/corrupt PNG flips the same way (that one is arguably an improvement — your call).

One fix covers all three

In the catch around renderImageOverview in fileUtils.ts, keep the FILE_TOO_LARGE mapping for source_too_large / output_too_large, and for every other ImageViewError code fall through to the legacy inline-bytes branch that already sits directly below (it still enforces the 9.9 MB / base64 limits, which is exactly right for a source we could not render). That turns renderer_unavailable, animated_image, unsupported_image and decode_failed back into main's behaviour instead of an error, and leaves the happy path untouched.

Independently of this PR, sharp + the needed @img/* should be declared in scripts/prepare-package.js and staged by build-standalone-release.js / create-standalone-package.js, or the feature never actually reaches users.


What I verified, and what it showed

fig1

16 real fixtures through the real read path, same harness on both arms, only fileUtils.ts / zoom-image.ts swapped. The wins are real: a 17.6 MB and an 83.6 MB screenshot that main rejected outright now return bounded overviews. The legacy paths are genuinely untouched: GIF, TIFF, AVIF/HEIF, SVG and audio come back byte-identical to main.

fig2

The motivating case end to end. main: file_too_large, the model sees no pixels. PR: overview text + a 1389×868 / 220 KB JPEG at 1,550 of 1,568 patches. I also ran the honest counterfactual for the zoom round-trip — cropping the overview the model already holds and upscaling it, versus a real zoom_image call. The overview alone already resolves the layout, the KPI values and the 8.5 px table; only the 6.5 px axis labels genuinely need the round-trip. That is the split your design assumes, and it holds.

fig3

Mutation matrix, 13/20 killed by your own tests. The budget, orientation, flatten, upscale-cap and canonical-set semantics are all tightly pinned — IMAGE_MAX_UPSCALE 8→9 still dies, and boundedSize(…, 1) → IMAGE_MAX_UPSCALE on the overview dies in 6 places. Notes on the survivors:

  • The three 100 MB guards mutually mask each other. Removing the fileUtils pre-check alone, or the image-view stat guard alone, or the post-readFile bytes.length check alone, is invisible; removing all three fails 2 tests. The bytes.length re-check added in the autofix round is the one nothing reaches — and since it runs after fs.readFile has already allocated, it can report the overrun but not prevent it.
  • The 9 MB output guard is unreachable. Worst case at the patch budget, measured on pure noise at 1568×784 q92 4:4:4, is 2.05 MB. Dead branch in both the overview and zoom paths.
  • Quality 92, chroma 4:4:4 and lanczos3 have no assertion — a lanczos3 → nearest swap would visibly alias every overview and no test notices.

fig4

Cost, measured out-of-process with /usr/bin/time -l:

  • IMAGE_MAX_SOURCE_BYTES is not a memory bound. The most expensive read in my whole fixture set is a 0.74 MB file — a 16000×16000 PNG, 357–362 MB peak RSS, ~300 ms — which main never decoded at all. Memory tracks pixels; sharp's limitInputPixels: true (268 Mpx) is the real ceiling. Worth stating explicitly in the risk section, and a pixel budget would bound it far better than the byte cap.
  • sharp(filePath)sharp(Buffer) costs the source size in RSS. Same pipeline, same crop, identical output bytes: 106.7 → 194.7 MB on an 83.6 MB source, i.e. +87 MB, in exchange for ~25% less wall time (one read instead of two). Fine for the overview, which must decode everything anyway; for zoom_image it gives up libvips' demand-driven read for a crop that may touch 1% of the image. Non-blocking, but it is a real trade and it isn't in the PR description.

Other behaviour I checked and would not change:

  • readManyFiles degrades gracefully — a failing image becomes error text inline and its siblings are unaffected; one bad file does not poison a glob.
  • convertToFunctionResponse handles the new two-part shape correctly (text → response.output, image → parts); it's the same path zoom_image has been using since feat(core): add full-resolution image zoom tool #7809. No base64 leaks as model-visible text.
  • read_file is already in COMPACTABLE_TOOLS / FILE_PATH_TOOLS, so microcompaction behaviour is unchanged in class.
  • Extreme aspect ratios collapse: panorama.png 20000×40 → overview 1568×3, using 56 of 1568 patches. The patch budget alone would allow it natively; the 1568 max-edge is what destroys it. Consistent with the existing zoom budget and with provider conventions, so not a regression — just worth knowing that only zoom_image recovers anything from a very wide image.
  • Small flat assets get larger: alpha.webp 270 B → 1,349 B, exif6.jpg 2,382 B → 3,082 B. Immaterial for tokens (patch count is unchanged), but @-attaching a folder of UI icons now sends more bytes.

Gates

gate result
the 6 spec files this PR touches 326 / 326 pass
same specs with base sources overlaid 11 fail — the discriminating set
prettier --check on all 8 changed files pass
eslint on all 8 changed files pass
tsc --noEmit (packages/core) 0 errors in any changed file; the 186 reported errors are all in ide-client.ts / client.test.ts / sessionService*.test.ts and are local toolchain drift (my worktree resolves tsc 6.0.3 vs the lockfile's 5.8.3)
full packages/core suite 17,431 pass / 19 fail — all pre-existing. Run in isolation, the same three files fail more on the merge base than on this PR (16 vs 11), so it's local flake, not the PR
Method

Isolated worktree detached at 974b1c35a5; merge-base b3873571aa. Before/after produced by overlaying only fileUtils.ts / zoom-image.ts from the base while keeping everything else, so both arms run the identical harness. Fixtures generated with sharp + a Playwright-rendered dashboard (6.5 px labels at deviceScaleFactor 3, photographic grain so the PNG is genuinely incompressible — 4800×3000, 17.6 MB). Peak RSS via /usr/bin/time -l, 2–3 runs each; sharp 0.34.5 / libvips 8.17.3, macOS arm64, Node 22.23.1. Every mutant was checked to produce a real diff before its run and reverted after.

The head moved from 6d1e431f2a to 974b1c35a5 while I was working (the decode_failed rename); everything above was re-run at the final head.

中文说明

本地验证 — 真实构建与真实图片读取,head 974b1c35a5

我在隔离 worktree 中本地构建并运行(merge-base b3873571aa),用真实processSingleFileContent / readManyFiles / renderImageOverview 读取磁盘上的真实图片,而不是内存里的假数据。功能本身可用且测试扎实。我阻塞在三点上,而它们共用同一个小修复。

结论:修完再合。 设计方向正确,共享预算模块是实打实的改进,11 个相对 main 有区分度的测试确实钉住了行为。但本 PR 把图片解码搬到了通用读取路径上,三类原本可用的输入在这条路径上变成硬失败——其中在已发布构建里,是所有 PNG/JPEG/WebP。

🔴 B1 — 已发布的 npm 包从不安装 sharp,因此所有标准图片读取都会失败

sharp 并未由 packages/corepackages/cli 声明,它进入 node_modules 只是 workspace 传递依赖(@qwen-code/mobile-mcp → mobilewright → @mobilewright/core → sharp@0.34.5);而 scripts/prepare-package.js:305-322 发布的是 dependencies: {} 加一份不含它的固定 optionalDependencies。我对着真实发布的包核对过(npm pack @qwen-code/qwen-code@0.21.0):没有 sharp,没有 @img/*

对照实验:用仓库自己的 esbuild 选项(含 inject: scripts/esbuild-shims.js)构建同一个 bundle,放进发布包的目录结构,唯一变量是 @img/* 是否可解析——不可解析时 renderer_unavailable,拷进去后正常输出 640×400 / 3,268 B。

main 上这个缺口是有界的:zoom_image 是可选的延迟工具,npm 安装的 CLI 只会报该工具不可用(#7809 复查时我提过同一个打包缺口)。到了本 PR,缺口落在 read_file / @ 附件 / read_many_files 上,图片从「无法放大」变成「完全读不了」。

🔴 B2 — 动图 WebP 读取变成硬失败

.webpCANONICAL_IMAGE_MIME_TYPES 中,动图 WebP 会进入 prepareImage,命中 (metadata.pages ?? 1) > 1,返回 READ_CONTENT_FAILURE — "Only static images are supported"main 上是原样透传。PR 描述把 GIF/BMP/音频/视频/SVG/PDF 列为不变,动图 WebP 不在列表里,却同样受影响。zoom_image 拒绝动图是对的(那是主动调用);read_file 拒绝则是丢掉了一项能力。

🔴 B3 — 扩展名是标准格式、内容不是的文件变成硬失败

实测:.png 里装 JPEG / WebP 字节、.jpg 里装 PNG 字节都正常;.png 里装 GIF / TIFF / AVIF 字节则全部 READ_CONTENT_FAILURE,而 main 会透传。截断损坏的 PNG 同样翻转(这一条算改进还是回退,由你们定)。

一个修复覆盖三点

fileUtils.tsrenderImageOverviewcatch 里,保留 source_too_large / output_too_largeFILE_TOO_LARGE 的映射,其余 ImageViewError 一律落回紧挨其下的原有 inline-bytes 分支(它仍然执行 9.9 MB / base64 限制,对渲染不出来的源正合适)。这样 renderer_unavailableanimated_imageunsupported_imagedecode_failed 都恢复成 main 的行为,正常路径完全不受影响。

与本 PR 相对独立地,sharp 与所需 @img/* 应在 scripts/prepare-package.js 中声明,并由 build-standalone-release.js / create-standalone-package.js 打包,否则这个特性根本到不了用户手里。

验证到了什么

  • Fig 1:16 个真实 fixture 走真实读取路径,两臂同一套 harness,只替换 fileUtils.ts / zoom-image.ts收益是真的main 直接拒绝的 17.6 MB 与 83.6 MB 截图现在返回有界概览。旧路径确实没动:GIF、TIFF、AVIF/HEIF、SVG、音频返回结果与 main 逐字节一致。
  • Fig 2:端到端的目标场景。main 返回 file_too_large,模型看不到任何像素;PR 返回概览文字 + 1389×868 / 220 KB JPEG(1,568 patch 用了 1,550)。我还做了放大往返的诚实对照——把模型已持有的概览裁同一区域再放大,对比真实 zoom_image。概览本身已能读出版式、KPI 数值和 8.5 px 表格,只有 6.5 px 的坐标轴标签确实需要往返。这正是你们设计所假设的分工,结论成立。
  • Fig 3:变异矩阵 13/20 被 PR 自带测试杀死。预算、方向、白底、放大上限、标准格式集合都钉得很紧(IMAGE_MAX_UPSCALE 8→9 仍会被杀)。存活项说明:三处 100 MB 守卫互相遮蔽(单独去掉任意一处都无人察觉,三处全去才失败 2 个测试),其中 autofix 轮次新增的 bytes.length 复查无人触达,且它在 fs.readFile 已分配之后才运行,只能报告不能预防;9 MB 输出守卫不可达(patch 预算下纯噪声实测最大输出仅 2.05 MB);quality 92 / 4:4:4 / lanczos3 无任何断言,把 lanczos3 换成 nearest 会让每张概览肉眼可见地锯齿化而无人发现。
  • Fig 4/usr/bin/time -l 实测成本。IMAGE_MAX_SOURCE_BYTES 不是内存上界——整组 fixture 里最贵的一次读取来自一个 0.74 MB 的文件(16000×16000 PNG,峰值 357–362 MB RSS,约 300 ms),而 main 根本不解码它;内存跟随像素数,真正的天花板是 sharp 自己的 limitInputPixels: true(268 Mpx)。建议在风险段落写明,并考虑加像素预算。sharp(filePath)sharp(Buffer) 的代价是源文件大小的常驻内存:同一条流水线、同一裁剪、输出字节完全一致,83.6 MB 源上从 106.7 MB 涨到 194.7 MB(+87 MB),换来约 25% 的耗时下降(少读一次盘)。概览路径无所谓(本来就要全量解码),但 zoom_image 因此放弃了 libvips 的按需读取,而裁剪可能只碰到 1% 的图像。不阻塞,但这是实打实的取舍,PR 描述里没写。

其他已核对、我不建议改的行为:readManyFiles 降级得体(失败图片就地变成错误文本,不影响同批其他文件);convertToFunctionResponse 正确处理新的两 Part 形态(文本进 response.output,图片进 parts),与 #7809zoom_image 走的是同一条路,没有 base64 以文本形式泄漏;read_file 本就在 COMPACTABLE_TOOLS / FILE_PATH_TOOLS 中,压缩行为类别不变;极端长宽比会塌陷(panorama.png 20000×40 → 概览 1568×3,1568 个 patch 只用了 56 个)——这由 1568 最长边而非 patch 预算造成,与既有 zoom 预算和 provider 惯例一致,不算回退,但很宽的图只有 zoom_image 能救;小的扁平素材反而变大(alpha.webp 270 B → 1,349 B,exif6.jpg 2,382 B → 3,082 B),对 token 无影响(patch 数不变),但 @ 附加一整个图标目录时字节数会上升。

关卡

关卡 结果
本 PR 改动的 6 个 spec 326 / 326 通过
同样的 spec 覆盖 base 源码 11 个失败 —— 有区分度的集合
prettier --check(8 个改动文件) 通过
eslint(8 个改动文件) 通过
tsc --noEmit(packages/core) 改动文件 0 错误;报出的 186 条全在 ide-client.ts / client.test.ts / sessionService*.test.ts,属本地工具链漂移(我的 worktree 解析到 tsc 6.0.3,锁文件是 5.8.3)
packages/core 全量 17,431 通过 / 19 失败,全部既有问题。单独重跑时这三个文件在 merge-base 上失败得更多(16 vs 11),属本地 flake,与本 PR 无关

方法:worktree detach 在 974b1c35a5,merge-base b3873571aa。前后对比只从 base 覆盖 fileUtils.ts / zoom-image.ts,其余保持不变,因此两臂跑的是同一套 harness。fixture 由 sharp 与 Playwright 渲染的 dashboard 生成(deviceScaleFactor 3 下 6.5 px 标签,叠加照片级颗粒使 PNG 真正不可压缩——4800×3000,17.6 MB)。峰值 RSS 用 /usr/bin/time -l,每项 2–3 次;sharp 0.34.5 / libvips 8.17.3,macOS arm64,Node 22.23.1。每个变异体都先确认产生了真实 diff 再运行,运行后还原。验证期间 head 从 6d1e431f2a 变为 974b1c35a5decode_failed 重命名),以上全部在最终 head 上重跑过。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round summary (PR #7911)

Both inline suggestions from the automated reviewer were addressed in code. No
conflict merge was needed (--conflict false).

Feedback points

  • [rc:3667684883] Suggestion — absolute path leaked into llmContent / returnDisplay (packages/core/src/utils/fileUtils.ts)

    • Decision: Implemented.
    • Change: In the image-overview catch (error instanceof ImageViewError) block, derive userMessage = message.replace(: ${filePath}, '') and use it for llmContent and returnDisplay, while error keeps the full message including the absolute path. This matches the function-wide convention used by the 100 MB and 10 MB size-limit returns, where only error carries the path. The renderer_unavailable message has no path suffix, so it is left unchanged.
    • Test: Strengthened the existing "returns READ_CONTENT_FAILURE for a corrupt canonical image" test to assert error contains the path while llmContent and returnDisplay do not.
  • [rc:3667684897] Suggestion — no overview-path test for animated canonical images (packages/core/src/utils/fileUtils.test.ts)

    • Decision: Implemented.
    • Change: Added "returns READ_CONTENT_FAILURE for an animated canonical image", which builds a two-frame animated WebP (mime image/webp, so shouldRenderImageOverview is true) and asserts processSingleFileContent returns errorType: READ_CONTENT_FAILURE with an llmContent matching /static/i. This mirrors the existing zoom-path test ("rejects animated images instead of silently zooming one frame") and guards the pages > 1animated_imageREAD_CONTENT_FAILURE mapping in the read/overview path.

Conflict notes

None — --conflict false, no merge performed.

Verification

  • npx vitest run src/utils/fileUtils.test.ts -t "canonical image" (packages/core) — 3 passed
  • npx vitest run src/utils/fileUtils.test.ts (packages/core) — 170 passed
  • npm run typecheck — passed
  • npx eslint packages/core/src/utils/fileUtils.ts packages/core/src/utils/fileUtils.test.ts — passed (no findings)
  • npm run build — passed
  • npm run lint — passed
中文说明

Autofix 审查轮次总结(PR #7911

自动化审查器提出的两条行内建议均已在代码中处理。无需冲突合并(--conflict false)。

反馈点

  • [rc:3667684883] 建议 —— 绝对路径泄漏到 llmContent / returnDisplaypackages/core/src/utils/fileUtils.ts

    • 决定: 已实现。
    • 改动: 在 image-overview 的 catch (error instanceof ImageViewError) 块中,派生 userMessage = message.replace(: ${filePath}, ''),并将其用于 llmContentreturnDisplay,而 error 保留包含绝对路径的完整消息。这与函数内部的约定一致(100 MB 和 10 MB 大小限制返回也是如此,只有 error 携带路径)。renderer_unavailable 消息没有路径后缀,因此保持不变。
    • 测试: 加强了现有的 "returns READ_CONTENT_FAILURE for a corrupt canonical image" 测试,断言 error 包含路径,而 llmContentreturnDisplay 不包含路径。
  • [rc:3667684897] 建议 —— 概览路径缺少动画标准图片的测试packages/core/src/utils/fileUtils.test.ts

    • 决定: 已实现。
    • 改动: 新增 "returns READ_CONTENT_FAILURE for an animated canonical image" 测试,构造一个双帧动画 WebP(MIME 为 image/webp,因此 shouldRenderImageOverview 为 true),并断言 processSingleFileContent 返回 errorType: READ_CONTENT_FAILURE,且 llmContent 匹配 /static/i。该测试镜像了放大路径已有的测试("rejects animated images instead of silently zooming one frame"),并守护读取/概览路径中 pages > 1animated_imageREAD_CONTENT_FAILURE 的映射。

冲突说明

无 —— --conflict false,未执行合并。

验证

  • npx vitest run src/utils/fileUtils.test.ts -t "canonical image"(packages/core)—— 3 个通过
  • npx vitest run src/utils/fileUtils.test.ts(packages/core)—— 170 个通过
  • npm run typecheck —— 通过
  • npx eslint packages/core/src/utils/fileUtils.ts packages/core/src/utils/fileUtils.test.ts —— 通过(无问题)
  • npm run build —— 通过
  • npm run lint —— 通过

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

[Critical] fileUtils.ts catch block — B1: In the shipped npm package, sharp is never installed (not declared in packages/core or packages/cli, omitted from scripts/prepare-package.js optionalDependencies). Every canonical image read (PNG/JPEG/WebP) would return renderer_unavailable → READ_CONTENT_FAILURE. On main this only affected the optional zoom_image tool; this PR moves it to read_file / @-attachments / read_many_files.

[Critical] fileUtils.ts catch block — B2: Animated WebP reads now hard-fail. .webp is in CANONICAL_IMAGE_MIME_TYPES, so animated WebP reaches prepareImage, trips (metadata.pages ?? 1) > 1, and returns animated_image → READ_CONTENT_FAILURE. On main the bytes were forwarded verbatim. Rejecting animated input in zoom_image is correct (opt-in); rejecting it in read_file removes a capability.

[Critical] fileUtils.ts catch block — B3: Non-canonical content behind a canonical extension (e.g. GIF bytes in a .png file) now hard-fails with decode_failed/unsupported_image → READ_CONTENT_FAILURE. On main the bytes were forwarded verbatim. The suggested fix for all three: fall through to the legacy inline-bytes branch (already at lines 1497-1515) for every non-size ImageViewError code, keeping FILE_TOO_LARGE for source_too_large/output_too_large.

— qwen3.7-max via Qwen Code /review

Comment on lines +180 to +184
} catch {
signal.throwIfAborted();
throw new ImageViewError(
'decode_failed',
`Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`,

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.

[Suggestion] The bare catch {} discards the underlying sharp error and always throws 'decode_failed'. However, sharp(bytes, { limitInputPixels: true }).metadata() can also reject when pixel dimensions exceed the default ~268 Mpx limit — a valid, non-corrupt JPEG that simply has too many pixels. The error code and message then claim the file "may be corrupt or not a static PNG, JPEG, or WebP", which is misleading.

Failure scenario: a 50 MB JPEG at 20000×20000 pixels (400M px) passes the 100 MB byte-size check but sharp rejects on limitInputPixels. The caller receives decode_failed and cannot distinguish this from actual corruption.

Concrete cost: the operator concludes the file is corrupt when it is a valid image exceeding the pixel-dimension ceiling, wasting debugging time.

Suggested change
} catch {
signal.throwIfAborted();
throw new ImageViewError(
'decode_failed',
`Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`,
} catch (error) {
signal.throwIfAborted();
const msg = error instanceof Error ? error.message : '';
if (msg.includes('pixels')) {
throw new ImageViewError(
'source_too_large',
`Image pixel dimensions exceed the processing limit: ${filePath}`,
);
}
throw new ImageViewError(
'decode_failed',
`Failed to decode image (file may be corrupt or not a static PNG, JPEG, or WebP): ${filePath}`,
);
}
中文说明

[Suggestion]catch {} 丢弃了底层 sharp 错误,始终抛出 'decode_failed'。但 sharp(bytes, { limitInputPixels: true }).metadata() 也可能因像素尺寸超过默认 ~268 Mpx 限制而拒绝——一个有效、未损坏的 JPEG 只是像素过多。错误码和消息会声称文件"可能已损坏或不是静态 PNG、JPEG 或 WebP",这是误导性的。

失败场景:一个 50 MB 的 20000×20000 像素(400M px)JPEG 通过了 100 MB 字节大小检查,但 sharp 在 limitInputPixels 上拒绝。调用方收到 decode_failed,无法将其与实际损坏区分开来。

具体代价:运维人员会认为文件已损坏,而实际上它只是超过了像素尺寸上限,浪费调试时间。

— qwen3.7-max via Qwen Code /review

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.

Declining this one. Since commit f5d1b2772, the read_file / @-attachment / read_many_files paths fall through to the legacy inline-bytes branch on decode_failed, so the "may be corrupt" message no longer reaches users there — a pixel-over-limit image instead surfaces as the base64-size FILE_TOO_LARGE from that branch. The only remaining surface for this message is zoom_image's opt-in path. The proposed msg.includes('pixels') matcher also depends on sharp/libvips' English error string, which is an unverified, fragile heuristic: it becomes dead branching if it does not match the real message and silently regresses if libvips rewords. Given the reduced impact and the fragile matcher, this is not worth the diff growth; leaving the thread open in case a robust detector (e.g. a structured sharp error) is preferred later.

中文说明

拒绝此项。自提交 f5d1b2772 起,read_file / @ 附件 / read_many_files 路径在 decode_failed 时回退到旧的内联字节分支,因此 "may be corrupt" 消息不再在这些路径中到达用户 — 超过像素上限的图像会改为以该分支的 base64 尺寸 FILE_TOO_LARGE 呈现。此消息唯一剩余的暴露面是 zoom_image 的可选路径。建议的 msg.includes('pixels') 匹配器还依赖 sharp/libvips 的英文错误字符串,这是一个未经验证、脆弱的启发式:若不匹配真实消息则成为死分支,若 libvips 改写措辞则会静默回归。鉴于影响已减小且匹配器脆弱,这不值得增加 diff;保持线程开启,以便后续倾向于更稳健的检测器(例如 sharp 的结构化错误)。

Comment thread packages/core/src/utils/image-view.ts Outdated
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review feedback addressed

Thanks for the thorough local verification. The three blocking regressions (B1–B3) all shared one root cause and are fixed by the single change you recommended: in the catch around renderImageOverview in fileUtils.ts, only the size codes (source_too_large / output_too_large) still return a hard FILE_TOO_LARGE; every other ImageViewError code now falls through to the legacy inline-bytes branch directly below, which still enforces the 9.9 MB / base64 cap. That restores main's forward-verbatim behaviour for renderer_unavailable, animated_image, unsupported_image, and decode_failed, and leaves the happy path and the size guards untouched.

Blocking findings

  • 🔴 B1 — sharp absent from the published package (renderer_unavailable). Fixed. When sharp cannot be loaded, renderImageOverview throws renderer_unavailable and the read now falls through to the legacy inline-bytes branch instead of hard-failing, so canonical images read exactly as on main in an npm-installed CLI. (Declaring sharp / @img/* in scripts/prepare-package.js is, as you noted, independent of this PR and out of scope here.)
  • 🔴 B2 — animated WebP hard-fail. Fixed. An animated canonical image trips animated_image and now falls through to the legacy branch, forwarding the original bytes verbatim (covered by the rewritten forwards an animated canonical image verbatim… test).
  • 🔴 B3 — non-canonical content behind a canonical extension. Fixed. GIF/TIFF/AVIF/etc. bytes behind a .png/.jpg/.webp name trip unsupported_image (or decode_failed when undecodable) and now fall through to the legacy branch, forwarding the bytes as on main (covered by the new forwards non-canonical content behind a canonical extension verbatim test). The corrupt/truncated-PNG case likewise falls through via decode_failed; the rewritten forwards a corrupt canonical image verbatim… test pins that.

Non-blocking observations (considered, not changed)

These were framed as non-blocking and are left as-is to keep the change minimal:

  • Three mutually-masking 100 MB guards / unreachable post-readFile bytes.length re-check / unreachable 9 MB output guard. Left in place. Removing or consolidating guards is a separate, riskier change; the byte re-check is defence-in-depth and the output guard mirrors the zoom path. Happy to revisit in a follow-up if you want them rationalised.
  • No assertion for quality 92 / 4:4:4 / lanczos3. Not added — a deterministic, meaningful assertion for the resampling kernel is fuzzy and would be speculative test growth. Open to a concrete oracle if you have one.
  • IMAGE_MAX_SOURCE_BYTES is a byte cap, not a memory bound; sharp(Buffer) trades RSS for wall time. Acknowledged. These are accurate trade-offs; noting them in the PR description's risk section is a documentation tweak that can ride along if a maintainer wants it. No code change.

Conflict notes

--conflict false — no merge performed; the branch stays based on its current head.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (fileUtils.ts, fileUtils.test.ts) — passed
  • npx vitest run src/utils/fileUtils.test.ts src/utils/image-view.test.ts (packages/core) — 175 passed
  • npx vitest run src/tools/read-file.test.ts src/utils/pathReader.test.ts src/utils/readManyFiles.test.ts (packages/core) — 134 passed
中文说明

已处理的复查反馈

感谢你扎实的本地验证。三个阻塞性回退(B1–B3)同属一个根因,已用你推荐的单一改动修复:在 fileUtils.tsrenderImageOverviewcatch 里,只有尺寸类错误码(source_too_large / output_too_large)仍返回硬性的 FILE_TOO_LARGE其余所有 ImageViewError 错误码现在都落回紧挨其下的原有 inline-bytes 分支,该分支仍然执行 9.9 MB / base64 上限。这样就把 renderer_unavailableanimated_imageunsupported_imagedecode_failed 都恢复成 main 的原样透传行为,正常路径与尺寸守卫完全不受影响。

阻塞项

  • 🔴 B1 — 已发布包里没有 sharprenderer_unavailable)。 已修复。当 sharp 无法加载时,renderImageOverview 抛出 renderer_unavailable,读取现在会落回原有 inline-bytes 分支而非硬失败,因此在 npm 安装的 CLI 里标准图片的读取与 main 完全一致。(在 scripts/prepare-package.js 中声明 sharp / @img/*,如你所述,与本 PR 相对独立,不在本次范围内。)
  • 🔴 B2 — 动图 WebP 硬失败。 已修复。动图标准图片会命中 animated_image,现在落回原有分支,原样透传原始字节(由改写后的 forwards an animated canonical image verbatim… 测试覆盖)。
  • 🔴 B3 — 扩展名是标准格式、内容不是的文件。 已修复。.png/.jpg/.webp 名字下装 GIF/TIFF/AVIF 等字节会命中 unsupported_image(无法解码时为 decode_failed),现在落回原有分支,像 main 一样透传字节(由新增的 forwards non-canonical content behind a canonical extension verbatim 测试覆盖)。截断/损坏的 PNG 同样经由 decode_failed 落回,改写后的 forwards a corrupt canonical image verbatim… 测试钉住了这一行为。

非阻塞观察项(已评估,未改动)

这些被明确标为非阻塞,为保持改动最小而未处理:

  • 三处互相遮蔽的 100 MB 守卫 / 无人触达的 readFilebytes.length 复查 / 不可达的 9 MB 输出守卫。 保留原样。移除或合并守卫是另一项风险更高的改动;bytes.length 复查属纵深防御,输出守卫与 zoom 路径保持一致。如果你希望统一梳理,乐意在后续 PR 跟进。
  • quality 92 / 4:4:4 / lanczos3 无断言。 未新增——对重采样内核做确定且有意义的断言比较模糊,会引入投机性测试膨胀。如果你有具体的判定基准,欢迎提供。
  • IMAGE_MAX_SOURCE_BYTES 是字节上限而非内存上界;sharp(Buffer) 用常驻内存换耗时。 已确认。这些取舍属实;在 PR 描述的风险段落写明属于文档微调,若维护者需要可顺带加上。不改代码。

冲突说明

--conflict false —— 未执行合并;分支保持基于当前 head。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(fileUtils.ts、fileUtils.test.ts)— 通过
  • npx vitest run src/utils/fileUtils.test.ts src/utils/image-view.test.ts(packages/core)— 175 通过
  • npx vitest run src/tools/read-file.test.ts src/utils/pathReader.test.ts src/utils/readManyFiles.test.ts(packages/core)— 134 通过

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

中文说明

未发现问题。LGTM!✅

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round summary — PR #7911

Feedback triage

[Critical] CHANGES_REQUESTED review — B1 / B2 / B3 (fileUtils.ts catch block). Already resolved in the code; no new change required this round. These three points (sharp absent in the shipped package → renderer_unavailable; animated WebP → animated_image; non-canonical bytes behind a canonical extension → decode_failed / unsupported_image) all describe a hard READ_CONTENT_FAILURE on read_file / @-attachments / read_many_files. The current HEAD already implements exactly the reviewer's suggested fix: commit f5d1b2772 ("fall back to inline bytes when image overview cannot render", 2026-07-28 19:00 UTC — after this feedback's evaluation cutoff) reworked the catch block so that every non-size ImageViewError code falls through to the legacy inline-bytes branch, while only source_too_large / output_too_large remain a hard FILE_TOO_LARGE error. I re-read the catch block at the reviewed commit and confirmed: renderer_unavailable, animated_image, unsupported_image, and decode_failed all fall through to forward the raw bytes verbatim (matching main), so none of B1/B2/B3 can occur on the current code.

[Suggestion] rc:3668422192 (image-view.ts:235) — implemented. The extract() → resize() → flatten() → jpeg() → toBuffer() pipeline runs after prepareImage() has already decoded the image, so a failure there is a render/encode error, not a decode error. Changed the thrown message from Failed to decode image: <path> to Failed to render image overview: <path>. The error code stays decode_failed (per the suggestion), which preserves the fileUtils fall-through behaviour — a non-size code still forwards the raw bytes instead of hard-failing the read. zoom_image displays error.message verbatim, so its users now see an accurate message too.

[Suggestion] rc:3668422177 (image-view.ts:184) — declined. See the threaded reply for the full reason. In short: (1) since commit f5d1b2772, the read_file / @-attachment / read_many_files paths fall through to inline bytes on decode_failed, so the "may be corrupt" message no longer reaches users there — a pixel-over-limit image surfaces as the base64-size FILE_TOO_LARGE instead; (2) the proposed msg.includes('pixels') matcher depends on sharp/libvips' unverified English error string, a fragile heuristic that becomes dead branching if it does not match and silently regresses if libvips rewords; (3) the only remaining surface is zoom_image's opt-in path. Not worth the fragile diff growth.

Conflict notes

--conflict false; no merge performed.

Verification

  • npx vitest run src/utils/image-view.test.ts src/utils/fileUtils.test.ts src/tools/read-file.test.ts src/tools/zoom-image.test.ts src/utils/pathReader.test.ts src/utils/readManyFiles.test.ts (packages/core, touched) — 328 passed (6 files)
  • npm run typecheck — passed
  • npx eslint packages/core/src/utils/image-view.ts — passed (no findings)
  • npm run build — passed
中文说明

Autofix 审查轮次总结 — PR #7911

反馈分类

[Critical] CHANGES_REQUESTED 审查 — B1 / B2 / B3(fileUtils.ts catch 块)。 代码中已解决;本轮无需新改动。这三点(发布的 npm 包中缺少 sharp → renderer_unavailable;动态 WebP → animated_image;规范扩展名背后的非规范字节 → decode_failed / unsupported_image)都描述 read_file / @ 附件 / read_many_files 上硬性 READ_CONTENT_FAILURE。当前 HEAD 已经实现了审查者建议的修复:提交 f5d1b2772("fall back to inline bytes when image overview cannot render",2026-07-28 19:00 UTC — 晚于本反馈的评估截止时间)重构了 catch 块,使每个非尺寸类 ImageViewError 码都回退到旧的内联字节分支,而仅 source_too_large / output_too_large 保留为硬性 FILE_TOO_LARGE 错误。我在被审查的提交上重新阅读了 catch 块并确认:renderer_unavailableanimated_imageunsupported_imagedecode_failed 都会回退以原样转发原始字节(与 main 一致),因此 B1/B2/B3 在当前代码上都不可能发生。

[Suggestion] rc:3668422192(image-view.ts:235)— 已实现。 extract() → resize() → flatten() → jpeg() → toBuffer() 管线在 prepareImage() 已经解码图像之后运行,因此其中的失败是渲染/编码错误,而非解码错误。将抛出的消息从 Failed to decode image: <path> 改为 Failed to render image overview: <path>。错误码保持 decode_failed(遵循建议),这保留了 fileUtils 的回退行为 — 非尺寸类码仍然转发原始字节而非硬性使读取失败。zoom_image 原样显示 error.message,因此其用户现在也能看到准确的消息。

[Suggestion] rc:3668422177(image-view.ts:184)— 拒绝。 完整原因见线程回复。简言之:(1) 自提交 f5d1b2772 起,read_file / @ 附件 / read_many_files 路径在 decode_failed 时回退到内联字节,因此 "may be corrupt" 消息不再在这些路径中到达用户 — 超过像素上限的图像会改为以 base64 尺寸 FILE_TOO_LARGE 呈现;(2) 建议的 msg.includes('pixels') 匹配器依赖 sharp/libvips 未经验证的英文错误字符串,这是一个脆弱的启发式:若不匹配则成为死分支,若 libvips 改写措辞则会静默回归;(3) 唯一剩余的暴露面是 zoom_image 的可选路径。不值得增加脆弱的 diff。

冲突说明

--conflict false;未执行合并。

验证

  • npx vitest run src/utils/image-view.test.ts src/utils/fileUtils.test.ts src/tools/read-file.test.ts src/tools/zoom-image.test.ts src/utils/pathReader.test.ts src/utils/readManyFiles.test.ts(packages/core,已触及)— 328 通过(6 个文件)
  • npm run typecheck — 通过
  • npx eslint packages/core/src/utils/image-view.ts — 通过(无发现)
  • npm run build — 通过

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

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

中文说明

⚠️ 已从批准降级为评论:CI still running。 已审查。

— qwen3.7-max via Qwen Code /review

@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

No changes were made this round.

There was no actionable feedback to address:

  • Reviews: none newer than the last evaluation.
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none.
  • Base conflict: none reported (--conflict false), so no merge was performed.

The only listed item is in the Deferred non-Critical feedback section. The PR
has completed five change-producing review rounds, so critical-only mode is
active. Per the workflow rules that deferred section is an audit record for
human follow-up — not work for this bot — so no code was modified, no threads
were resolved, and no replies were posted on its behalf.

The branch feat/image-zoom-canonical-overview remains at
9351160d4 with a clean working tree.

中文说明

Autofix 审查轮次 — 未采取任何操作

本轮未做任何更改。

没有需要处理的可操作反馈:

  • 审查(Reviews): 没有比上次评估更新的审查。
  • 行内评论(Inline comments): 无。
  • Issue 级评论(Issue-level comments): 无。
  • 失败的检查(Failed checks): 无。
  • 仍然失败的检查(Still-red checks): 无。
  • 与基础分支的冲突(Base conflict): 未报告(--conflict false),因此未执行合并。

唯一列出的条目位于 延迟的非 Critical 反馈(Deferred non-Critical feedback) 部分。该 PR
已经完成了五个产生改动的审查轮次,因此当前处于仅处理 Critical 的模式。
根据工作流规则,该延迟部分属于供人工跟进的审计记录,
而非本机器人的工作 —— 因此未修改任何代码、未解决任何线程,
也未代其发表任何回复。

分支 feat/image-zoom-canonical-overview 仍停留在
9351160d4,工作树干净。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf.

中文说明

完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。

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

Re-verification — head 9351160d4d, round 2

Follow-up to my round-1 report (#issuecomment-5107723627, head 974b1c35a5). Three commits landed since, all in response to that round: 1fb281c69e (path out of error displays), f5d1b2772a (fall back to inline bytes), 9351160d4d (error message rename). I re-ran everything below against the new head in a fresh isolated worktree — every carried-over finding was re-tested, not carried forward on trust.

Verdict: merge-ready from my side. All three blockers are fixed, each fix is pinned by its own test, and a revert-only-the-fix experiment proves those tests are the ones standing guard.

v2-fig1

B2 / B3 — re-tested with real reads, compared against current main

Same harness on both arms (only fileUtils.ts / zoom-image.ts swapped for origin/main's copies), 13 on-disk fixtures through the real processSingleFileContent:

v2-fig2

Every fallback case — animated WebP, GIF/TIFF/AVIF behind a .png name, a truncated PNG — now returns bytes identical to what main returns. The only remaining differences are the feature itself: bounded overviews for static canonical images, including a 12.3 MB incompressible PNG that main rejects with file_too_large. Legacy paths (.gif, SVG) are byte-identical on both arms, and the 101 MB source still hard-fails with FILE_TOO_LARGE on both arms — with the user-visible text now path-free (error keeps the full path for logs).

B1 — re-tested in the published-package layout

One bundle built with the repo's own esbuild options (including inject: scripts/esbuild-shims.js), run from node_modules/@qwen-code/qwen-code/dist/ outside any workspace; the only variable is the sharp install state:

v2-fig3

At R1, the no-sharp arm returned READ_CONTENT_FAILURE for every image. At this head, both broken arms (sharp entirely absent; sharp present but @img/* binaries missing) degrade to byte-identical inline reads with zero errors, and the full-install arm renders overviews.

One correction to my own R1 report: I wrote that scripts/prepare-package.js omits sharp. That was true of the published 0.21.0 tarball (which is what I had unpacked), but the script itself already declares sharp: '0.34.5' in optionalDependencies — it landed together with #7809 (bd2c0b47f1), so the next release ships it. With that packaging plus this PR's fallback, the failure mode is covered from both ends: a normal install gets the overview pipeline, and any install where the optional dep didn't materialize silently keeps main's behaviour.

Is the fix load-bearing, and is it pinned?

I reverted only the fallthrough (restoring the exact pre-fix catch block) and re-ran the suites:

v2-fig4

Exactly the three new forwards … verbatim tests fail — the R1 blockers reproduce the moment the fix is removed, and the tests added in f5d1b2772a are the ones that would catch a regression. Two smaller mutants survive (dropping the catch-side hard FILE_TOO_LARGE mapping, and dropping its path-strip): both sit behind the stat pre-check at fileUtils.ts:1248, which intercepts oversized files before renderImageOverview can throw — the same masked-guard family I measured in R1 fig 3. Non-blocking, just worth knowing that branch is effectively belt-and-suspenders.

Gates

gate result
6 touched spec files at head 328 / 328 pass
trial-merge onto current main 0c0ca5fed0 (main's drift since merge-base is comment-only), same suites clean merge · 328 / 328 pass
prettier --check on all 8 changed files pass
eslint on all 8 changed files pass (a planted unused-var control confirmed the linter actually fires)

One nit, non-blocking

9351160d4d renamed the decode_failed message to Failed to render image overview: …, but that message lives in the shared renderImageView, which the zoom path also calls — a zoom decode failure will now describe itself as an "overview" failure. On the read path the message is unused (decode failures fall through silently). Fine to fix in a follow-up or leave.

R1's non-blocking observations (memory tracks pixels, not bytes — a 0.74 MB / 16000×16000 PNG peaks at ~360 MB RSS; quality/lanczos3 unasserted) are unchanged by these commits and stand as documented risk notes.

Method

Isolated worktree detached at 9351160d4d, node_modules APFS-cloned from a lockfile-matching checkout (all @qwen-code/* entries verified to resolve in-tree). Matrix driven by tsx over the real processSingleFileContent with a real FileSystemService; main arm produced by overlaying only fileUtils.ts + zoom-image.ts from origin/main in the same tree (main's drift since merge-base b3873571aa is comment-only, verified by diff). Bundle arms: esbuild with the repo's external list (sharp externalized), packages: 'bundle', inject: scripts/esbuild-shims.js, run from a fake published layout in a directory with no node_modules ancestor; sharp state probed from inside the bundle via await import('sharp'). Arm C stages sharp plus its real dependency closure (@img/colour, detect-libc, semver) and @img/sharp-{darwin-arm64,libvips-darwin-arm64}. Mutants applied one at a time and restored; each verified to produce a real source diff before its run. sharp 0.34.5 / libvips 8.17.3, macOS arm64, Node 22.23.1. New figures pushed as pr7911-v2-* on the existing pr-assets/pr-7911-verify branch so round-1 images keep resolving.

中文说明

复验 — head 9351160d4d,第 2 轮

承接第 1 轮报告(#issuecomment-5107723627,head 974b1c35a5)。此后新增三个提交,均为响应该轮反馈:1fb281c69e(错误展示中去掉绝对路径)、f5d1b2772a(回退到 inline 字节)、9351160d4d(错误文案改名)。以下所有内容都在全新隔离 worktree 中针对新 head 重新实测,没有任何一条沿用旧结论。

结论:我这边认为可以合并。 三个阻塞项全部修复,每个修复都被自己的测试钉住,且"只还原修复"的对照实验证明站岗的正是这些测试。

B2 / B3 — 真实读取重测,与当前 main 对照(见 v2-fig2):同一套 harness 跑两臂(仅替换 fileUtils.ts / zoom-image.ts 为 origin/main 版本),13 个磁盘 fixture 走真实 processSingleFileContent。所有回退用例——动图 WebP、.png 扩展名下的 GIF/TIFF/AVIF、截断 PNG——返回字节与 main 完全一致;仅剩的差异就是特性本身:静态标准图片的有界概览,包括 main 直接拒绝的 12.3 MB 不可压缩 PNG。旧路径(.gif、SVG)两臂逐字节一致;101 MB 源文件两臂都保持硬性 FILE_TOO_LARGE,且用户可见文本已不含绝对路径(error 字段保留完整路径供日志用)。

B1 — 发布包环境重测(见 v2-fig3):用仓库自己的 esbuild 选项(含 inject: scripts/esbuild-shims.js)构建同一个 bundle,放在 workspace 之外的 node_modules/@qwen-code/qwen-code/dist/ 运行,唯一变量是 sharp 安装状态。R1 时无 sharp 臂对所有图片返回 READ_CONTENT_FAILURE;新 head 上,两个损坏臂(完全没有 sharp;有 sharp 但缺 @img/* 二进制)都优雅降级为逐字节 inline 读取、零错误,完整安装臂正常渲染概览。

对我 R1 报告的一处修正:我当时写 scripts/prepare-package.js 漏掉了 sharp。这对已发布的 0.21.0 tarball(我当时解包核对的对象)成立,但脚本本身其实已在 optionalDependencies 中声明 sharp: '0.34.5'——随 #7809(bd2c0b47f1)落地,下个版本即包含。打包与回退双保险:正常安装获得概览管线,可选依赖未装上的环境静默保持 main 行为。

修复是否承重、是否被钉住(见 v2-fig4):只还原 fallthrough(恢复修复前的 catch 块原文)后重跑套件,恰好三个新增 forwards … verbatim 测试失败——去掉修复,R1 阻塞项立即复现,且守卫正是 f5d1b2772a 新增的测试。两个较小变异存活(去掉 catch 内硬性 FILE_TOO_LARGE 映射;去掉其路径剥离):都被 fileUtils.ts:1248 的 stat 预检遮蔽——与 R1 fig 3 测得的守卫互遮同族,非阻塞。

关卡:6 个相关 spec 328/328 通过;trial-merge 到当前 main 0c0ca5fed0(main 自 merge-base 以来仅注释漂移)合并干净、328/328 通过;prettier / eslint 8 个改动文件全过(植入错误对照确认 linter 真实生效)。

一个非阻塞小瑕疵:9351160d4ddecode_failed 文案改为 Failed to render image overview: …,但该文案位于共享的 renderImageView 中,zoom 路径同样会走到——zoom 解码失败会自称"overview"失败。读取路径上该文案已不可见(解码失败静默回退)。可随手修或保留。

R1 的非阻塞观察(内存跟随像素而非字节——0.74 MB / 16000×16000 PNG 峰值约 360 MB RSS;quality/lanczos3 无断言)不受这三个提交影响,作为风险记录继续成立。

方法:worktree detach 在 9351160d4d,node_modules 从 lockfile 匹配的检出 APFS 克隆(已验证所有 @qwen-code/* 解析到树内)。矩阵用 tsx 驱动真实 processSingleFileContent 与真实 FileSystemService;main 臂仅覆盖 origin/main 的 fileUtils.ts + zoom-image.ts(main 自 merge-base b3873571aa 起仅注释漂移,已 diff 验证)。bundle 臂:esbuild 使用仓库 external 列表(sharp 外部化)、packages: 'bundle'inject: scripts/esbuild-shims.js,在无 node_modules 祖先目录的伪发布布局中运行,并从 bundle 内部用 await import('sharp') 探针确认状态;C 臂补齐 sharp 的真实依赖闭包(@img/colourdetect-libcsemver)及 @img/sharp-{darwin-arm64,libvips-darwin-arm64}。变异逐个应用、确认产生真实 diff 后运行、随后还原。sharp 0.34.5 / libvips 8.17.3,macOS arm64,Node 22.23.1。新图以 pr7911-v2-* 命名推到既有 pr-assets/pr-7911-verify 分支,第 1 轮图片链接继续有效。

@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

This round had no actionable feedback for PR #7911:

  • Reviews: none.
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none.
  • Base conflict: none reported (--conflict false); no merge performed.

The PR is in critical-only mode after five change-producing rounds. The one
remaining item — a PR comment by @wenshao
(#7911 (comment)) — is
recorded under "Deferred non-Critical feedback". Per the review policy it is an
audit record for human follow-up, not work for this bot: no code was changed,
no thread was resolved, and no reply was posted on its behalf.

No commits were made and the branch is unchanged.

中文说明

Autofix 审查轮次 —— 未采取任何操作

本轮针对 PR #7911 没有可处理的反馈:

  • 审查(Reviews): 无。
  • 行内评论(Inline comments): 无。
  • Issue 级评论(Issue-level comments): 无。
  • 失败的检查(Failed checks): 无。
  • 持续失败的检查(Still-red checks): 无。
  • 与 base 的冲突(Base conflict): 未报告(--conflict false);未执行合并。

在经历 5 个产生改动的轮次后,该 PR 已进入仅处理 Critical 的模式。剩余的唯一一项 —— @wenshao 的一条 PR 评论(https://github.com/QwenLM/qwen-code/pull/7911#issuecomment-5111110398)—— 被记录在“延迟的非 Critical 反馈(Deferred non-Critical feedback)”下。根据审查政策,它属于供人工跟进的审计记录,而非本机器人需要处理的工作:未修改任何代码,未解决任何线程,也未代其发表任何回复。

未创建任何提交,分支保持不变。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf.

中文说明

完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。

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

@qwen-code /verify

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: merge-ready (agent verdict) - workflow run
沙箱验证:可合入(agent 判定)

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

Scripted assertions: 97 passed · 0 failed · 97 total

脚本断言:97 通过 · 0 失败 · 97 总计

Verification report (report.md)

# PR #7911 Deep Verification — `feat(core): bound image reads for reliable zoom`

**Verdict: `merge-ready`** — 97/97 scripted assertions passed (86 A/B + 11 probe), 0 failures. Central claim proven load-bearing by a clean A/B against the base build. Targeted gate green (309/309). No behavioral defect or regression found; two non-blocking coverage gaps noted in Findings.

Verified head OID: `9351160d4d50e30174ed8081577454b7bb632d65` (`HEAD^2`); base tip `0c0ca5fed0e287b98d9be9e51d364d01be3d2041` (`HEAD^1`); merge commit `6f42e376945cacb5ea71b74ff3e6c045c63bc761`.

<details>
<summary>中文摘要</summary>

- **结论**:`merge-ready`。全部 97 条脚本化断言通过(A/B 86 条 + 探针 11 条),0 失败;未发现行为缺陷或回归。
- **A/B 结论**:见下方「A/B cell table」。对 8 个由真实 sharp 生成的图片驱动 `processSingleFileContent`:base 把全部 8 个文件**原样**以单个 inline part 返回(`verbatim=true`);head 把其中 7 个 canonical(PNG/JPEG/WebP)翻转为「概览文本 + 有界 JPEG」两段结构,GIF 两侧字节完全一致(保持旧行为)。`large.png` 3000x2000 → 概览 1344x896(受最长边/patch 预算约束),`oriented.jpg` EXIF-6 400x200 → 报告并渲染为 200x400(自动旋转),`tiny.png` 12x10 不被放大,透明 PNG 铺白后编码为 JPEG。
- **findings**:两条均为**非阻塞 Suggestion**(覆盖缺口,非缺陷):(1) `unsupported_image` 守卫经探针证明是 load-bearing,但现有「mismatch」测试用的是动画 GIF(先被 `animated_image` 拦截),没有任何单帧非 canonical 格式的用例覆盖它;(2) `source_too_large` 的 `stats.size` 预检被同函数内的 `bytes.length` 复检与 `fileUtils` 的提前拦截在行为上掩盖,属防御性冗余,无测试钉住。详见 Findings。
- **未覆盖范围**:见 Not covered(zoom_image 的独立 A/B、animated_image 的多页真实渲染、output_too_large、provider 转换下游、Windows/无 sharp 环境等)。

</details>

## Central claim + A/B proof

**Central claim:** reading a static PNG/JPEG/WebP via `processSingleFileContent` now returns a two-part `llmContent` — an overview text (`Image overview: WxH; oriented source: WxH. … zoom_image … normalized from 0 to 1000.`) followed by a bounded, auto-oriented, white-flattened JPEG — instead of forwarding the original bytes verbatim. Non-canonical formats (GIF/BMP/audio/video/SVG/PDF) keep legacy behavior.

**A/B design.** `packages/core` has **no `@qwen-code/*` workspace dependencies** and the PR touches **no `package.json`/`package-lock.json`**, so the internal-symlink confound does not apply and sharing the root `node_modules` is a clean control. I bundled `packages/core/src/utils/fileUtils.ts` from **both** trees with identical esbuild flags (`--bundle --format=esm --platform=node --external:sharp --external:@lydell/node-pty`), so the only variable is head-source vs base-source; `sharp` (the real renderer) resolves identically for both cells. Base source came from a scratch worktree at `HEAD^1` (confirmed pre-PR: `image-view.ts` absent). Fixtures are real images produced by `sharp` (`make-images.mjs`); the harness (`harness-ab.mjs`) drives the real compiled function — no stubs of the code under test.

### A/B cell table

| File (real sharp fixture) | Oracle | BASE (`HEAD^1`) | HEAD (`HEAD^2`) |
| --- | --- | --- | --- |
| `small.png` 100x80 | llmContent shape; data mime; verbatim; decoded | `single-inline`, image/png, verbatim | `array[2]`, overview text + image/jpeg 100x80, re-encoded |
| `tiny.png` 12x10 | not upscaled | single-inline verbatim | `array[2]`, JPEG **12x10** (no upscale) |
| `large.png` 3000x2000 | bounded by edge+patch budget | single-inline verbatim (83 460 B) | `array[2]`, overview **1344x896**, source reported 3000x2000, JPEG 14 380 B |
| `oriented.jpg` 400x200 EXIF-6 | auto-oriented dims | single-inline verbatim | `array[2]`, oriented source **200x400**, JPEG decodes 200x400 |
| `transparent.png` 200x150 alpha | flattened onto white | single-inline verbatim | `array[2]`, JPEG 200x150 (alpha removed) |
| `photo.webp` 320x240 | canonical WebP → overview | single-inline image/webp verbatim | `array[2]`, JPEG 320x240 |
| `plain.jpg` 640x480 | canonical JPEG → overview | single-inline verbatim | `array[2]`, JPEG 640x480 |
| `anim-frame1.gif` 60x60 | non-canonical unchanged | single-inline image/gif verbatim | single-inline image/gif verbatim — **byte-identical to base** |

**Result: 7/7 canonical images flip from "single raw inline part (verbatim)" to "overview text + bounded JPEG"; the GIF is identical on both sides.** Every overview JPEG was re-decoded with sharp to confirm its bytes match the reported output dimensions and stay within the shared budget (edge ≤ 1568, patches ≤ 1568, output ≤ 9 MB). 86/86 scripted assertions in `assert.mjs` pass.

### Secondary claims (probe, 11/11 assertions in `probe-assert.mjs`)

- **Error fall-through (documented intent).** A corrupt canonical PNG (`decode_failed`) falls through to the legacy inline-bytes branch — returned `single-inline`, image/png, `verbatim=true`, **no error**. A >100 MB canonical source stays a **hard error** (`errorType=file_too_large`, `Image file exceeds the 100 MB source limit.`), never an overview. A `.png` holding valid JPEG bytes still renders an overview (sharp decodes by content), confirming fall-through is keyed on decode failure, not extension mismatch.
- **`zoom_image` refactor.** `zoom-image.ts` now delegates to the shared `renderNormalizedImageCrop`; the crop/magnification path is the same code exercised by the A/B renderer and is pinned by `image-view.test.ts` ("may magnify a normalized crop…") and `read-file.test.ts`.

### Vacuity check (clean)

Disabling the feature (`shouldRenderImageOverview = false`, scratch edit, restored) makes the central test `fileUtils.test.ts > should process an image file` fail on its **intended assertion** — `expected undefined to deeply equal { text: "Image overview: 20x10; oriented source: 20x10. …" }` (expected-vs-actual, not an import/compile/fixture break). The test is pinned by the change, not vacuous. Source restored; `git status` clean.

### Mutation matrix (completeness reporting, not merge conditions)

| Mutant (guard removed/altered) | Suite that should catch it | Result |
| --- | --- | --- |
| remove `animated_image` guard | `forwards an animated canonical image verbatim…` | **killed** |
| flatten black instead of white | `flattens transparent overview pixels onto white` | **killed** |
| disable `autoOrient` | `applies EXIF orientation before describing and rendering an overview` | **killed** |
| remove `unsupported_image` guard | (none) | **survived** → coverage gap (see Findings) |
| remove `source_too_large` stats guard | (none) | **survived** → defensive redundancy (see Findings) |

Unmutated control is green (309/309). No mutant regressed a killed guard back to survived relative to the PR's own claims.

### Targeted gate

`npx vitest run` on the 5 affected files: `image-view.test.ts` (4), `pathReader.test.ts` (17), `readManyFiles.test.ts` (30), `read-file.test.ts` (87), `fileUtils.test.ts` (171) → **309/309 passed**. Gate liveness is established by the mutation matrix and vacuity check above (mutants produced real failures).

## Findings

### S1 — `unsupported_image` guard is load-bearing but untested (Suggestion, non-blocking)

The `SUPPORTED_IMAGE_FORMATS` guard in `image-view.ts` survived the mutation matrix, but it is **not dead code**. Probe (`probe-unsupported.mjs`): a single-frame **TIFF** presented under a `.png` extension returns `single-inline`/verbatim with the guard present, and flips to `array[2]`/image/jpeg overview with the guard removed (`unsup-guard.json` vs `unsup-noguard.json`). It survived only because the existing `forwards non-canonical content behind a canonical extension verbatim` test uses an **animated** GIF, which the `animated_image` guard catches first — so no test reaches `unsupported_image` on its own. Behavior is correct; the gap is test coverage.

<details>
<summary>Suggested fix (preserves intent)</summary>

Add a `fileUtils.test.ts` case mirroring the existing mismatch test but with a **single-frame** non-canonical format (e.g. a TIFF) behind a `.png` extension, asserting the verbatim fall-through. That single fixture pins the guard the animated fixture masks.
</details>

### S2 — `source_too_large` `stats.size` pre-check is untested defensive redundancy (Suggestion, non-blocking)

The `stats.size > IMAGE_MAX_SOURCE_BYTES` guard in `prepareImage` survived mutation because it is masked twice: `fileUtils.ts` rejects >100 MB canonical sources *before* calling `renderImageOverview` (pinned by `rejects canonical image sources above 100 MB before decoding`), and `prepareImage` re-checks `bytes.length` afterward. For the `zoom_image` path (which has no own size pre-check and handles `source_too_large` at `zoom-image.ts:99`), the `bytes.length` guard still throws the same code, so behavior is identical with the stats guard removed. The stats guard's real value is avoiding reading 100 MB+ into memory before the byte check (DoS hygiene); it is not a behavioral defect, just unpinned.

<details>
<summary>Suggested fix (preserves intent)</summary>

Either add a `renderImageOverview`/`renderNormalizedImageCrop` unit test that sparse-truncates a >100 MB canonical file and asserts `code: 'source_too_large'` without buffering it, or accept the redundancy and leave a one-line note that the stats check is the memory-safe fast path. No code change required for correctness.
</details>

## Not covered

- **Independent `zoom_image` A/B.** The shared renderer is proven, and the crop path is unit-pinned, but I did not run a head-vs-base A/B of the `zoom_image` tool invocation itself (the refactor is behavior-preserving by construction; the deleted local helpers are byte-for-byte the logic now in `image-view.ts`).
- **`animated_image` via real multi-page render.** This container's `sharp` lacks `.join`, so I could not synthesize a multi-page WebP/APNG for the harness; the path is covered by `fileUtils.test.ts` (animated WebP built from a 2-frame GIF) which passed in the gate, and by the mutation kill above.
- **`output_too_large` (>9 MB rendered output)** and **`renderer_unavailable` (sharp missing)** were not exercised end-to-end (hard to construct deterministically within budget); both are simple threshold/throw guards.
- **Downstream two-part consumers** named in the PR body (batch reads, `@` attachments, vision-bridge preservation, token estimation, Gemini/Anthropic/OpenAI conversion, telemetry, compaction) were not individually driven; `readManyFiles.test.ts` and `read-file.test.ts` passed in the gate.
- **Per-commit attribution.** The checkout is depth 2 (shallow); only the aggregate `HEAD^1..HEAD` diff was verifiable. The 7 commits in `$QWEN_VERIFY_CONTEXT` could not be exercised individually.
- **Windows / sharp-absent environments** (PR self-reports ⚠️ on Windows/Linux) and live model answer-quality/latency — out of scope for a sandboxed verify.
- **Repo-wide gate / typecheck / bundle** were not re-run (the PR's own CI covers them; the affected-workspace gate was run here).

## Methodology

CI verify job (credential-free container): merge-ref checkout at depth 2, `npm ci` + `npm run build` already complete at HEAD. I created a scratch worktree at `HEAD^1` (base tip), confirmed it is pre-PR (`image-view.ts` absent), and — because `packages/core` has no `@qwen-code/*` deps and the lockfile is unchanged — bundled `fileUtils.ts` from both trees with identical esbuild flags (only `sharp`/`node-pty` external) for a symmetric A/B isolating the source difference. `harness-ab.mjs` generated 8 real images with sharp and drove the real compiled `processSingleFileContent` from each bundle, re-decoding every returned JPEG to verify dimensions/budget; `assert.mjs` scripted the 86 cell assertions. `probe-errors.mjs`/`probe-unsupported.mjs` covered the error fall-through and guard-classification probes (11 assertions in `probe-assert.mjs`). `mutate.mjs` ran the mutation matrix against the vitest suites with backup/restore; the vacuity check used a scratch edit reverted afterward. The targeted gate ran the 5 affected test files via `npx vitest run`. Raw logs and harnesses: `head.json`, `base.json`, `probe.json`, `unsup-guard.json`, `unsup-noguard.json`, `gate-tests.log`, and the `.mjs` harnesses in this directory. The base worktree was removed after capture; `git status` on `packages/` is clean.

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@wenshao

wenshao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

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

No action taken

No new actionable feedback was found for PR #7911 in this round.

  • Reviews: none
  • Inline comments: none
  • Issue-level comments: none
  • Failed checks: none
  • Still-red checks: none

Critical-only mode is active (5 change-producing rounds completed). The one deferred non-Critical item (a CI bot PR comment) is an audit record and requires no code change, thread resolution, or reply.

The PR branch (feat/image-zoom-canonical-overview) is unchanged at 9351160d4.

中文说明

未采取任何操作

本轮在 PR #7911 中未发现新的可处理反馈。

  • 评审:
  • 行内评论:
  • Issue 级评论:
  • 失败的检查:
  • 持续失败的检查:

当前处于仅处理 Critical 的模式(已完成 5 个产生改动的轮次)。唯一一条被延迟的非 Critical 条目(CI 机器人的 PR 评论)属于审计记录,无需修改代码、解决线程或回复。

PR 分支(feat/image-zoom-canonical-overview)保持不变,停留在 9351160d4

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. Any items listed below stay open for human follow-up; do not modify code, resolve threads, or reply on their behalf.

中文说明

完成 5 个产生改动的轮次后,进入仅处理 Critical 的模式。以上内容保持开放,留待人工跟进;不要为其修改代码、解决线程或代为回复。

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


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

@wenshao
wenshao added this pull request to the merge queue Jul 29, 2026
Merged via the queue into QwenLM:main with commit 2ae9e1f Jul 29, 2026
85 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.2.

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.

6 participants