Skip to content

feat(web-shell): add unified session sources - #11262

Merged
callmeYe merged 19 commits into
mainfrom
codex/session-sources-design
Sep 10, 2026
Merged

feat(web-shell): add unified session sources#11262
callmeYe merged 19 commits into
mainfrom
codex/session-sources-design

Conversation

@callmeYe

@callmeYe callmeYe commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a unified Sources list for uploaded files, workspace-file references, and HTTP(S) links. Existing attachments remain discoverable without metadata migration, and registered attachment details take precedence without displaying the same file twice. Rows are single-line, default to three items, and offer View all / Collapse.

Adds explicit agent/client source registration, owner-routed session APIs, durable metadata snapshots, and list invalidation events. Registration stores metadata without reading files, fetching URLs, adding resource contents to prompts, or creating output artifacts. File previews reuse the existing session/workspace access checks; source HTML stays text and binary attachments remain downloadable.

Why it's needed

The existing attachment list only covers uploaded files, while workspace documents and links have no unified session reference list. Users need one place to reopen their materials without distinguishing upload storage from optional reference metadata. Keeping registration separate from message delivery also lets a failed metadata write be retried without resending a message.

Reviewer Test Plan

How to verify

  • Open a session with uploaded files, including files with no source registration. Confirm there is one Sources section, duplicate attachment IDs produce one row, explicit titles take precedence, and the default three rows expand and collapse while retaining the total count.
  • Add a workspace-file reference and an HTTP(S) link. Confirm repeated registration preserves identity and creation time, identical retries do not increase the revision, metadata edits retain ordering, invalid locators are rejected, and the 200-record limit applies to registered metadata rather than hiding uploaded files.
  • Open uploaded and workspace text, images, PDFs, and unsupported binary files. Confirm the PDF body renders, binary downloads retain their bytes, HTML sources remain text after refresh, links are not fetched automatically, and unavailable or replaced owners cannot redirect a preview to another workspace.
  • Send an attachment and inject a metadata failure. Confirm the message and file remain usable and metadata Retry creates no additional upload or prompt. A definite prompt rejection should clean the uploaded bytes. Removing only a source registration should leave uploaded bytes visible as a plain file without automatically recreating the registration.
  • Restart/resume, rewind, compact, and fork a conversation. Confirm reference metadata survives, forked IDs are independent, attachment bytes are copied to the target session, and archived mutations are rejected. Malformed or future snapshots should make registered metadata unavailable while ordinary conversation loading and attachment discovery continue.
  • Check older-daemon capabilities and explicit host section choices. Uploaded files should remain available without unsupported source actions. Source-list and attachment-list failures should be visible independently and preserve successful results from the other side.

Evidence (Before & After)

Before: uploaded files appeared in Attachments, with no unified registry for workspace references and links. After: one Sources list covers those materials, preserves historical uploads, and uses compact single-line rows with reversible three-item disclosure.

Collapsed Expanded
Three visible source rows All sources with Collapse

Full repository build and typecheck passed. Focused core, bridge, daemon, SDK and Web Shell regressions passed, including the latest 33 environment-panel checks. Actual local acceptance covered the daemon, ACP child, tool scheduler, recording files, REST/SSE and Chrome: 20 unified-flow checks, three capacity/long-filename checks and two final visual checks passed. The final single-line and 3 → all → 3 interactions were also exercised in Chrome. A separate PR comment records the E2E results and validation boundaries.

Tested on

OS Status
🍏 macOS ✅ Local build, typecheck, focused regression tests and actual Chrome acceptance
🪟 Windows ⚠️ Not run locally
🐧 Linux ⚠️ Not run locally

Environment

Node 22.17.0 and Chrome 152.0.7977.82. Model responses used a deterministic localhost fixture with a test-only credential; the product runtime, scheduling, storage and browser interactions were real. No external model endpoint is claimed as validated.

Risk & Scope

  • Main tradeoff: uploaded file storage and explicit reference metadata keep their separate ownership and durability contracts. Cancelling an attachment registration removes its metadata, while existing file bytes remain discoverable. Historical files are never silently registered or migrated.
  • Failure boundary: a degraded recording writer rejects metadata writes until session recovery/restart; the feature does not acknowledge memory-only success. Post-admission metadata enrichment and its retry queue remain best-effort in browser memory.
  • Not validated / out of scope: external model providers, Windows/Linux local runtime acceptance, new standalone CLI or Python/Java source APIs, cross-client source copying through standalone CLI branching, usage tracking and automatic content retention.
  • Compatibility: metadata records and capability support are additive; older sessions retain uploaded-file access. The legacy attachments host setting remains compatible. Blob-backed PDF frames are permitted alongside the existing daemon-port loopback frame origins; no external frame origin is added.

Linked Issues

None.

中文说明

本 PR 的内容

增加统一的“来源”清单,展示上传文件、工作区文件引用和 HTTP(S) 链接。历史附件不需要迁移元数据就能找到;有登记信息的附件优先使用该信息,同一文件不会重复展示。列表统一单行,默认三条,支持“查看全部 / 收起”。

增加 Agent/客户端显式来源登记、按会话归属路由的接口、持久化元数据快照和列表失效通知。登记只保存元数据,不读取文件、不抓取 URL、不把资源内容加入 prompt,也不创建产物。文件预览复用现有会话/工作区访问检查;来源 HTML 保持文本预览,二进制附件支持下载。

为什么需要

现有附件列表只覆盖上传文件,工作区文档和链接没有统一的会话参考资料清单。用户需要一个重新打开资料的入口,无须区分上传存储和可选的引用元数据。登记与消息投递分开后,元数据写入失败可以独立重试,不会重发消息。

审阅验证计划

如何验证

  • 打开包含上传文件的会话,其中应有未登记来源的历史文件。确认只显示一个“来源”分区,相同 attachment ID 只显示一次,显式标题优先,默认三条可以展开和收起,总数保持正确。
  • 添加工作区文件引用和 HTTP(S) 链接。确认重复登记保留身份和创建时间,相同请求重试不增加 revision,编辑元数据不改变排序,非法定位信息被拒绝,200 条限制只约束登记元数据而不隐藏上传文件。
  • 打开上传文件与工作区文本、图片、PDF 和不支持预览的二进制文件。确认 PDF 正文确实渲染,下载字节一致,刷新后来源 HTML 仍为文本,不自动请求链接,归属不可用或被替换时不能将预览改向其他工作区。
  • 发送附件并注入元数据失败。确认消息和文件仍可使用,“重试”不会增加上传或 prompt。明确拒绝消息时应清理已上传字节。仅取消来源登记后,上传文件仍以普通文件显示,不会自动重新登记。
  • 重启/恢复、回退、压缩并分叉会话。确认引用元数据保留,分叉后的 ID 独立,附件字节复制到目标会话,归档会话拒绝修改。损坏或未来版本快照应让登记元数据不可用,但普通对话加载和附件发现继续可用。
  • 检查旧 daemon 的能力标识和宿主显式分区配置。上传文件仍应可见,不暴露不支持的来源操作。来源列表和附件列表的读取失败应分别提示,并保留另一侧已成功读取的资料。

证据(前后对比)

之前:上传文件显示在“附件”,工作区引用与链接缺少统一登记清单。之后:一个“来源”清单覆盖这些资料,保留历史上传文件,以单行展示,并支持三条与全部之间的往返切换。

收起状态 展开状态
默认三条来源 全部来源及收起操作

全仓 build 和 typecheck 通过。Core、bridge、daemon、SDK 与 Web Shell 的定向回归通过,包括最新 33 项环境面板检查。实际本地验收覆盖 daemon、ACP child、工具调度、录制文件、REST/SSE 和 Chrome:统一流程 20 项、容量/长文件名 3 项、最终视觉检查 2 项通过。最后的单行与 3 → 全部 → 3 交互也在 Chrome 中实际验证。独立 PR 评论记录 E2E 结果和验证边界。

验证系统

系统 状态
🍏 macOS ✅ 本地构建、类型检查、定向回归与实际 Chrome 验收
🪟 Windows ⚠️ 未在本地运行
🐧 Linux ⚠️ 未在本地运行

环境

Node 22.17.0、Chrome 152.0.7977.82。模型回复使用带测试占位凭据的本机确定性 fixture;产品运行时、调度、存储和浏览器交互均真实执行。不声称已验证外部模型端点。

风险与范围

  • 主要取舍:上传文件存储和显式引用元数据保留各自的归属与持久化契约。取消附件登记只移除元数据,已有文件字节仍可发现。不会静默登记或迁移历史文件。
  • 失败边界:录制写入器降级后,在会话恢复/重启前拒绝元数据写入;功能不会对仅保存在内存的修改报告成功。消息准入后的元数据补充及重试队列仍在浏览器内存中尽力执行。
  • 未验证或不在范围内:外部模型提供方、Windows/Linux 本地运行验收、新增独立 CLI 或 Python/Java 来源 API、独立 CLI 分叉中的跨客户端来源复制、使用状态跟踪,以及自动内容留存。
  • 兼容性:元数据记录和能力标识为新增能力,旧会话仍可访问上传文件;宿主旧 attachments 配置继续兼容。PDF 预览允许 Blob frame,并保留既有 daemon 端口的回环 frame 来源,没有增加外部 frame 域名。

关联 Issue

无。

@callmeYe callmeYe changed the title docs(web-shell): design session source registration feat(web-shell): add unified session sources Sep 7, 2026
@callmeYe
callmeYe marked this pull request as ready for review September 7, 2026 12:54
@callmeYe

callmeYe commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Local implementation and E2E acceptance

Implementation commit: a9be7a0a6115704885afb2cdadaafee4d212b255.

The product daemon, ACP child, tool scheduler, recording files, session APIs, and installed Chrome ran locally. Model replies came from a deterministic localhost fixture with a test-only credential; this does not claim external-provider acceptance. Only isolated test configuration and runtime directories were used.

Verification Result
Full repository build and typecheck Passed, including integration typecheck
Final staged Prettier and ESLint checks Passed through the normal commit hook
Source service, recording/history/JSONL and tool regressions Passed; 211-check focused group and 251 SessionService checks
Owner-aware daemon and capability regressions Passed; 78 focused checks
Bridge source transport and replacement-owner race Passed; 5 checks
SDK event/REST and Web Shell component/preview regressions Passed; UI integration tests verify the actual Sources-only attachment fetch and preserved HTML preview mode
Latest three-item boundary and disclosure regressions 33 environment-panel checks passed, including duplicate identity counting and 3 → 4 → 3
Unified Sources real Chrome flow 20/20 passed; zero page errors and zero frame CSP violations
Real capacity and long-name checks 3/3 passed: 200 registered records plus a 221-character uploaded filename remain 201 visible materials without changing metadata revision
Final visual checks 2/2 passed; original Chinese session has one Sources list and historical binary files remain visible
Final manual disclosure check Actual Chrome confirmed 3 → 10 → 3 with View all / Collapse and uniformly single-line rows

The real API/lifecycle runs also covered 44 registration/admission checks, 22 restart/fork/rewind/compaction/archive checks, and 19 malformed-snapshot/write-recovery/privacy checks. Later UI-only changes were followed by targeted component/browser checks and final full build/typecheck. These counts describe separate runs with overlapping coverage; they are not a single aggregate suite count.

Confirmed behavior includes historical attachment access without registration, metadata-title precedence and deduplication, registration removal without automatic metadata resurrection, inert HTML after tab restoration, visibly rendered PDF body text, and binary downloads whose bytes match the original upload. Failed source/attachment-list reads remain independently visible. The actual metadata Retry issued no additional prompt or attachment upload; a definite rejected prompt cleaned the uploaded bytes.

Declared fault injection: targeted metadata/list HTTP 503 responses, definite prompt HTTP 400 rejection, and a capability response without source support; all unaffected requests used the real daemon. Recording tests used only dedicated backed-up fixtures for an unsupported snapshot version and a truncated final JSON line, plus a temporary read-only recording to verify the existing degraded-writer recovery boundary.

Local evidence remains under .qwen/e2e-tests/ in the implementation worktree. The PR includes cropped current UI screenshots for review. macOS and Chrome were exercised; Windows/Linux runtime behavior and external models were not tested locally.

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 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 Sep 7, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head fe5ebb1. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

code-review-artifact-dark before/after

session-workflow-cockpit-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a substantial one, so here is where the gate lands before the code review.

Template ✓ Every required section is present and filled in, including a real reviewer test plan, the Tested-on table, and a full Chinese translation.

Problem. This is a feature rather than a bug fix, so there is no reproduction to ask for — but there is also no linked issue and no user report behind it. The motivation is a product judgement: the attachments list only covers uploaded files, while workspace references and links have no session-level list. That may well be right; it just is not evidenced by anyone asking for it. Worth saying plainly, because it changes who should make the call.

Direction. Web Shell is a first-class surface here, and folding uploaded files, workspace references and links into one list is coherent with how the environment panel already works. I checked the reference agent's CHANGELOG: no direct match for a unified session sources list, though session attachments and their restore robustness are an actively patched area — so the territory is relevant rather than off-mission. My concern is scale, not direction.

Size. Production logic 2,590 lines across 42 files; tests 2,166 across 17; docs/design 696 across 5; generated/schema 0. Of the production total, 779 lines across 12 files sit in core paths (packages/core/src/services, tools, config, utils, permissions), and the change spans five packages. The title is feat, so the large-core-refactor hard block does not apply and this is not rejected on size. But 500+ core production lines requires maintainer awareness, and at 2,590 the large-PR advisory applies as well: if any part of this can land separately, review would be considerably more effective.

Approach. Most of my simplification instincts were already answered by the diff, and I want to say so: the old attachments section and its row component are deleted rather than left sitting beside the new one, hosts that still ask for attachments keep working, and the new record_source tool is deferred, so it costs nothing in the base prompt. That is the right shape. The open question is the 80/20 one. The user-visible feature is the Sources list — a read route, a section component, and rows derived from attachments the session already has. What carries most of the lines and most of the risk is the rest: agent-side registration as a model-facing tool, a fork-time copy across sessions, and new public SDK surface. Have you considered landing the list first, with agent registration and fork copy as a follow-up? The list alone seems to cover the stated need.

Risk. One high-risk path matched: packages/cli/src/acp-integration/acpAgent.ts, which correlates with post-merge reverts in this repo. I reviewed at full depth and kept the enrichments. Alongside it: four new ACP extension methods, a new core service and core tool, new public SDK types, and a CSP widening (frame-src blob:). Each is examined individually in the review comment.

Escalating to a maintainer for awareness on size and product direction — a notification, not a rejection. Moving on to code review. 🔍

中文说明

感谢贡献!这个 PR 体量不小,先说明在进入代码审查之前 gate 的判断。

模板 ✓ 所有必填章节都完整填写,包含真实的 reviewer test plan、Tested-on 表格和完整中文翻译。

问题。 这是 feature 而非 bug fix,所以不需要复现——但也没有关联 issue 或用户反馈作为支撑。动机是一个产品判断:现有附件列表只覆盖上传文件,工作区引用和链接没有会话级清单。这个判断可能是对的,只是没有"有人提出过这个需求"作为证据。直说是因为这决定了该由谁来拍板。

方向。 Web Shell 在本仓库是一等公民,把上传文件、工作区引用和链接合并为一个清单,与 environment panel 现有形态是一致的。查了参考 agent 的 CHANGELOG:没有统一的会话来源清单的直接对应项,但会话附件及其恢复健壮性是持续在修的区域——所以这个领域是相关的,不算偏离主线。我的顾虑在于规模,不在于方向。

规模。 生产代码 2,590 行 / 42 个文件;测试 2,166 行 / 17 个;文档与设计 696 行 / 5 个;生成/schema 0 行。其中 779 行 / 12 个文件落在核心路径packages/core/src/servicestoolsconfigutilspermissions),改动横跨五个 package。标题是 feat,因此不触发大规模核心重构的硬性拦截,不会因为体量被拒。但 500+ 核心生产行需要维护者知悉,2,590 行也触发大 PR 提示:如果有部分可以单独落地,审查效率会高很多。

方案。 我原本想提的简化点,diff 里大多已经回答了,这点要明确说:旧的 attachments 分区和它的行组件是被删除的,而不是与新组件并存;仍然声明 attachments 的宿主继续可用;新增的 record_source 工具是 deferred 的,不占用基础 prompt。这个形态是对的。剩下的问题是 80/20:用户可见的功能是"来源"清单——一个读接口、一个分区组件、以及从会话已有附件推导出来的行。而承载大部分代码量和大部分风险的是其余部分:作为模型可见工具的 agent 侧登记、跨会话的 fork 复制、以及新增的 SDK 公开接口。有没有考虑先落地清单本身,把 agent 登记和 fork 复制作为后续 PR?单看清单似乎已经覆盖了陈述的需求。

风险。 命中一个高风险路径:packages/cli/src/acp-integration/acpAgent.ts,该路径在本仓库与合并后 revert 相关。因此按最大深度审查并保留了补充内容。同时涉及:四个新的 ACP extension method、新的 core service 与 core tool、新的 SDK 公开类型、以及 CSP 放宽(frame-src blob:)。这些都会在审查评论中逐项说明。

因规模和产品方向转交维护者知悉——这是通知,不是拒绝。进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head fe5ebb1, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

capabilities

field PR base (before) this PR (after)
features[] "session_sources"

health-deep-with-session

field PR base (before) this PR (after)
activeWorkStaleMs 5 6

Qwen Code · serve A/B

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Code review

I read the core service, the new tool, the daemon routes, the bridge plumbing, the ACP child handlers, the recording/restore path and the Web Shell surface. No critical blocker — nothing I can demonstrate as a correctness bug, a security hole or a regression. Three things are worth the author's attention, and a longer list of things I checked and found clean, because at this size knowing what was verified matters as much as what was not.

1. ACP error flattening turns authorization failures into 503. In acpAgent.ts, the catch around extMethodInternal returns a sourceError payload for all four qwen/session/sources/* methods. Only SessionSourceError keeps its own code; everything else — including the RequestError.invalidParams thrown for an untrusted parent, an invalid session id, or a malformed copy target — is mapped to source_persistence_unavailable. The bridge re-throws that as a SessionSourceError, and sendBridgeError maps that code to 503. Net effect: a rejected caller and a genuinely degraded recording writer look identical to the client, and a caller-side authorization problem is reported as server unavailability. It still fails closed, so this is fidelity rather than a hole, but it will send whoever debugs it in the wrong direction. Mapping RequestError to invalid_source (400), or preserving a distinct code, would fix it.

2. copyFrom replaces the target snapshot rather than merging. SessionSourceService.copyFrom ends in commit(copied), which writes copied as the entire snapshot. The only caller is the fork path, where the target session is new and the parent's snapshot record is excluded from the copied transcript — so nothing is lost today. The contract is silent about that precondition, though: a second call, or a call against a session that already registered something, would drop those records without a warning. Either a guard or a comment stating the fresh-target assumption would make this safe to reuse.

3. Every mutation costs one redundant refetch. useSessionSources.upsert and remove apply an optimistic update and then await refresh(). The service also fires qwen/notify/session/sources-changed after commit, the bridge publishes it as source_changed, the provider bumps sourcesVersion, and the hook's effect refreshes on that signal. So a successful mutation triggers two authoritative refetches. Harmless, but one is unnecessary.

What I checked and found clean:

  • jsonl.read() switched from parseLineTolerant to parseLineTolerantWithIntegrity. That utility has many callers well outside this feature, so I verified rather than assumed: base defines parseLineTolerant as parseLineTolerantWithIntegrity(...).records, so the recovered record set is byte-for-byte the same and only the complete flag is newly captured. No regression.
  • getSessionSourceService()'s Object.hasOwn guard reads like dead ceremony, but base config.ts already uses exactly this idiom for goalRuntime in five places. Consistent with the file, not novel complexity.
  • Route ownership (the thing most likely to go wrong here): the three new daemon routes reuse withOwnerReadSession / withOwnerMutableSession plus mutate({ strict: true }), and both mutations return 403 without a session-bound client id. That is live-session-owner scoped, matching the sibling artifacts routes rather than falling back to the primary runtime.
  • Cross-workspace redirect is guarded twice: readSessionSources rejects a session whose first record does not belong to the current project, and SourceDetail re-checks owner currency, session id, capability, workspace id and the recorded workspaceCwd before fetching any bytes.
  • Locator validation is tight: relative-only workspace paths with .. escape and Windows drive letters rejected, http/https only with no embedded credentials, control characters rejected, length caps on every field. Registration reads no file and fetches no URL, as described.
  • CSP frame-src blob: is narrower than it looks. Base already contained PdfAttachmentPreview, which creates a blob object URL and frames it — so this enables a path that already existed rather than introducing one. HTML is forced to previewKind='source' in both the tab view and the detail view, so untrusted HTML renders as text and never reaches a frame. No external origin is added.
  • No dead switches. sourceWarnings reaches a toast in App.tsx, retrySourceRegistration and attachmentsError / onRetryAttachments are rendered by SourcesSection, sourcesVersion drives the hook's refetch, and the session_sources capability gates both the hook and the detail view. Every added field has a real read site.
  • record_source passes shouldDefer: true, matching record_artifact's positional arguments exactly, and registration is gated on trusted managed parent + chat recording enabled + non-provisional workspace + not sdkMode + not a subagent. It does not leak into the standalone CLI prompt.
  • The fork poisoning path I went looking for does not exist: a forked transcript filters out session_sources_snapshot, so the parent's snapshot cannot be restored under the child's session id and fail id re-validation.

Test coverage is genuinely good — 58 added cases, and they aim at the right things: owner replacement, workspace change, malformed and future-version snapshots, revision monotonicity, blob URL revocation, HTML-as-text, fork id regeneration, and registration only after prompt admission.

sequenceDiagram
    participant P1 as Web Shell client
    participant P2 as Daemon session route
    participant P3 as ACP bridge
    participant P4 as ACP child agent
    participant P5 as SessionSourceService
    participant P6 as ChatRecordingService
    P1->>P2: POST sources with bound client id
    P2->>P3: upsertSessionSource owner scoped
    P3->>P3: validate input and attachment existence
    P3->>P4: ext method sources upsert
    P4->>P5: upsert validated input
    P5->>P6: append sources snapshot record
    P6-->>P5: append acknowledged
    P5-->>P4: new revision and source
    P4-->>P3: result
    P3-->>P2: result
    P2-->>P1: 200 with new revision
    P5-->>P4: notify revision after commit
    P4-->>P3: sources changed notification
    P3-->>P1: source_changed event
    P1->>P2: GET sources for authoritative list
Loading
Files changed (22 rows covering 31 of 64 files — production code only)
File What changed
packages/core/src/services/session-sources.ts New registry. Locator validation, id hashing, snapshot parse and restore, serialized upsert/remove/copy with a 200-record cap.
packages/core/src/tools/record-source.ts New deferred model-facing tool. Workspace files and URLs only, metadata only.
packages/core/src/config/config.ts Service factory plus getter, and lazy tool registration gated on a bound service.
packages/core/src/services/chatRecordingService.ts New snapshot record subtype and a strict append method.
packages/core/src/services/sessionService.ts Reads sources for a session with a workspace-match check, threads read integrity, and excludes snapshots from fork copies.
packages/core/src/services/session-transcript-reader.ts Selects the latest snapshot record during restore and live projection, degrades to unavailable on a truncated read.
packages/core/src/utils/jsonl-utils.ts Exposes the integrity-aware line parser and adds an incomplete-read callback.
packages/core/src/utils/transcript-records.ts Registers the subtype and keeps it out of the conversation view.
packages/core/src/utils/conversation-branches.ts Treats the snapshot as a neutral tail subtype.
packages/core/src/permissions/rule-parser.ts Permission aliases for the new tool name.
packages/cli/src/acp-integration/acpAgent.ts Four ext methods, the error-flattening catch, and the only place the service factory is wired.
packages/cli/src/serve/routes/session.ts Three owner-scoped routes. Mutations require a bound client id.
packages/cli/src/serve/server/error-response.ts Maps source error codes to 400, 404, 409 and 503.
packages/cli/src/serve/capabilities.ts Declares the new capability under the artifacts feature.
packages/cli/src/serve/web-shell-static.ts One line — adds the blob scheme to the frame source directive.
packages/acp-bridge/src/bridge.ts Three bridge methods, a source-error unwrapper, and source copying on branch and fork.
packages/acp-bridge/src/bridgeClient.ts Validates the child notification and publishes the invalidation event.
packages/acp-bridge/src/bridgeTypes.ts Interface additions and a warnings field on branched sessions.
packages/sdk-typescript/src/daemon/* Ten files — client methods, event type, normalizer case and exported source types.
packages/web-shell/client/components/panels/SourcesSection.tsx New unified list. Single-line rows, three-item disclosure, merged attachment and registered rows.
packages/web-shell/client/components/panels/EnvironmentPanel.tsx Deletes the old attachments section and its row component, delegates to the new one, keeps the legacy host option working.
packages/web-shell/client/components/artifacts/ArtifactPanel.tsx New source tab and detail view, blob preview for non-text non-PDF bytes, owner and workspace re-validation.
…and 33 more files Hook, actions, i18n, App and provider wiring, core index and tool-name exports, two design docs, one plan doc, two screenshots, and 17 test files.

Testing evidence

This is an unattended CI run, so I did not build, run or execute anything from this PR — the review above is static. The evidence below is the PR's own CI, read through the API for the reviewed commit. No tmux capture applies on this path.

Nothing has failed. The load-bearing legs are still running, so there is no green signal to report yet: the ubuntu unit suite, lint and static analysis, the serve A/B comparison, the no-AK integration tests, the real daemon E2E and the web-shell visual capture were all in progress at review time. What has completed is green — desktop shell on ubuntu and windows, live host on macOS, TUI parity snapshots, the no-flicker gate, and the Java matrix.

One gap worth naming: Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) are both skipped on this PR, and Integration Tests (CLI, No Sandbox) is skipped too. The unit-suite signal is therefore ubuntu-only, while the author's own acceptance was macOS-only with a deterministic localhost fixture standing in for the model. So no leg of this — CI or local — has exercised the feature on the platform the author tested, or on Windows at all.

Final CI results for a9be7a0 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Integration Tests (no-AK, No Sandbox) ❌ failure
Test (ubuntu-latest, Node 22.x) ❌ failure
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ❌ failure
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Live Host (macos-latest) ✅ success
macos-latest / Java 21 ✅ success
OpenTUI no-flicker gate ✅ success
Real daemon E2E / Java 11 ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
TUI parity snapshots (ink vs opentui) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle this: @qwen-code /verify — the claims that actually carry this PR are behavioural and none of them is observable from the diff. Specifically: that a registered workspace file resolves through the real daemon and bridge to the same bytes the owner-scoped read returns, that copyFrom produces target-session-scoped ids on a real fork rather than only against a mocked recording writer, and that the owner and workspace re-checks in the preview path fail closed when the owner is replaced mid-request. The PR's 58 new cases cover each of these against mocks, which is good but is not the same as the wire path. The author has write access, so this is a normal run rather than a sponsored one.

@qwen-code /tmux is the wrong lane here — the surface is the Web Shell in a browser, not the TUI. The Capture web-shell visuals job above is the equivalent visual lane and is still running.

Not verified: runtime behaviour on any platform, by me, for the reason stated above. The macOS acceptance results in the PR description are the author's claim, not independently re-run evidence.

中文说明

代码审查。 我读了 core service、新工具、daemon 路由、bridge 管线、ACP child 处理、录制与恢复路径以及 Web Shell 界面。没有发现关键阻断项——没有可以证明的正确性 bug、安全漏洞或回归。有三点值得作者关注,另外附上一份我确认过没问题的清单,因为在这个体量下,"哪些被验证过"和"哪些没有"同样重要。

1. ACP 错误扁平化会把鉴权失败变成 503。 acpAgent.ts 中包裹 extMethodInternal 的 catch 会为四个 qwen/session/sources/* 方法返回 sourceError。只有 SessionSourceError 保留自己的 code,其余全部——包括非受信父进程、非法 session id、非法 copy 目标抛出的 RequestError.invalidParams——都被映射为 source_persistence_unavailable。bridge 据此重新抛出 SessionSourceErrorsendBridgeError 把该 code 映射为 503。结果是:调用方被拒绝与录制写入器真的降级,在客户端看来完全一样,而调用侧的鉴权问题被报告为服务端不可用。它仍然是 fail-closed 的,所以这是准确性问题而非漏洞,但会把排查引向错误方向。把 RequestError 映射为 invalid_source(400)或保留独立 code 即可解决。

2. copyFrom 是替换目标快照,而不是合并。 SessionSourceService.copyFrom 最终调用 commit(copied),把 copied 写成整个快照。唯一调用方是 fork 路径,此时目标会话是新的、且父会话的快照记录已被排除在复制的 transcript 之外——所以今天不会丢数据。但这个前置条件没有写在契约里:第二次调用,或对已经登记过内容的会话调用,会无声地丢掉那些记录。加一个保护或一行注释说明"目标必须是新会话"会让它更安全。

3. 每次变更多一次冗余重新拉取。 useSessionSources.upsertremove 先做乐观更新,然后 await refresh()。同时 service 在提交后会发 qwen/notify/session/sources-changed,bridge 发布为 source_changed,provider 递增 sourcesVersion,hook 的 effect 又会据此刷新一次。所以一次成功变更会触发两次权威重新拉取。无害,但其中一次是多余的。

确认过没问题的部分:

  • jsonl.read()parseLineTolerant 换成 parseLineTolerantWithIntegrity。这个工具函数在本 feature 之外有大量调用方,所以我去核实而不是假设:base 中 parseLineTolerant 就是 parseLineTolerantWithIntegrity(...).records,因此恢复出的记录集合完全一致,只是多捕获了 complete 标志。无回归。
  • getSessionSourceService()Object.hasOwn 保护看起来像多余仪式,但 base 的 config.ts 已经用同样的写法处理 goalRuntime(5 处)。与文件既有风格一致,不是新引入的复杂度。
  • 路由归属(这里最容易出问题的点):三个新 daemon 路由复用了 withOwnerReadSession / withOwnerMutableSessionmutate({ strict: true }),两个变更操作在缺少会话绑定 client id 时返回 403。这是 live-session-owner 作用域,与同级 artifacts 路由一致,不会回退到 primary runtime。
  • 跨工作区改向有两层防护:readSessionSources 拒绝首条记录不属于当前项目的会话;SourceDetail 在读取任何字节前重新校验 owner 有效性、session id、capability、workspace id 以及记录的 workspaceCwd
  • 定位信息校验很严:工作区路径必须相对、拒绝 .. 逃逸和 Windows 盘符,URL 仅限 http/https 且不得含凭据,拒绝控制字符,每个字段都有长度上限。登记不读文件、不请求 URL,与描述一致。
  • CSP frame-src blob: 比看起来窄。 base 中已经有 PdfAttachmentPreview 会创建 blob object URL 并放进 frame——所以这是启用已存在的路径,而非引入新路径。HTML 在 tab 视图和详情视图都被强制为 previewKind='source',因此不可信 HTML 以文本渲染,永远进不了 frame。没有新增外部域名。
  • 没有死开关。 sourceWarnings 会走到 App.tsx 的 toast,retrySourceRegistrationattachmentsError / onRetryAttachmentsSourcesSection 渲染,sourcesVersion 驱动 hook 重新拉取,session_sources capability 同时约束 hook 和详情视图。每个新增字段都有真实读取点。
  • record_source 传入 shouldDefer: true,位置参数与 record_artifact 完全一致;注册受"受信托管父进程 + 开启录制 + 非临时工作区 + 非 sdkMode + 非子 agent"约束,不会泄漏到独立 CLI 的 prompt 里。
  • 我专门去找的 fork 污染路径不存在:fork 出的 transcript 会过滤掉 session_sources_snapshot,所以父会话的快照不会在子会话 id 下被恢复并因 id 校验失败而毒化状态。

测试覆盖确实不错——新增 58 个用例,而且对准了正确的位置:owner 被替换、工作区变更、损坏与未来版本快照、revision 单调性、blob URL 释放、HTML 以文本渲染、fork id 重生成、以及仅在 prompt 准入之后才登记。

测试证据。 这是无人值守的 CI 运行,因此我没有构建、运行或执行本 PR 的任何代码——以上审查是静态的。下面的证据来自 PR 自身的 CI,通过 API 针对被审查的 commit 读取。本路径不适用 tmux 抓取。

没有失败项。但关键几项仍在运行,所以目前还没有可报告的绿色信号:ubuntu 单元测试、lint 与静态分析、serve A/B 对比、no-AK 集成测试、real daemon E2E 和 web-shell 视觉抓取,在审查时都还在进行中。已完成的都是绿的——ubuntu 与 windows 的 desktop shell、macOS 的 live host、TUI parity 快照、no-flicker gate 以及 Java 矩阵。

有一个缺口需要点名:Test (macos-latest, Node 22.x)Test (windows-latest, Node 22.x) 在本 PR 上都是 skippedIntegration Tests (CLI, No Sandbox) 也是 skipped。因此单元测试信号只来自 ubuntu,而作者自己的验收只在 macOS 上做、且用本机确定性 fixture 代替模型。也就是说,无论 CI 还是本地,都没有在作者实际测试的平台上跑过这个功能,Windows 更是完全没有覆盖。

沙箱验证可以定论:@qwen-code /verify —— 本 PR 真正承载的主张都是行为性的,而且从 diff 里一个都看不出来。具体是:登记的工作区文件是否能通过真实 daemon 与 bridge 解析到与 owner 作用域读取相同的字节;copyFrom 在真实 fork 上是否生成目标会话作用域的 id,而不只是对着被 mock 的录制写入器成立;以及预览路径中的 owner 与工作区重新校验,在请求中途 owner 被替换时是否真的 fail-closed。PR 新增的 58 个用例对每一项都有覆盖,但都是基于 mock 的,这与真实链路不是一回事。作者有 write 权限,所以这是常规运行而非 sponsored run。

@qwen-code /tmux 在这里不是合适的通道——被审查的界面是浏览器中的 Web Shell,不是 TUI。上面那个 Capture web-shell visuals job 就是对应的视觉通道,目前仍在运行。

未验证:任何平台上的运行时行为,由我,原因如上。PR 描述中的 macOS 验收结果是作者的主张,不是独立复跑的证据。

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the review itself came out clean and I would not block this, but Stage 0 escalated it for maintainer awareness on size and core breadth, and that caps the score as a matter of policy rather than doubt.

Stepping back. Before reading the diff I wrote down what I would have done: extend the existing attachment record with optional metadata, render one list, and skip both the model-facing tool and the fork-time copy. The PR went further than that, and some of the extra scope is genuinely justified — keeping metadata durability separate from byte durability is a real requirement if a failed metadata write has to be retryable without resending a message, and a single snapshot record inside the existing recording service is simpler than the event-log or tombstone designs I had assumed it would need. Where my instinct held up is the rest: agent-side registration and cross-session fork copying are not needed to deliver "one place to reopen your materials", and they are where nearly all of the risk in this PR sits.

The honest answer to "does this solve something users care about" is probably, but nobody has asked for it. There is no linked issue and no user report. That is the single biggest reason this belongs with a human rather than with the gate — a polished 2,590-line product bet is exactly the shape that gets approved because it is polished.

On the code itself I have no reservations worth blocking on. It does not feel like it is trying too hard at the implementation level; the ambition is in the scope, not the style. The conventions are respected, the ownership scoping on the new routes is right, the validation is tighter than I expected, and the test suite aims at the failure modes that actually matter rather than at coverage numbers. The three findings in the review comment are all minor. If I had to maintain this in six months, the thing I would most expect to bite someone is copyFrom replacing the target snapshot instead of merging — correct today only because its single caller happens to pass a fresh session.

So: I am deferring, not approving, and not requesting changes.

Why this needs a human call — what I could not resolve from the diff, the tests and the description:

  1. Product need. No issue, no user report. Is a unified sources list wanted now, and at this scope?
  2. Scope sequencing. Should the list land first, with agent registration and fork copy as a follow-up? That split would make both halves far easier to review.
  3. Permanence of the new public surface. New exported SDK types and client methods, a new core tool, and four new ACP extension methods are hard to walk back once released. Is this the intended long-term contract, or an internal detail that just became public?
  4. CSP frame-src blob:. Low risk as implemented — it enables a blob PDF path that already existed, and untrusted HTML is forced to text — but a security-surface widening should be signed off by a person, not by a gate.
  5. Evidence. CI is still running (3 PR workflow runs pending; the ubuntu unit suite, lint and static analysis, serve A/B, the no-AK integration tests, real daemon E2E and the web-shell visual capture were all in flight at review time, and the macOS and Windows unit legs are skipped). The behavioural claims are covered by 58 new mock-based cases, which is good but is not the wire path — @qwen-code /verify is named in the review comment as the lane that would settle it.

One process note: I tried to resolve an accountable maintainer deterministically and got nothing back — no QWEN_MAINTAINER_HANDLE, no area label on this PR for the owner map to match, and no prior human review to fall back to. Rather than guess a login and notify the wrong person, I am leaving this unmentioned. Adding an area label and re-running @qwen-code /triage would route it to a named owner.

No approval is posted in this run, and no approve-on-green marker is attached, so the finalize job will not approve this automatically when CI lands either — the escalation has to be cleared by a human first.

中文说明

Confidence: 3/5 —— 审查本身是干净的,我不会阻断这个 PR,但 Stage 0 因规模和核心路径覆盖面把它升级为"需维护者知悉",按规则这会把评分上限压到 3/5;这是政策导致的上限,不是我真的存疑。

退一步看整体。在读 diff 之前我写下了自己会怎么做:在现有附件记录上扩展可选元数据、渲染成一个清单,并且不做模型可见工具、也不做 fork 时的跨会话复制。这个 PR 走得比我的方案更远,其中一部分额外范围确实是必要的——如果元数据写入失败后必须能在不重发消息的前提下重试,那么元数据的持久化契约与文件字节的持久化契约就必须分开;而在现有录制服务里写一条快照记录,也比我原先以为的事件日志或墓碑方案更简单。但我的直觉在其余部分依然成立:agent 侧登记和跨会话 fork 复制,对"给用户一个重新打开资料的入口"这个目标来说不是必需的,而这个 PR 几乎所有的风险都集中在它们身上。

对"这是否解决了用户在意的问题",诚实的回答是大概是的,但没有人提出过这个需求。没有关联 issue,也没有用户反馈。这正是它应该由人来判断、而不是由 gate 判断的最主要理由——一个打磨得很精致的 2,590 行产品决策,恰恰是最容易"因为精致而被批准"的形态。

对代码本身我没有值得阻断的保留意见。它在实现层面并没有用力过猛;野心体现在范围上,而不是写法上。约定被遵守,新路由的归属作用域是对的,校验比我预期的更严格,测试对准的是真正重要的失败模式而不是覆盖率数字。审查评论里的三条发现都很轻微。如果六个月后由我来维护,我最担心会出问题的是 copyFrom 替换目标快照而非合并——它今天正确,只是因为唯一的调用方恰好传入一个新会话。

所以:我选择 defer,既不批准,也不要求修改。

为什么需要人来拍板——无法从 diff、测试和 PR 描述中解决的:

  1. 产品需求。 没有 issue,没有用户反馈。现在是否需要统一的来源清单,是否需要做到这个范围?
  2. 范围切分。 是否应该先落地清单本身,把 agent 登记和 fork 复制作为后续 PR?这样切分之后两半都会好审得多。
  3. 新公开接口的不可逆性。 新导出的 SDK 类型与客户端方法、新的 core tool、四个新的 ACP extension method,一旦发布就很难收回。这是有意的长期契约,还是本来只是内部细节、现在变成了公开接口?
  4. CSP frame-src blob: 按当前实现风险很低——它启用的是已经存在的 blob PDF 路径,且不可信 HTML 被强制以文本渲染——但安全面的放宽应该由人签字,而不是由 gate 签字。
  5. 证据。 CI 仍在运行(3 个 PR workflow run 未完成;审查时 ubuntu 单元测试、lint 与静态分析、serve A/B、no-AK 集成测试、real daemon E2E 和 web-shell 视觉抓取都还在进行中,且 macOS 与 Windows 的单元测试 leg 是 skipped)。行为性主张由 58 个新增的基于 mock 的用例覆盖,这很好,但不等于真实链路——审查评论里点名了 @qwen-code /verify 作为可以定论的通道。

一个流程说明:我尝试用确定性方式解析出应负责的维护者,但没有得到结果——没有 QWEN_MAINTAINER_HANDLE,PR 上没有可供 owner map 匹配的领域标签,也没有可供兜底的历史人工 review。与其猜一个 login 去通知错的人,我选择不做 mention。补一个领域标签再重新运行 @qwen-code /triage,就能把它路由到具名负责人。

本次运行不会提交批准,也不会附带 approve-on-green 标记,因此 CI 变绿后 finalize job 也不会自动批准——这个升级必须先由人解除。

Qwen Code · qwen3.8-max-2026-09-02

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

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

本 PR 主要做了什么

在 web-shell 里把「上传文件 / 工作区文件引用 / HTTP(S) 链接」统一成一个 Sources 面板:新增一个只登记元数据(不读文件、不抓 URL、不进 prompt)的 record_source 工具与 GET/POST/DELETE /session/:id/sources 三个 owner 作用域 REST 接口,用带版本+revision 的 session_sources_snapshot 系统记录做持久化,走既有 chat-recording 严格落盘通道;acp-bridge 负责路由与 source_changed 失效事件;SDK 与 web-shell 提供 client 方法、事件归一(非对话气泡)与前端 hook/预览 tab。UI 侧用一个 Sources 分区替换旧的 Attachments 分区,按 attachmentId 去重、显式标题优先、默认三行可「查看全部/收起」,预览复用既有 owner/工作区信任校验,来源 HTML 强制按文本渲染、URL 不自动抓取。改动横跨 core / cli-acp / bridge / sdk / web-shell 五个包,并附了较完整的设计文档。

What this PR does

Unifies uploaded files, workspace-file references and HTTP(S) links into one web-shell "Sources" panel. Adds a metadata-only record_source tool (no file read / URL fetch / prompt injection) and three owner-scoped GET/POST/DELETE /session/:id/sources routes, persisted as a versioned session_sources_snapshot through the existing strict chat-recording append path. acp-bridge handles routing + a source_changed invalidation event; SDK/web-shell add client methods, non-transcript event normalization, a data hook and preview tab. UI replaces the old Attachments section, dedups by attachmentId with explicit-title precedence, 3-row view-all/collapse, reuses owner/workspace trust checks, forces source HTML to text, and never auto-fetches URLs. Spans core / cli-acp / bridge / sdk / web-shell, with a thorough design doc.

结论 / Verdict:工程完成度和契约对齐都很高,但本 PR 当前 CI 是红的,且有两处我核对过的真实缺陷,故以 COMMENT 发布(无 Critical)。


🔴/🟡 需要处理 / Must address

I-1 (Important) 三处「配套登记表」没跟着更新,直接导致 CI 漂移守卫失败

head a9be7a0aTest (ubuntu-latest)Integration Tests (no-AK)web-shell E2E Smoke 全部 fail。Test job 里失败的三个测试与本 PR 的新增一一对应,而我确认这三处配套文件都没出现在 diff 里:

  • src/i18n/index.test.ts > has a zh translation for every core tool display name:新增了 RECORD_SOURCE / 'RecordSource'tool-names.ts:65,123)但没有对应中文译名。本 PR 只改了 web-shell 的 i18n.tsx,没动 CLI/core 的中文工具名目录。
  • src/serve/capabilities-docs-contract.test.ts > documents exactly the conditional feature registry keys> keeps the daemon index capability counts in syncsession_sources 加进了 capabilities.ts,但没同步到条件能力文档登记表 / daemon index 计数。
  • src/serve/server/telemetry-catalog.test.ts > ... matches the explicit Express route registrations in both directions:新加了三个 /session/:id/sources 路由,却没在 telemetry catalog 注册。

(我本地 main 落后于合并基,没法直接跟真 main 逐条 diff,但三条失败测试的名字精确对应本 PR 的新增,且相应配套文件均未改动。)请把这三处配套登记补上,CI 应能转绿。

I-2 (Important) 附件元数据登记发生在「准入后取消/清理」之前,会留下坏状态

packages/web-shell/client/daemon/session/actions.ts:1327 先调 registerAcceptedAttachmentSources(...)void、不 await;其体内只守 sessionRef.current!==session,不守 signal.aborted),紧接着 :1332 才判断 if (options?.signal?.aborted),走 removePendingPromptremoveUploadedAttachments(清掉上传字节)→ 返回 removedAfterAbort

竞态后果二选一:(a) upsert 先落库、附件字节随后被清,留下一条指向已删除附件的持久化来源行(元数据与字节生命周期独立,取消登记不会删元数据);(b) upsert 后到 → daemon 返回 404 source_attachment_not_found → 用户明明取消了,却弹出「Message sent; some source details could not be saved」并带一个永远重试不成功的 Retry。设计里明确「取消(acceptance 之前)应什么都不登记」。修法:把 registerAcceptedAttachmentSources(...) 移到 abort 分支之后,即只有在会真正保留该 prompt(走 return { promptId }、未 removedAfterAbort)时才登记。

I-3 (Important,纵深防御) 来源 URL 的 <a href> 没走 isSafeHref

packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3044 直接 href={locator.url}:3047 openExternal(event, locator.url)。同文件里 artifact.url 那条路径是守的::2706 const safeUrl = isSafeHref(artifact.url) ? artifact.url : undefined;:2813/:2822isSafeHref 已在 :55 导入却在来源路径上没用。当前服务端 validateSessionSourceInputsession-sources.ts:157-163)与快照恢复校验(parseSessionSourcesSnapshot:226)都强制 http(s),所以正常流程不可达;但兄弟路径都防了、这条没防,面对可被编辑/损坏后回放的 transcript 快照,建议对齐:href={isSafeHref(locator.url) ? locator.url : undefined},非法时不渲染链接。

🟢 建议 / Nits

  • session-sources.ts:152-171locator.url 存成 parsed.href(会补尾斜杠/重编码,属规范化;但设计只禁止「靠重定向推断规范 URL」,query/fragment 保留,非破约);且把 >2048 的 href 长度校验并进了「must be HTTP(S) without credentials」这句措辞里,建议把超长单独报错。
  • session-sources.ts:455-488 copyFromcopied(仅父列表)整体 commit 而非合并目标已有列表;目前唯一调用方是 fork 到新会话(列表为空,无碍),属潜在问题。可加「目标非空则拒绝/合并」以策安全。
  • SourcesSection.tsx:371AttachmentRow 按钮只有 title、没有 aria-label(登记的来源行 :143aria-label),a11y 不一致。
  • i18n sources.remove 键定义了但没用到(设计本就禁止逐行删除),属死键。

🎉 做得好的地方 / Positives

  • 工具只接受 workspace_file|urlrecord-source.ts 的 oneOf 明确不含 attachment)且直接调本会话 service,无法绕过 daemon 侧「附件必须存在于本会话」的校验;只有 await upsert 成功才回执 ID。
  • 路由 owner 归属严谨:读用 withOwnerReadSession、写用 withOwnerMutableSession+strict gate,resolveOwnerSessionRuntime 从不回退到 primary(多工作区测试逐条覆盖 unknown/untrusted/ambiguous/replacing),归档会话拒绝变更,unknown 与越权附件返回同一个 404,不构成跨会话存在性探针。
  • 持久化走 appendRecordStrict(updateActiveTail:false),commit 仅在落盘 ack 后置为 live、失败置 loaded=false 强制重载且绝不 ack 易失成功;恢复只读最后一条,损坏/未来版本 → sourcesUnavailable 且不复活旧列表。
  • SDK 与 server 的 wire 类型逐字段对齐;source_changed 归为非对话 bookkeeping 事件;revision/owner 陈旧响应有守卫;准入后补登记不 await、不改 prompt body、Retry 不重传消息。
  • 预览严格按 owner + workspaceCwd 匹配、绝不把路径 rebase 到新 cwd;来源 HTML 强制文本、URL 不自动抓取;frame-src 只加 blob:,未引入外部 origin。

Reviewer note: static review only — no tests/build run; the three failing checks above are read from the Actions logs on head a9be7a0a, and each cited line is verified against that commit's tree.

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

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit of chunk 1 — the round-2 auditor never returned.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": whether the normal resume/ loadSession read path tolerates a torn JSONL tail that readSessionSources treats as complete:false → sourcesUnavailable , i.e. wh…; "agent reverse-audit (round 2)": whether the Web Shell client's connection.workspaceCwd follows a mid-session relocation, and whether the daemon canonicalizes a workspace path at session-crea…; "agent reverse-audit (round 1)": whether sessionArtifactsPersistenceAvailable — the toggle now gating session_sources — is actually derived from artifact-persistence availability or from th…; "agent reverse-audit (round 1)": the full record_artifact → record_source registry parity sweep — my repo-wide grep returned 122 matches and was truncated, so beyond rule-parser.ts:252-253,…; "agent reverse-audit (round 2)": the Compaction lifecycle row — I did not verify that a compaction preserves the latest session_sources_snapshot and keeps it out of model context and summarie…, and 7 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not reviewed: "agent verify (round 2)" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

中文说明

仅完成部分审查,审查缺口已披露。

未审查(原文为英文):reverse audit of chunk 1 — the round-2 auditor never returned.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"whether the normal resume/ loadSession read path tolerates a torn JSONL tail that readSessionSources treats as complete:false → sourcesUnavailable , i.e. wh…"agent reverse-audit (round 2)"whether the Web Shell client's connection.workspaceCwd follows a mid-session relocation, and whether the daemon canonicalizes a workspace path at session-crea…"agent reverse-audit (round 1)"whether sessionArtifactsPersistenceAvailable — the toggle now gating session_sources — is actually derived from artifact-persistence availability or from th…"agent reverse-audit (round 1)"the full record_artifact → record_source registry parity sweep — my repo-wide grep returned 122 matches and was truncated, so beyond rule-parser.ts:252-253,…"agent reverse-audit (round 2)"the Compaction lifecycle row — I did not verify that a compaction preserves the latest session_sources_snapshot and keeps it out of model context and summarie…,另有 7 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

未审查:"agent verify (round 2)"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread docs/design/web-shell-session-sources.zh-CN.md
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/cli/src/serve/capabilities.ts
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/core/src/services/session-sources.ts
Comment thread packages/web-shell/client/components/panels/SourcesSection.tsx Outdated
Comment thread packages/web-shell/client/components/panels/SourcesSection.tsx Outdated
Comment thread packages/web-shell/client/components/panels/SourcesSection.tsx
Comment thread packages/web-shell/client/daemon/session/actions.ts
Comment thread packages/web-shell/client/daemon/session/actions.ts

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

Partially reviewed — gaps disclosed.

23 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-9 copySources is the only best-effort child call in branchSession with no timeout — still stands; already reported (comment 3953036869, packages/acp-bridge/src/bridge.ts:11210)
  • R1-10 the fork source-copy path is untested end to end — still stands; already reported (comment 3953036874, packages/acp-bridge/src/bridge.ts:11243)
  • R1-6 the fork source-copy path is untested end to end (child side) — still stands; already reported (comment 3953036877, packages/cli/src/acp-integration/acpAgent.ts:10330)
  • R1-12 the only test of the sources trusted-parent gate cannot fail if the gate is deleted — still stands; already reported (comment 3953036878, packages/cli/src/acp-integration/acpAgent.test.ts:6864)
  • R1-14 the sources ext-method catch-all discards the original error twice over — still stands; already reported (comment 3953036886, packages/cli/src/acp-integration/acpAgent.ts:8505)
  • R1-11 a teardown failure in the finally block replaces the already-successful copyFrom result — still stands; already reported (comment 3953036890, packages/cli/src/acp-integration/acpAgent.ts:10373)
  • R1-60 the new source branch returns before any recordExpectedBridgeError call — still stands; already reported (comment 3953036893, packages/cli/src/serve/server/error-response.ts:290)
  • R1-64 the workspace_file guard applies Windows path syntax unconditionally — still stands; already reported (comment 3953036894, packages/core/src/services/session-sources.ts:121)
  • R1-22 the new fail-closed branch voids source restoration for the whole index and is untested — still stands; already reported (comment 3953036903, packages/core/src/services/session-transcript-reader.ts:2002)
  • R1-21 the needsDeferredDispatch disjunct for sourcesUuid is untested for the first-record shape — still stands; already reported (comment 3953036907, packages/core/src/services/session-transcript-reader.ts:2959)
  • R1-23 the widened restore/resume projections carry sourcesSnapshot with no production reader — still stands; already reported (comment 3953036908, packages/core/src/services/sessionService.ts:3094)
  • R1-24 conversation-branches.test.ts pins neutral-tail collapsing with hardcoded subtype lists and was not extended — still stands; already reported (comment 3953036912, packages/core/src/utils/conversation-branches.ts:21)
  • R1-25 the only negative case for source_changed normalization drops sessionId — still stands; already reported (comment 3953036916, packages/sdk-typescript/test/unit/SessionSources.test.ts:82)
  • R1-27 the prune removes source tabs without the panel bookkeeping both other prune paths perform — still stands; already reported (comment 3953036918, packages/web-shell/client/App.tsx:5034)
  • R1-85 the source-tab prune effect matches by id only and keeps the captured snapshot — still stands; already reported (comment 3953036919, packages/web-shell/client/App.tsx:5043)
  • R1-26 the prune effect depends on the whole artifactWorkspaceTarget object, rebuilt every render — still stands; already reported (comment 3953036922, packages/web-shell/client/App.tsx:5048)
  • R1-86 retry closures accumulate one per failed prompt-send and one click replays all — still stands; already reported (comment 3953036931, packages/web-shell/client/App.tsx:10190)
  • R1-87 sourcePreview is decided by the entry point rather than the attachment and is not part of the tab id — still stands; already reported (comment 3953036937, packages/web-shell/client/App.tsx:18627)
  • R1-100 the placeholder skeleton is now rendered in addition to the list, ungated on first hydration — still stands; already reported (comment 3953036945, packages/web-shell/client/components/panels/SourcesSection.tsx:181)
  • R1-101 the fallback title derived from the location is not clamped to the 200-character cap — still stands; already reported (comment 3953036951, packages/web-shell/client/components/panels/SourcesSection.tsx:255)
  • …and 3 more (see the run report)

3 candidate finding(s) this round's reviewers re-derived matched entries already carried on this PR and were set aside before verification (R1-60, R1-25, R1-101) — a matched posted finding is ruled in the previous-round status as always, and a matched deferral stays on the standing deferral record.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit findings R2-60, R2-61, R2-62 — the verifier never ruled on them.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": whether the workspace_file read site ( readWorkspaceFileAsBlob / readFileBytes in the web-shell SourcePreview path) applies a realpath or containment check…; "agent reverse-audit (round 2)": whether the fork path rewrites each record's sessionId field, which decides if readSessionSources 's restoreSessionSources(records, sessionId) (requested i…; "agent reverse-audit (round 2)": the "Reuse existing size limits and supported image/PDF/text previews" claim (doc line ~372) — the source preview lives inside ArtifactPanel.tsx (there is no …; "agent reverse-audit (round 2)": provenance of AttachmentRow 's use of attachment.attachmentId as the visible row label and as FileTypeIcon 's name (doc line ~337 promises "a single-line, t…; "agent reverse-audit (round 2)": the matrix rows "Conversation isolation" and "Compatibility" ("unrelated clients ignore metadata safely", "source notifications produce no transcript bubble") —…, and 15 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

[Critical] R1-4 The three new /session/:id/sources* route registrations were not added to legacySessionTelemetryRoutes (packages/cli/src/serve/server/telemetry.ts:45), so npm test --workspace="packages/cli" fails in src/serve/server/telemetry-catalog.test.ts — measured AssertionError: expected [ 'DELETE /session/:id', …(71) ] to have a length of 69 but got 72 at :101, exactly the three routes this diff registers, and the following expect(registered).toEqual(catalog) is also violated. This is net-new by measurement (the file fails on the PR side only against base 7dc5cc745). It is not only a stale count: daemonTelemetryMiddleware (telemetry.ts:858) calls resolveDaemonTelemetryRoute(req) and does if (!route) { next(); return; }, and that resolver matches only against legacySessionTelemetryRoutes (telemetry.ts:495), so a real GET/POST/DELETE on the new sources routes from the Sources panel produces no OTel span, no recordDaemonHttpRequest counter sample and no entry in the Daemon Status requests time-series — sources traffic is invisible to daemon telemetry while every neighbouring session route is measured. Fix: add three entries mirroring the artifacts block at telemetry.ts:191-207{ method: 'GET', path: '/session/:id/sources', attribution: 'handler_resolved', route: 'GET /session/:id/sources' } and the same for POST /session/:id/sources and DELETE /session/:id/sources/:sourceId. The fix must not violate packages/cli/src/serve/server/telemetry.test.ts:1094-1120 (currently green), which asserts expect(keys).toHaveLength(69), expect(new Set(keys).size).toBe(69), handler_resolved → 67 and pre_resolved → 2 with the pre_resolved set pinned to exactly ['POST /permission/:requestId', 'POST /session/:id/a2ui-action'] — so adding 3 entries requires 69→72 AND 67→70, the new routes must be handler_resolved, and route must be the exact "METHOD path" concatenation. Acceptance criterion: telemetry-catalog.test.ts > "matches the explicit Express route registrations in both directions" (already red) and telemetry.test.ts > "matches every catalog entry with its declared canonical attribution", which calls resolveDaemonTelemetryRoute for each catalog path and so pins that /session/session-1/sources actually resolves rather than merely being listed. Please prove it by mutation: remove the added catalog entries, run those tests, and confirm they go red. This Critical could not be anchored to a diff line — the route string '/session/:id/sources' appears in more than one added hunk position and the resolver refused to choose between them — so it is reported here in the body instead. 三条新的 /session/:id/sources* 路由注册没有被加入 legacySessionTelemetryRoutespackages/cli/src/serve/server/telemetry.ts:45),因此 npm test --workspace="packages/cli"src/serve/server/telemetry-catalog.test.ts 失败::101 实测 AssertionError: expected [ 'DELETE /session/:id', …(71) ] to have a length of 69 but got 72,正好是本 diff 注册的三条路由,随后的 expect(registered).toEqual(catalog) 也被违反。按实测这是本 PR 新引入的失败(相对合并基 7dc5cc745 只在 PR 侧失败)。这不只是计数过期:daemonTelemetryMiddlewaretelemetry.ts:858)调用 resolveDaemonTelemetryRoute(req) 并执行 if (!route) { next(); return; },而该解析器只匹配 legacySessionTelemetryRoutestelemetry.ts:495),所以来源面板发出的真实 GET/POST/DELETE 不会产生 OTel span、不会有 recordDaemonHttpRequest 计数样本、也不会出现在 Daemon Status 的请求时序里——来源流量对 daemon 遥测不可见,而相邻的每个会话路由都被度量。修法:仿照 telemetry.ts:191-207 的 artifacts 块补三条目录项。约束:telemetry.test.ts:1094-1120(当前为绿)断言 toHaveLength(69)new Set(keys).size 为 69、handler_resolved → 67、pre_resolved → 2 且该集合精确为 ['POST /permission/:requestId', 'POST /session/:id/a2ui-action'],因此新增 3 条要求 69→72 且 67→70,新路由必须是 handler_resolvedroute 必须是精确的 "METHOD path" 拼接。验收标准:上述两个测试用例。请用变异法证明:移除新增目录项,跑这些测试,确认变红。该 Critical 无法锚定到 diff 行——路由字符串 '/session/:id/sources' 出现在多个新增位置且解析器拒绝在其间做选择——因此改在正文中报告。

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 23 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

本轮评审重新推导出的 3 条候选发现与本 PR 已携带的条目匹配,已在验证前搁置(R1-60, R1-25, R1-101)——被匹配的已发布条目照常在上一轮状态区裁定,被匹配的延后条目仍保留在延后清单记录中。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查(原文为英文):reverse audit findings R2-60, R2-61, R2-62 — the verifier never ruled on them.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"whether the workspace_file read site ( readWorkspaceFileAsBlob / readFileBytes in the web-shell SourcePreview path) applies a realpath or containment check…"agent reverse-audit (round 2)"whether the fork path rewrites each record's sessionId field, which decides if readSessionSources 's restoreSessionSources(records, sessionId) (requested i…"agent reverse-audit (round 2)"the "Reuse existing size limits and supported image/PDF/text previews" claim (doc line ~372) — the source preview lives inside ArtifactPanel.tsx (there is no …"agent reverse-audit (round 2)"provenance of AttachmentRow 's use of attachment.attachmentId as the visible row label and as FileTypeIcon 's name (doc line ~337 promises "a single-line, t…"agent reverse-audit (round 2)"the matrix rows "Conversation isolation" and "Compatibility" ("unrelated clients ignore metadata safely", "source notifications produce no transcript bubble") —…,另有 15 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

[Critical] R1-4 The three new /session/:id/sources* route registrations were not added to legacySessionTelemetryRoutes (packages/cli/src/serve/server/telemetry.ts:45), so npm test --workspace="packages/cli" fails in src/serve/server/telemetry-catalog.test.ts — measured AssertionError: expected [ 'DELETE /session/:id', …(71) ] to have a length of 69 but got 72 at :101, exactly the three routes this diff registers, and the following expect(registered).toEqual(catalog) is also violated. This is net-new by measurement (the file fails on the PR side only against base 7dc5cc745). It is not only a stale count: daemonTelemetryMiddleware (telemetry.ts:858) calls resolveDaemonTelemetryRoute(req) and does if (!route) { next(); return; }, and that resolver matches only against legacySessionTelemetryRoutes (telemetry.ts:495), so a real GET/POST/DELETE on the new sources routes from the Sources panel produces no OTel span, no recordDaemonHttpRequest counter sample and no entry in the Daemon Status requests time-series — sources traffic is invisible to daemon telemetry while every neighbouring session route is measured. Fix: add three entries mirroring the artifacts block at telemetry.ts:191-207{ method: 'GET', path: '/session/:id/sources', attribution: 'handler_resolved', route: 'GET /session/:id/sources' } and the same for POST /session/:id/sources and DELETE /session/:id/sources/:sourceId. The fix must not violate packages/cli/src/serve/server/telemetry.test.ts:1094-1120 (currently green), which asserts expect(keys).toHaveLength(69), expect(new Set(keys).size).toBe(69), handler_resolved → 67 and pre_resolved → 2 with the pre_resolved set pinned to exactly ['POST /permission/:requestId', 'POST /session/:id/a2ui-action'] — so adding 3 entries requires 69→72 AND 67→70, the new routes must be handler_resolved, and route must be the exact "METHOD path" concatenation. Acceptance criterion: telemetry-catalog.test.ts > "matches the explicit Express route registrations in both directions" (already red) and telemetry.test.ts > "matches every catalog entry with its declared canonical attribution", which calls resolveDaemonTelemetryRoute for each catalog path and so pins that /session/session-1/sources actually resolves rather than merely being listed. Please prove it by mutation: remove the added catalog entries, run those tests, and confirm they go red. This Critical could not be anchored to a diff line — the route string '/session/:id/sources' appears in more than one added hunk position and the resolver refused to choose between them — so it is reported here in the body instead. 三条新的 /session/:id/sources* 路由注册没有被加入 legacySessionTelemetryRoutespackages/cli/src/serve/server/telemetry.ts:45),因此 npm test --workspace="packages/cli"src/serve/server/telemetry-catalog.test.ts 失败::101 实测 AssertionError: expected [ 'DELETE /session/:id', …(71) ] to have a length of 69 but got 72,正好是本 diff 注册的三条路由,随后的 expect(registered).toEqual(catalog) 也被违反。按实测这是本 PR 新引入的失败(相对合并基 7dc5cc745 只在 PR 侧失败)。这不只是计数过期:daemonTelemetryMiddlewaretelemetry.ts:858)调用 resolveDaemonTelemetryRoute(req) 并执行 if (!route) { next(); return; },而该解析器只匹配 legacySessionTelemetryRoutestelemetry.ts:495),所以来源面板发出的真实 GET/POST/DELETE 不会产生 OTel span、不会有 recordDaemonHttpRequest 计数样本、也不会出现在 Daemon Status 的请求时序里——来源流量对 daemon 遥测不可见,而相邻的每个会话路由都被度量。修法:仿照 telemetry.ts:191-207 的 artifacts 块补三条目录项。约束:telemetry.test.ts:1094-1120(当前为绿)断言 toHaveLength(69)new Set(keys).size 为 69、handler_resolved → 67、pre_resolved → 2 且该集合精确为 ['POST /permission/:requestId', 'POST /session/:id/a2ui-action'],因此新增 3 条要求 69→72 且 67→70,新路由必须是 handler_resolvedroute 必须是精确的 "METHOD path" 拼接。验收标准:上述两个测试用例。请用变异法证明:移除新增目录项,跑这些测试,确认变红。该 Critical 无法锚定到 diff 行——路由字符串 '/session/:id/sources' 出现在多个新增位置且解析器拒绝在其间做选择——因此改在正文中报告。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread packages/core/src/tools/tool-names.ts
Comment thread packages/core/src/tools/tool-names.ts
Comment thread packages/cli/src/serve/capabilities.ts
Comment thread packages/web-shell/client/daemon/session/actions.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/core/src/utils/transcript-records.ts
Comment thread packages/core/src/config/config.ts Outdated
@callmeYe

callmeYe commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up in a2b12d0, with current main incorporated by af2c846. The source lifecycle, cancellation, preview state, capability catalogs, translations and telemetry findings are addressed. In particular, standalone activation now binds sources, refreshes the model tool snapshot, and rebuilds the existing startup context after replay; a real provider request confirmed the default deferred record_source reminder without a forced reveal. The unified single-line Sources panel still starts at three entries and supports View all/Collapse, with historical attachments retained and metadata removal independent of file bytes.

Validation:

  • Full build, typecheck and bundle passed, along with ESLint, Prettier and required locale checks. The source-fix commit passed 2,728 focused unit tests. After merging main, the four complete affected Web Shell test files passed again: 1,098 tests, including 802 App tests.
  • Actual Chrome 152 against the real local daemon passed the final combined candidate's 20 unified-source checks, five desktop/narrow disclosure and single-line checks, and two standalone checks. This includes real uploaded text/image/PDF/binary/HTML previews, metadata deduplication/removal semantics, old-capability fallback, retry behavior, keyboard/focus/ARIA, Chinese labels, and the actual standalone model request. The model endpoint was an isolated local fixture; these results do not claim live external-model behavior.
  • Earlier unchanged API/lifecycle paths passed 44 API checks, the capacity matrix, 38 real-daemon capabilities integration tests, and real cwd/fork checks covering stable source IDs/revision, copied attachment bytes, regenerated child IDs, and independent metadata removal. The final combined bundle SHA256 is 73281ca4cd94b363af34c530189a87a383fa228f489cb4b54bd2780c6cf5446f; served HTML/JS/CSS matched the local artifacts before and after the final browser run. No page or CSP errors were observed.
  • The earlier browser CI failure was investigated separately: 22 retry traces contained 1,928 ERR_NETWORK_CHANGED resource-load failures, and 67 captured error pages showed the boot fallback. See the original CI artifact. This is separate from the reproduced and repaired code-contract failures; the newly pushed head is running fresh CI.

The review threads record explicit follow-ups for acknowledged copy cancellation/draining, unused restore-projection cleanup, host retry-notice aggregation, the portable path namespace, and a separate writer-error transport taxonomy. A plain timeout was deliberately not added around a mutation that may still hold the target writer. Required reviews and CI remain in force; this update is validation evidence, not a replacement for those gates.

@callmeYe
callmeYe dismissed stale reviews from qwen-code-ci-bot and qwen-code-ci-bot September 8, 2026 05:22

This automated review inspected 0b51688. All current 39 review threads are resolved through fixes, supported rebuttals or explicitly recorded follow-ups. The original Critical findings are fixed by a2b12d0, and the additional subagent/teammate source boundary is fixed by current head 4bdfaba. Full build/typecheck, focused regressions and real daemon/Chrome/provider-request verification pass. The human correctness concerns about catalogs, queued cancellation, and URL rendering are addressed. Dismissing this stale bot decision leaves required review and CI gates in force; it is not an approval.

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

Partially reviewed — gaps disclosed.

18 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • copySources awaits the child source-copy RPC with no timeout — already reported (comment 3953036869, R1-9); the author explicitly deferred a bounded cancellation/draining contract to a separate change
  • the sourcesSnapshot / sourcesUnavailable restore fields have no production reader — already reported (comment 3953036908, R1-23); the author explicitly deferred it as projection cleanup
  • the source ext-method catch-all sits above getSessionWriterError, flattening writer classifications — already reported (comment 3953036886, R1-14)
  • a malformed source_changed frame is dropped without a fallbackDebug diagnostic — already recorded (review 5145009229, round-3 deferral list, normalizer.ts:375)
  • an empty required field is reported to the model as a length overflow — already recorded (review 5145009229, round-3 deferral list, session-sources.ts:90)
  • SourceDetail hand-inlines the base64-to-Blob conversion the exported helper exists for — already recorded (review 5145009229, round-3 deferral list, ArtifactPanel.tsx:3013)
  • the download anchor omits its imageDownloadButton class — already recorded (review 5145009229, round-3 deferral list, ArtifactPanel.tsx:3172)
  • the new SDK source types were inserted between a JSDoc comment and the declaration it documented — already recorded (review 5145009229, round-3 deferral list, types.ts:1663)
  • the design doc internal child-method enumeration omits qwen/session/sources/copy — already recorded (review 5145009229, round-3 deferral list, web-shell-session-sources.md:261)
  • source_changed was added to REFRESH_APPEND_BOOKKEEPING_EVENT_TYPES but not to the it.each that pins it — already recorded (review 5145009229, round-3 deferral list, bridge.ts:1989)
  • the only writer of the snapshot record is executed by no test — already recorded (review 5145009229, round-3 deferral list, chatRecordingService.ts:3001)
  • the list-ordering contract is never asserted — already recorded (review 5145009229, round-3 deferral list, session-sources.test.ts:83)
  • the provider link of the invalidation chain is untested — already recorded (review 5145009229, round-3 deferral list, DaemonSessionProvider.tsx:5394)
  • the registration-failed Retry affordance is rendered by no test — already recorded (review 5145009229, round-3 deferral list, EnvironmentPanel.tsx:40)
  • the sourceDialogOpen dismiss suppression is unpinned — already recorded (review 5145009229, round-3 deferral list, EnvironmentPanel.tsx:204)
  • R3-2 the retry classifier decides permanence from a hand-listed set of three codes — still standing, already reported (comment 3960586024)
  • R3-3 the branch-session source-warning toast sits below the switchStarted early return — still standing, already reported (comment 3960586029)
  • R3-4 the beforeAdmit forwarding on the live-boundary branch is untested — still standing, already reported (comment 3960586040)

Not reviewed: reverse audit round 3 — stopped before round 3 by the review time budget.

Not reviewed: reverse audit rounds 1-2 candidate gaps — the convergence pair reported new candidates that no Step 4 verifier ruled on before the budget ran out, so none of them is part of this round's findings.

Not reviewed: test-efficacy — the probe harness produced no verdict (harnessValidated: null): its positive control ran and died before answering, so 0 mutants and 0 hunk-necessity probes were measured and every revert probe came back inconclusive.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) is outside the scoped npm test set, so integration-tests/cli/qwen-serve-routes.test.ts did not run in this review.

Not reviewed: build-and-test — packages/live-host is negated out of the npm workspace graph, so its own tsx --test suite (including the new package-manager.test.ts) was not executed here.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and their suites did not run locally, so the Windows path-normalization arms of the new locator validator were exercised on Linux only.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": the doc's "must not acquire an idle-only prompt gate or hold a prompt scheduling lock" — I read the qwen/session/sources/* ext-method case (acpAgent.ts:10276-…; "agent reverse-audit (round 1)": the body of the qwen/notify/session/sources-changed handler at bridgeClient.ts:2210 — I confirmed the method name is handled and that the normalizer requires …; "agent reverse-audit (round 1)": the "Archive | disable mutations under existing archive rules" row — I inferred it from mutate({ strict: true }) + withOwnerMutableSession on both mutation …; "agent reverse-audit (round 2)": the capability-advertising claim in implementation step 5 ("then advertise session_sources and expose the tool for supported sessions") — I never opened pack…; "agent reverse-audit (round 2)": the cross-client compatibility claim ("Python/Java SDKs, standalone CLI, other ACP clients, and Desktop receive no new public source API in this phase; their ex…, and 21 more.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/web-shell/client/App.tsx:5028 — [review] openSourcePanel does not check the same…
  • packages/core/src/services/session-transcript-reader.ts:1996 — [review] Source availability is gated on the integrity of *every*…
  • packages/core/src/services/sessionService.ts:2941 — [review] readSessionSources parses the whole live transcript into…
  • packages/core/src/services/sessionService.ts:2951 — [review] Both new trust-boundary throws in readSessionSources —…
  • packages/core/src/services/sessionService.ts:3885 — [review] forkSession 's new exclusion of session_sources_snapshot …
  • packages/core/src/tools/record-source.ts:86 — [review] The model-facing schema and tool description never say…
  • packages/core/src/services/session-sources.test.ts:124 — [review] Both scheme-rejection rows in the invalid-input list are…
  • packages/web-shell/client/hooks/useSessionSources.ts:113 — [review] The remove mutation path is unreachable from any…
  • packages/web-shell/client/hooks/useSessionSources.ts:84 — [review] upsert — the only user-reachable mutation in this hook —…
  • packages/web-shell/client/hooks/useSessionSources.test.tsx:96 — [review] The test named ignores an older revision and refreshes…
  • packages/web-shell/client/components/panels/EnvironmentPanel.tsx:189 — [review] Unlike the branch picker, the source dialog is not…
  • packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx:667 — [review] Retitling the section to "Sources" leaves the panel's only…
  • packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx:854 — [review] The Add-source dialog's url branch is never submitted by…
  • packages/web-shell/client/components/panels/SourcesSection.tsx:178 — [review] The loading placeholder is a verbatim second copy of…
  • packages/web-shell/client/components/artifacts/SourcePreview.test.tsx:135 — [review] The case named rejects a changed workspace and revoked…
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:1071 — [review] The new kind: 'source' tab variant has no branch in the…
  • packages/web-shell/client/daemon/session/actions.ts:2957 — [review] The sourceWarnings chain is tested only at its origin —…
  • docs/design/web-shell-session-sources.md:239 — [review] The doc promises the user-visible notice unconditionally…
  • packages/cli/src/serve/server.test.ts:24212 — [review] The new GET route test only exercises the…
  • packages/cli/src/serve/routes/session.ts:6328 — [review] The same nine-line client_id_required 403 block is…
  • …and 1 more (see the run report)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 18 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):reverse audit round 3 — stopped before round 3 by the review time budget.

未审查(原文为英文):reverse audit rounds 1-2 candidate gaps — the convergence pair reported new candidates that no Step 4 verifier ruled on before the budget ran out, so none of them is part of this round's findings.

未审查(原文为英文):test-efficacy — the probe harness produced no verdict (harnessValidated: null): its positive control ran and died before answering, so 0 mutants and 0 hunk-necessity probes were measured and every revert probe came back inconclusive.

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) is outside the scoped npm test set, so integration-tests/cli/qwen-serve-routes.test.ts did not run in this review.

未审查(原文为英文):build-and-test — packages/live-host is negated out of the npm workspace graph, so its own tsx --test suite (including the new package-manager.test.ts) was not executed here.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and their suites did not run locally, so the Windows path-normalization arms of the new locator validator were exercised on Linux only.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"the doc's "must not acquire an idle-only prompt gate or hold a prompt scheduling lock" — I read the qwen/session/sources/* ext-method case (acpAgent.ts:10276-…"agent reverse-audit (round 1)"the body of the qwen/notify/session/sources-changed handler at bridgeClient.ts:2210 — I confirmed the method name is handled and that the normalizer requires …"agent reverse-audit (round 1)"the "Archive | disable mutations under existing archive rules" row — I inferred it from mutate({ strict: true }) + withOwnerMutableSession on both mutation …"agent reverse-audit (round 2)"the capability-advertising claim in implementation step 5 ("then advertise session_sources and expose the tool for supported sessions") — I never opened pack…"agent reverse-audit (round 2)"the cross-client compatibility claim ("Python/Java SDKs, standalone CLI, other ACP clients, and Desktop receive no new public source API in this phase; their ex…,另有 21 条。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 21 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.1)

Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/core/src/services/session-sources.ts Outdated
@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Closed the current source security/bootstrap review findings and the CI regressions in cfa059c5fe.

Source metadata now rejects Unicode control and format characters while retaining significant filename whitespace. All three source operations use the established standalone activation guard; ordinary persistence failures keep their existing semantics. The history regression respects main's deferred-row loading and now proves response-time anchor recapture by mutation. The split-session tests use the renamed activity hook, and the HTML source/attachment preview test waits for its scheduled list load and asserts the button exists before clicking.

Validation: full locked install, build, typecheck and bundle passed; 871 focused unit/contract tests passed, including all 819 tests in the two previously failing WebShell files. The current original browser Smoke suite passed 58/58, and all three built-asset history cases passed at CPU6. All final browser/test runs used zero retries. The original failures, invalid-input red tests and the anchor callback mutation failure were retained as evidence; no assertion tolerance or timeout was relaxed.

The three Suggestion threads now explicitly record their remaining follow-up scope (general retry classification, session-scoped branch warnings, and an additional live-boundary mutation test). They are not claimed implemented. Valid Critical findings are fixed; fresh CI and required review remain authoritative for merging.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": the two lifecycle/ownership sentences "Archived-session mutations follow the shared archive coordinator's rejection" and "Persisted-only sessions must be resume…; "agent reverse-audit (round 2)": reading packages/core/src/services/session-sources.test.ts — both Fix witness fields above are stated as the test that must exist, not as a confirmed present-…; chunk 27: npm run typecheck for packages/web-shell was not run (the SDK dist/ output the typecheck resolves through is absent in this worktree); type-level conclusion…; "agent reverse-audit (round 1)": whether a failed retrySourceRegistrations (App.tsx:4056, which clears sourceRegistrationRetries *before* awaiting the closures) re-emits its notice.sourceR…; "agent reverse-audit (round 2)": whether attachmentsRoots.root passed at packages/cli/src/serve/server.ts:1153 and run-qwen-serve.ts:5498/6071/6749 can itself be undefined in the shipped da…, and 12 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not reviewed: verification — its prompt was built, but no agent was launched with it, so the posted findings cannot be counted as verified.

⚠️ 56 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/acp-bridge/src/bridge.ts:1988 — [review] The set exists so an idle non-turn event does not defeat the refresh-append of a queued turn-error terminal (isRefreshAppendTurnContent, bridge.ts:2056, consumed at :2377). Every other membe…
  • packages/acp-bridge/src/bridge.ts:11253 — [review] 'catch {' binds nothing, so for a channel death mid-copy (getTransportClosedReject(entry) rejecting), or a non-ENOENT readdir/stat failure thrown out of attachments.list() (sessionAttachmen…
  • packages/core/src/services/session-sources.ts:197 — [review] session-sources.test.ts:96 is titled 'keeps identity, creation time and order on metadata edits...' but asserts only id, createdAt, description, change and revision on a single-so…
  • packages/core/src/services/sessionService.ts:3092 (+3 locations) — [review] Grepping every read site of both identifiers across packages/*/src and integrations: the only non-test reads are session-sources.ts:349 and :354 inside SessionSourc…
  • packages/core/src/services/sessionService.ts:3885 — [review] sessionService.test.ts:6779 'drops creation metadata so the fork inherits no lineage or source' seeds parent_session and session_source records and asserts they are not copied, bu…
  • packages/sdk-typescript/src/daemon/types.ts:1663 — [review] Confirmed at the reviewed commit (git show HEAD:packages/sdk-typescript/src/daemon/types.ts, lines 1662-1667): the comment sits immediately above 'export type SessionSourceLocator'…
  • packages/web-shell/client/App.tsx:1868 — [review] Open a registered session source from the Sources section (openSourcePanel, App.tsx:5027) so it is the only right-panel tab, then reload. serializeArtifactPanelTabs yields [], so the layout …
  • packages/web-shell/client/App.tsx:5078 (+2 locations) — [review] useSessionSources.refresh() stores the freshly parsed {...result}, so every refresh yields new SessionSource object identities even when nothing changed; 'fresh !== tab.source…
  • packages/web-shell/client/App.tsx:10415 — [review] actions.test.ts:4768 asserts the notice carries a callable sourceRetry that re-issues the upsert without resubmitting the prompt. From there the chain is untested: this effect collects the …
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3013 — [review] The added code is an identical atob + byte-wise Uint8Array + new Blob([bytes],{type}) conversion; App.tsx:5493 and ChatPane.tsx:1047 both call the canonical ba…
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3072 — [review] .pdf is a member of DOWNLOAD_ONLY_EXTENSIONS (artifacts/artifactUtils.ts:94-101), so deleting the !isPdf conjunct makes every PDF source take the unsupported b…
  • packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx:988 — [review] The panel used to render t('environment.attachments') as the section heading; after this change the unified section is headed t('sources.title')==='Sources…
  • packages/web-shell/client/components/panels/EnvironmentPanel.tsx:203 — [review] Delete sourceDialogOpen from this early return and from the dependency array at EnvironmentPanel.tsx:230, and EnvironmentPanel.test.tsx still passes. Every poin…
  • packages/web-shell/client/daemon/session/DaemonSessionProvider.tsx:5394 — [review] useSessionSources re-fetches only when refresh identity or signals?.sourcesVersion changes (useSessionSources.ts:76-82). useSessionSources.test.tsx:28 drives…
  • packages/web-shell/client/daemon/session/actions.ts:1175 — [review] packages/web-shell/client/components/ChatPane.tsx:871 calls actions.sendPrompt(trimmed, {...files}) whenever the pane is idle, so this call site, not the queued one, is the…
  • packages/web-shell/client/daemon/session/actions.ts:2957 — [review] The design requires a target snapshot-write failure to be 'reported as sources not copied' without invalidating the fork, and the plan states warnings are visible 'for both…
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:1196 — [review] The new test (npx vitest run client/daemon/session/turn-navigation-store.test.ts -t 'pre-admission capture' -> 3 passed) must take the 'direction !== "newer" …
  • packages/web-shell/client/hooks/useSessionSources.ts:113 (+3 locations) — [review] Grepping the whole tree, the only caller of state.remove is the hook's own test (useSessionSources.test.tsx:133); no component references remove on the sourc…

Convergence: round 5 posted 5 inline comment(s), 5 of them reported for the first time; the previous round posted 3 (2 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"the two lifecycle/ownership sentences "Archived-session mutations follow the shared archive coordinator's rejection" and "Persisted-only sessions must be resume…"agent reverse-audit (round 2)"reading packages/core/src/services/session-sources.test.ts — both Fix witness fields above are stated as the test that must exist, not as a confirmed present-…;chunk 27:npm run typecheck for packages/web-shell was not run (the SDK dist/ output the typecheck resolves through is absent in this worktree); type-level conclusion…"agent reverse-audit (round 1)"whether a failed retrySourceRegistrations (App.tsx:4056, which clears sourceRegistrationRetries *before* awaiting the closures) re-emits its notice.sourceR…"agent reverse-audit (round 2)"whether attachmentsRoots.root passed at packages/cli/src/serve/server.ts:1153 and run-qwen-serve.ts:5498/6071/6749 can itself be undefined in the shipped da…,另有 12 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

未审查:验证——它的 prompt 已构建,但没有 agent 用它启动,发布的发现不能算作已验证。

⚠️ 循环结束时仍有 56 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 18 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 5 条行内评论,其中 5 条是首次提出;上一轮发布了 3 条(其中 2 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.1)

Comment thread packages/web-shell/client/components/panels/EnvironmentPanel.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/live-host/src/main/__tests__/package-manager.test.ts
Comment thread integration-tests/cli/qwen-serve-routes.test.ts
Comment thread packages/live-host/src/main/__tests__/package-manager.test.ts
@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Resolved the conflict with main 1919ff97f5 in the standalone entry point. The merge preserves main's browser notifications, authentication bootstrap and Plan action while retaining this PR's Sources entry. The existing entry configuration test now explicitly asserts that Sources is present.

Locked installation, full build, typecheck and bundle passed. Source/history tests passed 103/103 and entry/authentication/notification tests passed 48/48. An independent browser check of the actual standalone entry confirmed Sources and its add action after startup. The built-asset 200-record history scenario also passed at CPU6, retaining the reading row at 12px. Final browser assertions and timeouts are unchanged.

The current original Smoke suite passed all 58 cases with zero retries (2.0m), including main's updated transcript/composer edge alignment. No browser page errors were recorded.

@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the two reproduced round-5 UI defects: hiding the environment panel now closes its Add source portal through the existing dialog state callback, and standalone URL source details stay open without requiring workspace file authority. File previews still require their trusted workspace target and matching stored cwd; unavailable standalone file references show an explanation and perform no workspace reads. Unsafe URLs remain non-clickable.

Both defects were reproduced in an isolated Chromium browser before editing. The same two browser cases now pass with zero retries, including modal/body-pointer cleanup and the standalone Open original link. New React regressions cover floating/docked hidden panels, restored outside dismissal, standalone URL and unavailable workspace-file behavior, and unsafe URLs.

The collection Suggestions were checked against actual runners: Live Host job 102382259435 logs show the packaging dependency-tree regression ran and passed. The serve route E2E is explicitly collected by the dedicated E2E runner. Packaging dependency ownership is recorded as a separate maintenance follow-up; this UI fix does not restructure that package.

Full build/typecheck/bundle and lint passed. Final complete App/environment tests passed 868/868, and the original full Smoke suite passed 58/58 (9 workers, zero retries, 1.9m). The first Smoke run had two transient failures (catalog count and details popup bounds); both passed unchanged in a focused run and then in the final full run. The first failure logs and traces are retained; no product, assertion or timeout change was made for them.

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

Reviewing the overall trajectory rather than re-deriving the per-line findings: this PR has now run five review rounds (R1–R5), and each round has surfaced fresh Criticals — R5 at 07:37Z still opened two new ones (the EnvironmentPanel Add-source path and the standalone record_source path), fixed only in c817df5c8b at 08:07Z. That's the signature of a diff too large to converge: 6,831 additions across 88 files spanning core, ACP bridge, CLI serve, SDK, and Web Shell.

The individual fixes look responsive, but the fact that every pass discovers new certifies-falsely/fails-closed surface in a different layer suggests the review is sampling rather than covering. My recommendation is to split this into the source-model + persistence core and the Web Shell UI/panel layer (or at least pause until a full round lands with no new Critical), rather than continue pushing through single-fix rounds on a diff this size.

@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Acknowledged the convergence concern in review 5151984218. The current head is c817df5, with both R5 findings reproduced in Chromium and fixed, all 51 threads addressed, and its CI passing. Those results cover the fixes; they do not establish that the whole change has converged.

I will wait for the full review already running on this head (34327474704) and a round with no new Critical findings before pursuing merge. The old blocking reviews remain in place. If that full round still finds new Criticals across layers, I will bring back a concrete split proposal rather than treating another isolated fix as proof of convergence. No review gate is being dismissed or bypassed.

@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Following up on the convergence concern with concrete work on fe5ebb17a189905315a61dd8e959a344468f3371.

I reviewed the complete added/changed production logic across core persistence/tool activation, ACP ownership and copying, HTTP routes/error mapping, SDK events, and the WebShell state/preview paths. An independent test engineer reran the real daemon and browser workflows against isolated local fixtures. This was broader than the last changed lines, and it found one further real defect: an uploaded standalone attachment previewed normally until it was registered as a source, when the source preview incorrectly required workspace authority. That finding is fixed in this commit. Session-owned bytes now use the existing Blob/previewOnly path; workspace file references still require their trusted target and stored cwd, and HTML stays text-only. The new text/HTML regressions failed before the change and pass afterward, with revoked-owner access denied and no workspace reads.

Evidence and limits:

Area Result
Baseline real cross-layer acceptance 117 assertions passed: API/tool operations, actual daemon stop/start with exact list/revision recovery, fork/rewind, UI, write failure/recovery, corrupt/future snapshots, owner and invisible-character checks
Newly discovered attachment defect Real ordinary-versus-registered standalone control reproduced it before editing
Fixed standalone formats and boundaries 9 real checks passed: text, inert HTML including reopening after reload, PNG, PDF frame, exact binary download, URL no-prefetch and unavailable workspace files without reads
Complete related test files App + SourcePreview: 839/839 passed
Original full browser Smoke 58/58 passed, 9 workers, zero retries
Build/static checks Full build, typecheck, bundle and lint passed

The PDF observation establishes frame visibility and sizing, not exhaustive browser-PDF rendering. Held activation and every possible race are not claimed newly proven by real E2E; existing focused tests cover those gates. The post-fix delta was checked against the complete boundary map with no additional confirmed Critical. This is author-side review plus independent execution evidence, not a self-approval or a substitute for your formal judgment.

There is also a concrete split option, with per-file ownership mapped locally: runtime/API/SDK (56 files, 3,486 additions), WebShell UI (20 files, 2,527 additions), independent history-anchor follow-up (5 files, 98 additions), and the packaging regression separately (1 file, 22 additions); docs account for another 698 lines. Runtime comes first, UI depends on its contracts, and public history/test coverage would be preserved. Splitting is ready to plan from those boundaries rather than deleting tests to reduce the number. No replacement PR has been published without agreement.

Please re-review the repaired head against the full convergence concern. All 51 inline threads are addressed; the two old formal bot requests refer to commits 1673176 and b265680 and their findings have been fixed or explicitly dispositioned. The maintainer's broader convergence discussion remains visible and the required review/CI checks remain in force.

@callmeYe
callmeYe requested a review from yiliang114 September 9, 2026 09:46
@callmeYe
callmeYe dismissed stale reviews from qwen-code-ci-bot and qwen-code-ci-bot September 9, 2026 09:47

Superseded technical findings at 1673176/b2656809 are fixed or explicitly dispositioned on fe5ebb1; all 51 inline threads are addressed. Cc/Cf validation, standalone activation guards, viewport regression and source preview fixes have passing regression/E2E evidence. The author explicitly requested dismissal of stale bot requests. This is not an approval: a fresh maintainer review from yiliang114 was requested for the overall convergence concern, and required CI/review remain in force.

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

增量复审 / Incremental re-review

基线 → head:a9be7a0a6fe5ebb17a(本轮只走查该区间内属于本 PR 的提交,合并 main 的 merge commit 不计;引用的每一行都在 head fe5ebb17a 的树上重新核对过)。

Baseline → head: a9be7a0a6fe5ebb17a. This pass only walks the PR's own commits in that range (main-merge commits excluded); every cited line is re-verified against the head fe5ebb17a tree.

本 PR 主要做了什么 / What this PR does

把 web-shell 里「上传文件 / 工作区文件引用 / HTTP(S) 链接」统一成一个 Sources 面板:新增一个只登记元数据(不读文件、不抓 URL、不进 prompt)的 record_source 工具与 owner 作用域的 /session/:id/sources REST 接口,用带 version+revision 的 session_sources_snapshot 系统记录、经既有 chat-recording 严格落盘通道持久化;acp-bridge 负责路由与 source_changed 失效事件,SDK/web-shell 提供 client 方法、非对话事件归一、数据 hook 与预览 tab,UI 用 Sources 分区替换旧 Attachments 分区。横跨 core / cli-acp / bridge / sdk / web-shell 五个包。

Unifies uploaded files, workspace-file references and HTTP(S) links into one web-shell "Sources" panel: a metadata-only record_source tool (no file read / URL fetch / prompt injection) plus owner-scoped /session/:id/sources routes, persisted as a versioned session_sources_snapshot through the existing strict chat-recording append path. acp-bridge handles routing and a source_changed invalidation event; SDK/web-shell add client methods, non-transcript event normalization, a data hook and a preview tab; the UI replaces the old Attachments section. Spans core / cli-acp / bridge / sdk / web-shell.

结论 / Verdict:上一轮我提的三条 Important(I-1/I-2/I-3)在 head 上全部已修,CI 也转绿了。但我在 core 侧新核出两处 Important(错误分类/可观测性、来源读取的全量转录解析),故仍以 COMMENT 发布(无 Critical)。

All three Importants from my last round (I-1/I-2/I-3) are fixed at head, and CI is green. But I found two new Important issues on the core side (error classification/observability, and a full-transcript parse to read sources), so this stays a COMMENT (no Critical).


✅ 上一轮 findings 已解决 / Prior findings resolved

  • I-1(CI 红:三处配套登记表缺更新)已解决。 head 上 Test (ubuntu)Integration (no-AK)LintDesktop ShellCapture web-shell visualsweb-shell E2E Smoke 全绿(仅 bot 的 review-pr 还 pending)。提交 a2b12d047 补齐了 packages/cli/src/i18n/locales/{zh,zh-TW,en}.jsserve/server/telemetry-catalog.test.ts+telemetry.ts、以及 docs/developers/daemon/00-index.md+qwen-serve-protocol.md 的能力登记。
  • I-2(附件来源登记早于 abort 清理,留坏状态)已解决。 daemon/session/actions.ts 里登记调用已挪到 abort-removal 分支之后::1338 命中 signal.abortedremovePendingPrompt 成功时,先 removeUploadedAttachmentsreturn {removedAfterAbort:true}:1343-1344),从而跳过 :1363registerAcceptedAttachmentSources;另一条准入路径也在 :1107removeUploadedAttachments+throwIfAborted 再登记(:1174)。「acceptance 前取消 → 什么都不登记」的契约现在成立。
  • I-3(来源 URL 的 <a href> 未走 isSafeHref)已解决。 components/artifacts/ArtifactPanel.tsx:3054 现在用 {isSafeHref(locator.url) && (...)} 包住链接,:3057href:3060openExternal 仅在通过校验时渲染,与兄弟路径 artifact.url:2723)对齐。

🟡 本轮新发现 / New this round (Important)

R2-1 (Important) 四个 qwen/session/sources/* ext-method 把所有非 SessionSourceError 一律压成 source_persistence_unavailable,丢失错误分类且几乎无可观测性

packages/cli/src/acp-integration/acpAgent.ts:8492-8520 的 catch:

} catch (error) {
  if (['qwen/session/sources/list','.../upsert','.../remove','.../copy'].includes(method)) {
    if (!(error instanceof SessionSourceError)) debugLogger.error('[ACP] Session source ext-method error:', error);
    return { sourceError: error instanceof SessionSourceError
      ? { code: error.code, message: error.message }
      : { code: 'source_persistence_unavailable', message: 'Session source operation failed' } };
  }
  const writerError = getSessionWriterError(error);   // :8514 — 对 sources 方法永远到不了这里
  if (writerError) throw new RequestError(writerError.rpcCode, writerError.message, { errorKind: writerError.errorKind });
  throw error;
}

问题一:永久性的调用方错误被当成瞬时存储故障上报。 copy 处理器在目标不是本会话 fork 时 throw RequestError.invalidParams(...)acpAgent.ts:10348,另见 :10333 的 invalid copy target),这是 4xx 级的「你请求错了、重试也没用」;但它不是 SessionSourceError,于是被这个 catch 压成 code:'source_persistence_unavailable' —— 下游 serve/server/error-response.ts:283-284 把该 code 映射成 503。客户端由此无法区分「请求非法(永久)」与「存储挂了(可重试)」,会对一个永远不会成功的操作做重试。

问题二:真正的持久化故障没有任何诊断信息。 ENOSPC / EACCES / writer 拒绝等 5xx 级失败,同样落到 source_persistence_unavailable 这个泛化 message,底层 cause 只在 debugLogger.error 里(debug 构建才可见)。再叠加 services/session-sources.ts 自身的三处裸 catch {}:313 快照解析失败、:343 load 失败、:374 persist 失败)也都把 cause 丢掉换成泛化 SessionSourceError,整个 sources 子系统在生产环境几乎无法排障。

建议:在这个 catch 里把 RequestError(尤其 invalidParams)与 writer 错误放行到 :8514 的既有路径,只对真正的 SessionSourceError/意外异常返回 source_persistence_unavailable;并在 session-sources.tscatch {} 里保留 causenew SessionSourceError(code, msg) 附带 { cause: error } 或至少一条非 debug 级日志)。

The catch at acpAgent.ts:8492-8520 flattens every non-SessionSourceError thrown by the four qwen/session/sources/* ext-methods into { code: 'source_persistence_unavailable' } before getSessionWriterError (:8514) is ever reached. (1) Permanent caller errors get reported as transient storage failures: the copy handler deliberately throw RequestError.invalidParams(...) when the target is not a fork of this session (acpAgent.ts:10348, also :10333), a 4xx-class "your request is wrong, retrying won't help" — but it is not a SessionSourceError, so it collapses to source_persistence_unavailable, which serve/server/error-response.ts:283-284 maps to 503. The client can no longer tell "invalid request (permanent)" from "storage down (retryable)" and will retry an operation that can never succeed. (2) Genuine persistence failures (ENOSPC/EACCES/writer-rejected) surface with the same generic message and their cause only in debugLogger.error (debug builds), compounded by three bare catch {} in services/session-sources.ts (:313, :343, :374) that also discard the cause — leaving the whole sources subsystem near-undebuggable in production. Suggest letting RequestError/writer errors fall through to the existing :8514 path and preserving cause in the session-sources.ts catches.

R2-2 (Important) 读取会话来源要把整份转录 jsonl 全量解析一遍,只为 findLast 一条快照记录

packages/core/src/services/sessionService.ts:2941-2942

const { records, complete } =
  await jsonl.readLinesWithIntegrity<ChatRecord>(filePath, Infinity);

Infinity 上限意味着把整份会话转录逐行读出并 JSON.parse 成 ChatRecord[],随后 restoreSessionSources(records, sessionId)services/session-sources.ts:301)只做一次 records.findLast(type==='system' && subtype==='session_sources_snapshot') —— 即只需要最后一条快照记录,却物化了整份文件。

调用时机:acpAgent.ts:14203 bindSessionSourceService 把它接成 service 的 load(),在该会话首次触碰来源(打开 Sources 面板 / list / upsert / remove / copy)时触发一次,且前面还 await recording.flush()。放大因素:ACP 子进程是单线程事件循环、多会话复用同一个 child,一次大文件的全量 JSON.parse 会在这个循环上形成一个同步尖峰,stall 掉同一 child 上的其它会话;会话转录越大(长会话可达数 MB),首开 Sources 面板的这一下越明显。

这是「一次性、惰性」的成本,不是每条来源操作都付,所以严重度有限;但既然快照就是「最后一条 system 记录」,用从尾部反向的有界扫描(或复用 readRestoreProjection 那条已有的选择性读取路径)就能避免整份物化。

Reading a session's sources parses the entire transcript: sessionService.ts:2941-2942 calls readLinesWithIntegrity<ChatRecord>(filePath, Infinity), materializing every record, only for restoreSessionSources (session-sources.ts:301) to findLast the single latest session_sources_snapshot record. It runs once per session, on first sources access, via bindSessionSourceService's load() (acpAgent.ts:14203, after recording.flush()). The amplifier: the ACP child is a single-threaded event loop shared by multiple sessions, so a full-file JSON.parse is a synchronous spike that stalls every other session on that child, and it grows with transcript size (long sessions reach multiple MB). It is a one-time lazy cost, so severity is bounded — but since the snapshot is just "the last system record", a bounded reverse scan from the tail (or reusing the existing selective readRestoreProjection path) would avoid materializing the whole file.

🟢 建议 / Nits

  • services/session-sources.ts:489 copyFrom 仍以 commit(copied)(只含父列表)整体替换目标快照而非合并;当前唯一调用方有「目标必须是本会话 fork」的守卫(acpAgent.ts:10343)且 fork 初始为空,故实际安全,但若将来对已自行登记过来源的 fork 再次 copy,会静默丢弃子会话自己的来源。(上一轮已提,未变,留作潜在项。)
  • tools/record-source.ts:36 RecordSourceInvocation.execute() 以零参覆盖了基类签名,忽略了 AbortSignaltools.ts:154),in-flight 的元数据写入无法被取消;鉴于只是一次快速的快照 append,影响很小。getDefaultPermission 继承默认 'allow'tools.ts:117)不弹权限确认 —— 因为该工具只登记元数据、不读文件内容、且 validateSessionSourceInput 强制 workspacePath 为相对且不逃逸根目录,这个默认是站得住的,只是提醒这是有意为之。

🎉 做得好的地方 / Positives

  • 上一轮三条 Important 全部按建议修掉,且修法干净(登记挪到 abort 分支之后、isSafeHref 与兄弟路径对齐、配套登记表补齐使 CI 转绿)。
  • 持久化契约依旧稳:appendRecordStrict、commit 仅在落盘 ack 后置 live、失败置 loaded=false 强制重载、恢复只读最后一条、损坏/未来版本 → sourcesUnavailable 且不复活旧列表。
  • 路由 owner 归属、归档会话拒写、unknown 与越权附件同一 404(不构成跨会话存在性探针)、来源 HTML 强制文本、URL 不自动抓取 —— 这些安全边界在本轮增量里保持不变。

Reviewer note: 纯静态审查 —— 未跑测试/构建。CI 状态读自 head fe5ebb17a 的 Actions(除 bot 的 review-pr 仍 pending 外全绿)。每条引用的行号都在 head fe5ebb17a 的文件树上核对;core 侧文件(acpAgent.ts / sessionService.ts / session-sources.ts / record-source.ts / error-response.ts)自 c817df5c 起未变,head fe5ebb17a 仅改了 ArtifactPanel.tsxSourcePreview.test.tsx

Static review only — no tests/build run. CI read from Actions on head fe5ebb17a (all green except the bot's review-pr, still pending). Every cited line is verified against the head fe5ebb17a tree; the core files (acpAgent.ts / sessionService.ts / session-sources.ts / record-source.ts / error-response.ts) are unchanged since c817df5c, and head fe5ebb17a touched only ArtifactPanel.tsx and SourcePreview.test.tsx.

@callmeYe

callmeYe commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for rechecking the previous findings. I checked R2-1/R2-2 against fe5ebb1 and am recording both as follow-up work, consistent with the repository's late-review rule to land only Critical fixes after roughly five rounds. This is not a claim that either improvement has been implemented.

  • R2-1: preserving internal error causes and a more precise private-ACP error taxonomy is useful follow-up. One scope clarification: the invalid-copy example is a trusted private-parent operation; the bridge consumes its sourceError once and produces a copy warning rather than retrying that invalid target. Public invalid source input already receives invalid_source/400. The tested persistence failure contract remains a failed write with no success acknowledgement, followed by authoritative reload. A diagnostics follow-up should preserve useful causes without leaking private child paths through the HTTP response.
  • R2-2: confirmed that first source access still materializes the transcript once. The complete audit explicitly listed large-history performance as not newly benchmarked. A bounded/selective implementation should be benchmarked separately and preserve the integrity rule: a malformed or unsupported newest snapshot must fail closed rather than fall back to an older pre-removal snapshot. The existing corruption/restore tests and real recovery probes are the acceptance baseline for that optimization.

The copy-to-an-already-populated-fork and cancellation points are also retained as follow-up considerations, not silently marked fixed. Current user-facing Critical defects have been addressed with regression and real-browser evidence; fresh CI and the requested full/maintainer review remain the merge gates.

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

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D6-7 branch-time sourceWarnings toast unreachable below the switchStarted guard (packages/web-shell/client/App.tsx:11994) — already reported (round 5 deferral list, packages/web-shell/client/daemon/session/actions.ts:2957)

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and the suites ran only on Linux (locally and in CI), while this diff adds platform-dependent path handling.

Not reviewed: verification — 39 of 41 finder candidates and 49 of the convergence pair's 67 candidates were not put to a verifier within this round's time budget, so they are not reported as confirmed.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": the zh design doc's lifecycle-table rows in this chunk (compaction retaining the latest snapshot as session metadata, rewind carrying the latest snapshot into r…; "agent reverse-audit (round 1)": the zh doc's notification claims ( qwen/notify/session/sources-changed carrying session ID + revision, bridge republishing source_changed with the same field…; chunk 8: verifying the literal value of ToolDisplayNames.RECORD_SOURCE in core against the added toolDisplayName.RecordSource i18n keys (left to packages/cli/src/i1…; "agent reverse-audit (round 2)": whether the Web Shell's connection.workspaceCwd actually advances on a live cwd change (the last link making the stale record render "unavailable" rather than…; "agent reverse-audit (round 2)": my assigned diff read was offset=0, limit=8 (the two binary PNG entries only), while my brief carries a file-specific rule for docs/developers/qwen-serve-pro…, and 19 more.

⚠️ 40 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round; 2 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/core/src/services/session-sources.ts:85 — [review] Critical [fails-closed] [new-surface] Invisible-character gate rejects all of Unicode Cf, blocking ZWNJ/ZWJ input
  • packages/core/src/services/session-sources.ts:343 — [review] Critical [fails-closed] [new-surface] One latched recorder write failure permanently breaks read-only source listing
  • packages/web-shell/client/daemon/session/actions.ts:608 — [review] Mid-turn admission path never registers attachment sources
  • packages/web-shell/client/components/panels/SourcesSection.tsx:262 — [review] A source description can never be cleared from the Web Shell dialog
  • packages/web-shell/client/components/panels/SourcesSection.tsx:265 — [review] Source error surfaces render the raw REST route label to end users
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3073 — [review] Source preview classifies download-only from the file extension alone

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):reverse audit — stopped before round 3 by the review time budget.

未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and the suites ran only on Linux (locally and in CI), while this diff adds platform-dependent path handling.

未审查(原文为英文):verification — 39 of 41 finder candidates and 49 of the convergence pair's 67 candidates were not put to a verifier within this round's time budget, so they are not reported as confirmed.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"the zh design doc's lifecycle-table rows in this chunk (compaction retaining the latest snapshot as session metadata, rewind carrying the latest snapshot into r…"agent reverse-audit (round 1)"the zh doc's notification claims ( qwen/notify/session/sources-changed carrying session ID + revision, bridge republishing source_changed with the same field…;chunk 8:verifying the literal value of ToolDisplayNames.RECORD_SOURCE in core against the added toolDisplayName.RecordSource i18n keys (left to packages/cli/src/i1…"agent reverse-audit (round 2)"whether the Web Shell's connection.workspaceCwd actually advances on a live cwd change (the last link making the stale record render "unavailable" rather than…"agent reverse-audit (round 2)"my assigned diff read was offset=0, limit=8 (the two binary PNG entries only), while my brief carries a file-specific rule for docs/developers/qwen-serve-pro…,另有 19 条。

⚠️ 循环结束时仍有 40 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改;其中 2 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 6 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.1)

@wenshao

wenshao commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Local real-environment verification (maintainer pass) — head fe5ebb17a1

I built this PR and drove it as a product: a real qwen serve daemon, a real ACP child, real transcript files on disk, real Chrome against the built Web Shell bundle. No mocked daemon, no mocked bridge. Model replies came from a local deterministic OpenAI-compatible fixture; everything else (scheduling, storage, REST/SSE, browser) was the real thing.

Bottom line: the feature does what the description says, and its stated failure boundaries hold — nothing certified a wrong result and nothing silently lost a file in any probe I ran. Two of the recorded deferred Criticals reproduce live, and one of them blocks a legitimate everyday input (a workspace file whose name contains a Persian/Indic/emoji joiner). Neither is a data-loss or wrong-answer defect, so this is mergeable in my view, with the invisible-character gate worth narrowing before or right after merge.

Rig

Piece What was real
Daemon node packages/cli/dist/index.js serve --port 8931 --workspace <git repo>, isolated HOME, own trustedFolders.json
Web Shell bundle index-POJ2tBKO.js, served from packages/web-shell/dist (startup banner confirms), driven in Chrome
Transcripts real ~/.qwen/projects/.../chats/<id>.jsonl files, inspected and mutated on disk between runs
Model local deterministic SSE fixture (only the model is fake)
Build npm run build clean, npm run typecheck clean

Fixtures in the workspace: a Markdown file, an HTML file containing <script>window.__RIG_HTML_EXECUTED = true;</script>, a 64×64 PNG, a one-page PDF with visible text, an application/octet-stream blob, plus two files whose names contain U+200C / U+200D.

Reviewer test plan — reproduced

Plan item Result
One Sources section, dedupe by attachment ID, explicit title wins, 3 rows + View all/Collapse, count correct ✅ 10 registered + 4 uploaded → header Sources 13; uploaded-diagram.png appears once, as its explicit title "Architecture diagram (explicit title)"; collapsed 3 → expanded 13 → collapsed 3
Workspace-file + HTTP(S) registration: identity/createdAt preserved, identical retry does not bump revision, edits keep ordering, invalid locators rejected, 200-cap applies to metadata only ✅ create → revision 1 / created; identical retry → revision 1 / unchanged; title edit → revision 2 / updated with createdAt unchanged. Rejected: absolute path (400), ../.. escape (400), file:// (400), https://user:pw@… (400), unknown field (400), unknown attachment (404). Cap: 200 accepted, 201st → 409 source_limit_reached, and the UI still showed Sources 201 because the uploaded file stays listed
Previews: PDF body renders, binary bytes intact, HTML stays text after refresh, links not fetched, owner cannot redirect preview ✅ PDF renders its page text; all 4 attachment downloads byte-identical to source (sha256); HTML renders as literal <h1>/<script> text, no iframe, window.__RIG_HTML_EXECUTED still undefined after a full page reload; a URL source pointed at my own HTTP server got 0 requests. ⚠️ owner-redirect case not attempted
Attachment send + injected metadata failure: message/file usable, Retry adds no upload or prompt, removing only the registration keeps the bytes ✅ send with a degraded recorder → message answered, file uploaded and listed, banner "Message sent; some source details could not be saved · Try again"; Retry left the transcript byte-identical and the attachment count unchanged. Deleting only the registration → removed:true, attachment bytes still served, nothing auto-re-registered
Restart/resume, rewind, compact, fork; malformed/future snapshots ✅ daemon restart + resume → revision and all rows intact; rewind (rewound:true, truncatedCount:6) and /compress ("Context compressed (10 -> 10)") both left registered metadata untouched; fork copied all 10 sources with fresh ids, preserved createdAt, revision reset to 1, and carried all 4 attachments into the fork (spot-checked byte-identical), and later writes to the fork did not touch the parent; archived session → 404 on GET/POST/DELETE. A hand-planted version: 2 snapshot → 503 sources unavailable, old list not resurrected, while attachments listed 200 and the conversation loaded and rendered normally
Older daemon + independent list failures ✅ with session_sources removed from the capability registry (140 features), the Add-source button disappears, no registered rows, no error, and all 5 uploaded files stay listed and openable. With the source list failing and attachments healthy, the panel shows the source error inline with its own Try again and keeps the uploaded files

Screenshots for each of these are in the gallery below.

The two deferred Criticals both reproduce

1. session-sources.ts:85 — the \p{Cf} gate rejects real files, not just decorative characters.

invisible character gate

The finding as recorded says "blocking ZWNJ/ZWJ input", which reads like a cosmetic title restriction. Live it is stronger: text() also validates workspacePath, so a file that exists in the workspace cannot be registered at all if its name contains U+200C or U+200D — which is ordinary for Persian and Indic filenames and for any emoji ZWJ sequence. In the UI the failure is also mis-attributed: the Add-source dialog derives the title from the filename, so pasting the path first fails with "Invalid title", and only after you type an ASCII title do you get the real reason, "Invalid workspacePath".

Add source, path only Add source, with an ASCII title
invalid title invalid workspacePath

It is genuinely fail-closed (nothing is stored, nothing wrong is certified), so it is not a merge blocker — but the cheap fix is to keep \p{Cc} plus the specific format characters worth banning (the bidi overrides U+202A–U+202E and U+2066–U+2069, and U+FEFF) instead of all of \p{Cf}.

2. session-sources.ts:343 — one write failure latches the read-only listing, and repairing the disk does not clear it.

latched source listing

The PR's stated failure boundary is "a degraded recording writer rejects metadata writes until session recovery/restart". Live, the read side goes down with it, and it stays down after the cause is gone: commit() sets loaded = false on a persist failure, so the next list() re-enters load(), which does await recording.flush() — and ChatRecordingService.writeFailure is a permanent per-process latch that pre-dates this PR. Neither repairing the file nor POST /session/:id/resume clears it; only a daemon restart does.

The user-visible shape of this is the one flow the test plan already asks about — and it is where the two findings meet:

After the injected failure After the disk is repaired, pressing Try again
retry banner retry still fails

Retry can never succeed, and pressing it adds a second, permanent error to the panel. Nothing is lost — the uploaded file stays listed and the restart-time list is correct (no false success) — but "Try again" is offering an action that cannot work. A read-only list() could serve the in-memory snapshot, or load() could skip flush() when it is only reading.

On the standing round-6 Importants

  • R2-1 (error classification). Through every REST route I could reach, the mapping is correct: invalid_source→400, source_limit_reached→409, source_attachment_not_found→404, source_persistence_unavailable→503. The 4xx-flattened-to-503 case is confined to qwen/session/sources/copy, which has no REST route and is only reachable from the bridge's fork path — so the user-facing blast radius today is smaller than the finding implies. The second half of R2-1 is real and I hit it repeatedly: the daemon log shows only the generic message, never the underlying EACCES.
  • R2-2 (full transcript parse). Measured rather than argued:

transcript read cost

One cold read per session at ≈1.7 ms/MB (219 ms at 129 MB), warm reads flat at ~2 ms, and transient ACP-child RSS that tracks file size and does not settle back (+123 MB at 129 MB). The "synchronous spike stalls other sessions on the same child" part does not reproduce: the read is a streaming readline loop with an await per line, and a second session on the same child polled 60× during the 61 MB cold read saw p90 1.1 ms / max 2.5 ms. So: worth a tail-bounded read as follow-up, not a blocker.

Mutation matrix

mutation matrix

8 of 10 killed. Both survivors are defence-in-depth rather than the primary path: M4 is the parse-side 200 cap, only reachable via a hand-edited or corrupted snapshot because the write path caps first; M8 drops workspaceCwd from sessionSourceId, which changes the hash but which I could not turn into a live divergence, since a session's project root is fixed for its lifetime. Worth one test each, not worth blocking on.

Local suite results at this head

Suite Result
npm run build / npm run typecheck ✅ clean
core (session-sources, record-source, session-transcript-reader, session-source-tool-activation, conversation-branches) ✅ 202/202
acp-bridge (session-sources + bridge) ✅ 933/933
sdk-typescript SessionSources ✅ 2/2
cli serve (server, error-response, telemetry, telemetry-catalog, web-shell-static, multi-workspace-sessions) ✅ 1539/1539
web-shell, full client suite ✅ 294 files / 6912 tests

One earlier acp-bridge run reported 2 failures; that run overlapped the 6912-test web-shell run on the same machine and it is green on a clean re-run — load flake, not a real failure.

Smaller things I noticed

  • Error strings render route templates to end users: "GET /session/:id/sources: Stored sources are unavailable" and "POST /session/:id/sources: Invalid title" in the panel and the Add-source dialog.
  • Opening a workspace-file binary shows a stale "Loading file…" line above GET /file: binary file: /abs/host/path/..., with no download affordance — the uploaded-attachment path for the same bytes offers a clean "Preview is not available for this file type" card with a download button. The absolute host path also comes through verbatim.
  • At the 200-source cap, sending an attachment silently does not register it (source_limit_reached is deliberately treated as permanent and dropped from the retry queue). Nothing is lost — the file still shows as an uploaded file — but there is no signal at all. Fine as designed; flagging in case the silence was not intended.

Not verified

Owner-unavailable / owner-replaced preview redirection; "a definite prompt rejection cleans the uploaded bytes"; cross-client source copying through the standalone CLI; Windows and Linux (macOS only); external model providers. Compaction was exercised via /compress on a small transcript, so it proved "registered metadata survives compaction" but not the large-context compaction path.

On the convergence concern in review 5151984218

For what it is worth as a data point: this pass was independent of the review rounds — real daemon, real browser, mutation matrix, failure injection at the filesystem — and it surfaced no new Critical. It reproduced the two already-recorded deferred ones and sharpened their reachability, and everything else it found is a nit or a follow-up. That does not settle the diff-size argument, but it is one full pass over the feature's behaviour that did not open a new fails-closed surface.

Screenshot gallery (Web Shell, real daemon)

Collapsed — default 3 rows, total 13 Expanded — all 13, dedup + explicit title
collapsed expanded
Workspace PDF — body actually renders Workspace HTML — stays literal text, script never runs
pdf html
Binary attachment — no preview, download offered Source list down, attachments healthy — independent errors
binary independent

Older daemon (capability removed) — no Add-source affordance, uploads still listed:

older daemon

Evidence images: wenshao/qwen-code@assets-pr11262 · rig details reproducible from the tables above.

中文说明

本地真实环境验证(维护者复核)— head fe5ebb17a1

我把这个 PR 构建出来当产品来跑:真实的 qwen serve daemon、真实 ACP 子进程、磁盘上真实的转录文件、真实 Chrome 打真实的 Web Shell 产物。没有 mock daemon,没有 mock bridge。只有模型回复来自本机确定性的 OpenAI 兼容 fixture,其余(调度、存储、REST/SSE、浏览器)全部是真的。

结论:功能与描述一致,声明的失败边界成立——我跑过的所有探针里,没有任何一次给出错误结果、也没有任何一次静默丢文件。两条被记为「推迟」的 Critical 都能在活体上复现,其中一条会挡住一个完全正常的日常输入(文件名里带波斯语/印地语/emoji 连接符的工作区文件)。两条都不属于数据丢失或错误结论,所以我认为可以合入,但那个不可见字符门建议在合入前后尽快收窄。

验证台

部件 真实性
Daemon node packages/cli/dist/index.js serve --port 8931 --workspace <git 仓库>,隔离 HOME,独立 trustedFolders.json
Web Shell 产物 index-POJ2tBKO.js,由 packages/web-shell/dist 提供(启动横幅可证),在 Chrome 中操作
转录 真实的 ~/.qwen/projects/.../chats/<id>.jsonl,在各轮之间直接读取和篡改
模型 本机确定性 SSE fixture(只有模型是假的)
构建 npm run buildnpm run typecheck 均通过

工作区夹具:Markdown 文件、含 <script>window.__RIG_HTML_EXECUTED = true;</script> 的 HTML、64×64 PNG、带可见文字的单页 PDF、application/octet-stream 二进制,另加两个文件名里含 U+200C / U+200D 的真实文件。

审阅验证计划 — 复现结果

计划条目 结果
只有一个「来源」分区、按 attachment ID 去重、显式标题优先、默认三条 + 查看全部/收起、总数正确 ✅ 10 条登记 + 4 个上传 → 标题 Sources 13uploaded-diagram.png 只出现一次,显示为显式标题「Architecture diagram (explicit title)」;收起 3 → 展开 13 → 再收起 3
工作区文件与 HTTP(S) 登记:身份/创建时间保留、相同重试不涨 revision、编辑不改排序、非法定位被拒、200 上限只约束元数据 ✅ 创建 → revision 1 / created;相同重试 → revision 1 / unchanged;改标题 → revision 2 / updatedcreatedAt 不变。被拒:绝对路径(400)、../.. 逃逸(400)、file://(400)、https://user:pw@…(400)、未知字段(400)、未知附件(404)。上限:接受 200 条,第 201 条 → 409 source_limit_reached,而界面仍显示 Sources 201,因为上传文件照常列出
预览:PDF 正文渲染、二进制字节一致、刷新后 HTML 仍为文本、链接不自动抓取、归属不可改向 ✅ PDF 渲染出页面文字;4 个附件下载 sha256 与源文件逐字节一致;HTML 渲染为字面 <h1>/<script> 文本,无 iframe,整页刷新后 window.__RIG_HTML_EXECUTED 仍是 undefined;指向我自建 HTTP 服务的 URL 来源收到 0 次请求⚠️ 归属改向那条未尝试
发送附件 + 注入元数据失败:消息与文件仍可用、重试不增加上传或 prompt、仅删除登记后字节仍在 ✅ 在录制器降级状态下发送 → 消息拿到回复、文件上传并列出、出现横幅「Message sent; some source details could not be saved · Try again」;点重试后转录逐字节不变、附件数量不变。仅删除登记 → removed:true,附件字节仍可下载,且不会自动重新登记
重启/恢复、回退、压缩、分叉;损坏/未来版本快照 ✅ daemon 重启 + resume → revision 与全部条目完好;回退(rewound:truetruncatedCount:6)与 /compress("Context compressed (10 -> 10)")都没有动登记元数据;分叉复制了全部 10 条来源、ID 全部不同createdAt 保留、revision 归 1,4 个附件全部带入分叉(抽查逐字节一致),之后写分叉不影响父会话;归档会话对 GET/POST/DELETE 一律 404。手工植入 version: 2 快照 → 503 sources unavailable不会复活旧列表,同时附件列表 200、对话正常加载渲染
旧 daemon 与两侧失败独立 ✅ 把 session_sources 从能力表移除后(140 项),新增来源按钮消失、无登记条目、无报错,5 个上传文件照常列出并可打开。来源列表失败而附件正常时,面板单独显示来源错误并带自己的重试,同时保留上传文件

收起/展开/PDF/HTML 文本化/二进制/降级/旧 daemon 的截图见文末链接。

两条推迟的 Critical 都能复现

1. session-sources.ts:85\p{Cf} 门挡的不只是装饰字符,而是真实文件。

记录里写的是「阻止 ZWNJ/ZWJ 输入」,读起来像是标题的装饰性限制。活体上更强:text() 同样校验 workspacePath,所以只要文件名含 U+200C 或 U+200D,工作区里真实存在的文件根本无法登记——而这在波斯语、印地语文件名和任何 emoji ZWJ 序列里都是常态。界面上报错还归错了字段:新增来源对话框会从文件名推导标题,所以只粘路径先报 「Invalid title」,再手填一个 ASCII 标题才会看到真正原因「Invalid workspacePath」。

这确实是 fail-closed(什么都没存、没有认定错误结果),所以不是合并阻断项——但便宜的修法是保留 \p{Cc} 再加上确实该禁的少数格式字符(bidi 覆盖 U+202A–U+202E、U+2066–U+2069 以及 U+FEFF),而不是整个 \p{Cf}

2. session-sources.ts:343 — 一次写失败会闩死只读列表,修好磁盘也解不开。

PR 声明的失败边界是「录制写入器降级后,在会话恢复/重启前拒绝元数据写入」。活体上读也一起挂,而且原因消失后依然挂着:commit() 在持久化失败时把 loaded = false,于是下一次 list() 重新进入 load(),而 load()await recording.flush()——ChatRecordingService.writeFailure 是本 PR 之前就存在的进程级永久闩。修好文件不行,POST /session/:id/resume 也不行,只有重启 daemon 能恢复。

用户看到的形态正是测试计划已经问到的那条流程,也是两条发现交汇的地方:重试永远不会成功,而且点一次就在面板上多出第二条永久错误。数据没丢——上传文件仍在列表里,重启后的列表也正确(没有虚假成功)——但「Try again」提供的是一个不可能成功的动作。只读的 list() 可以直接返回内存快照,或者 load() 在纯读取时跳过 flush()

关于第 6 轮仍未决的两条 Important

  • R2-1(错误分类)。 我能触达的每一条 REST 路由,映射都是对的:invalid_source→400、source_limit_reached→409、source_attachment_not_found→404、source_persistence_unavailable→503。4xx 被压成 503 的情况仅限于 qwen/session/sources/copy,它没有 REST 路由,只能从 bridge 的分叉路径到达——所以目前对用户的影响面比该发现描述的要小。R2-1 的后半部分是真的,我反复撞到:daemon 日志只有泛化 message,从来看不到底层的 EACCES
  • R2-2(全量转录解析)。 我用测量代替争论:每个会话一次冷读约 1.7 ms/MB(129 MB 时 219 ms),热读稳定在 ~2 ms,ACP 子进程的瞬时 RSS 随文件大小增长且不回落(129 MB 时 +123 MB)。而「同步尖峰会 stall 同一 child 上的其它会话」这部分复现不出来:读取是每行带 await 的流式 readline 循环,61 MB 冷读期间同 child 上另一个会话轮询 60 次,p90 1.1 ms、max 2.5 ms。所以:值得作为后续改成从尾部有界读取,但不是阻断项。

变异矩阵

10 个变异体杀死 8 个。两个存活的都是纵深防御而非主路径:M4 是解析侧的 200 上限,只有手工编辑或损坏的快照才能到达(写入路径先行截断);M8workspaceCwdsessionSourceId 里去掉,哈希确实变了,但我构造不出活体差异,因为一个会话的项目根在其生命周期内是固定的。各补一个用例即可,不值得阻断。

本 head 上的本地套件结果

套件 结果
npm run build / npm run typecheck ✅ 通过
core(session-sources、record-source、session-transcript-reader、session-source-tool-activation、conversation-branches) ✅ 202/202
acp-bridge(session-sources + bridge ✅ 933/933
sdk-typescript SessionSources ✅ 2/2
cli serve(servererror-responsetelemetrytelemetry-catalogweb-shell-staticmulti-workspace-sessions ✅ 1539/1539
web-shell 全量 client 套件 ✅ 294 个文件 / 6912 个用例

早先有一次 acp-bridge 运行报了 2 个失败;那次与 6912 用例的 web-shell 运行在同一台机器上重叠,单独重跑全绿——是负载抖动,不是真失败。

其它小问题

  • 报错字符串把路由模板暴露给最终用户:面板里的 「GET /session/:id/sources: Stored sources are unavailable」,以及新增来源对话框里的 「POST /session/:id/sources: Invalid title」
  • 打开工作区文件类型的二进制时,会在 GET /file: binary file: /绝对/宿主/路径/... 上方留着一行过时的「Loading file…」,且没有下载入口——而同样的字节走上传附件路径时,是一张干净的「Preview is not available for this file type」卡片加下载按钮。绝对宿主路径也原样透出。
  • 在 200 条上限时发送附件会静默不登记(source_limit_reached 被有意当作永久失败从重试队列里剔除)。没有任何损失——文件仍以普通上传文件显示——但也完全没有提示。作为设计我认为是合理的,只是提出来确认这份沉默是有意的。

未验证

归属不可用/被替换时的预览改向;「明确拒绝消息时应清理已上传字节」;独立 CLI 的跨客户端来源复制;Windows 与 Linux(仅 macOS);外部模型提供方。压缩是在小转录上通过 /compress 跑的,所以只证明了「登记元数据在压缩后存活」,没有覆盖大上下文压缩路径。

关于 review 5151984218 的收敛性顾虑

作为一个数据点:这一轮与既有评审轮次相互独立——真 daemon、真浏览器、变异矩阵、文件系统层的故障注入——而且没有产生新的 Critical。它复现了已记录的两条推迟项并把可达性讲清楚了,其余发现都是小问题或后续项。这不能了结关于 diff 体量的争论,但它是对该功能行为的一次完整走查,没有打开新的 fails-closed 面。

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

Approving. The round-6 incremental review at this head is under the convergence posture: the two remaining Criticals (the Unicode-Cf invisible-character gate at session-sources.ts:85, and the latched recorder write-failure path at :343) are both fails-closed on new surface — no wrong result is certified and the merge base had neither the surface nor the defect — so they are deferred as follow-up work, not blockers.

CI is fully green (Lint, Test, web-shell E2E, Capture visuals, no-AK integration, Desktop Shell, Real daemon E2E, Serve A/B, Live Host). This has run six rounds with the blocking surface shrinking each time; the remaining items are recorded follow-ups.

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

结论 / Verdict: APPROVE(附条件说明 / with explicit caveats)

中文 — head 未变(仍为 fe5ebb17a),与我上一条 COMMENT(review id 5152619692)所钉的 commit 完全一致,其间没有新提交。本次 APPROVE 不改变我此前的技术判断,而是对当前状态做出放行决定,并如实记录仍未关闭的两项。

我此前提出、且在 fe5ebb17a 复核确认依然存在的两条 Important,均未在本 PR 内修复,由维护者显式登记为后续跟进项(非 Critical),依据仓库「审到约第五轮后只落地 Critical 修复」的 late-review 约定放行:

  • R2-1 —qwen/session/sources/* 错误压平acpAgent.ts 对任何非 SessionSourceError 统一返回 source_persistence_unavailable,经 error-response.ts 映射为 503,内部 cause 仅走 debugLogger,不经 HTTP 暴露。维护者的范围澄清成立——copy「目标非本会话 fork」是受信任的私有父进程操作,bridge 只消费一次 sourceError 产出 copy 警告、不重试非法目标;公开侧非法 source 输入本就已映射 invalid_source/400。因此这条更准确的定性是诊断/可观测性层面的改进(保留 cause、细化私有 ACP 错误分类,且不泄漏私有子进程路径),并非用户可见的错误状态码缺陷。作为 follow-up 接受。
  • R2-2 — readSessionSources 全量物化 transcript:首次访问 source 时以 readLinesWithIntegrity(..., Infinity) 一次性解析整份 transcript,运行在多个会话共享的单线程 ACP child 上,大历史下可能阻塞其他会话。维护者确认「首次访问确实一次性物化整份 transcript」,且完整审计已声明大历史性能未做新基准。后续若做有界/选择性读取,须单独 benchmark 并保住 fail-closed 完整性规则(最新快照损坏/不支持必须失败关闭,不得回退到删除前的旧快照),以现有 corruption/restore 测试与真实恢复探针为验收基线。作为 follow-up 接受。

放行的其余依据:所有 Critical 级用户可见缺陷均已带回归与真机(真 daemon / 真 ACP child / 真 transcript / 真 Chrome)证据修复;CI 在本 head 全绿(Test、Lint & Static、web-shell E2E Smoke、Integration、Capture web-shell visuals、Real daemon E2E、Desktop Shell 均 pass);另有维护者 yiliang114 已 APPROVE。

一句话:代码可合并;R2-1/R2-2 是已知、被明确登记、非 Critical 的延后项,不是被忽略或被判为不存在。


English — The head is unchanged (still fe5ebb17a), identical to the commit pinned by my previous COMMENT (review id 5152619692); no new commits since. This APPROVE does not revise my earlier technical assessment — it records a ship decision on the current state, with the two still-open items documented honestly.

The two Important findings I raised, re-verified as still present at fe5ebb17a, are not fixed within this PR and have been explicitly logged by the maintainer as follow-up work (non-Critical), consistent with the repository's late-review convention of landing only Critical fixes after roughly five rounds:

  • R2-1 — qwen/session/sources/* error flattening: in acpAgent.ts, any non-SessionSourceError is collapsed to source_persistence_unavailable, mapped by error-response.ts to 503, with the internal cause only going to debugLogger and never surfaced over HTTP. The maintainer's scope clarification holds — the copy "target is not a fork of this session" case is a trusted private-parent operation; the bridge consumes the sourceError once to produce a copy warning and does not retry the invalid target, while public invalid-source input already maps to invalid_source/400. So this is more precisely a diagnostics/observability improvement (preserve causes, refine the private-ACP error taxonomy, without leaking private child paths through the HTTP response), not a user-facing wrong-status defect. Accepted as follow-up.
  • R2-2 — readSessionSources materializes the full transcript: first source access parses the entire transcript via readLinesWithIntegrity(..., Infinity), on the single-threaded ACP child shared by multiple sessions, which can stall other sessions on large histories. The maintainer confirmed first access still materializes the transcript once, and the complete audit explicitly listed large-history performance as not newly benchmarked. Any bounded/selective implementation should be benchmarked separately and preserve the fail-closed integrity rule (a malformed/unsupported newest snapshot must fail closed, never falling back to an older pre-removal snapshot), using the existing corruption/restore tests and real recovery probes as the acceptance baseline. Accepted as follow-up.

Remaining basis for shipping: all user-facing Critical defects are fixed with regression and real-environment (real daemon / real ACP child / real transcript / real Chrome) evidence; CI is fully green at this head (Test, Lint & Static, web-shell E2E Smoke, Integration, Capture web-shell visuals, Real daemon E2E, Desktop Shell all pass); and maintainer yiliang114 has separately APPROVED.

Bottom line: the code is mergeable; R2-1/R2-2 are known, explicitly logged, non-Critical deferred items — not ignored, and not judged absent.

@callmeYe
callmeYe added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 471b6e5 Sep 10, 2026
124 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants