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

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.

@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 a9be7a0a 上 Test (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 sync:session_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),走 removePendingPrompt → removeUploadedAttachments(清掉上传字节)→ 返回 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/:2822。isSafeHref 已在 :55 导入却在来源路径上没用。当前服务端 validateSessionSourceInput(session-sources.ts:157-163)与快照恢复校验(parseSessionSourcesSnapshot→:226)都强制 http(s),所以正常流程不可达;但兄弟路径都防了、这条没防,面对可被编辑/损坏后回放的 transcript 快照,建议对齐:href={isSafeHref(locator.url) ? locator.url : undefined},非法时不渲染链接。

🟢 建议 / Nits

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

🎉 做得好的地方 / Positives

  • 工具只接受 workspace_file|url(record-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.

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

@callmeYe

callmeYe commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

CI follow-up in 59bc323: Electron Builder followed the newly introduced ancestor workspace configuration and selected pnpm while Live Host uses its own npm lockfile and install. Declaring an empty workspace boundary for the standalone app keeps the dependency collector at the app root. A regression test calls the actual locked builder detector and was verified red before the fix.

Validation passed with the locked Electron Builder 26.15.3: clean npm installation, typecheck, all 69 host tests, build, and the exact unsigned macOS packaging step from CI, including ASAR/native-resource checks, Info.plist checks, strict ad-hoc signature verification and entitlement checks. Dependency versions and the lockfile did not change. Fresh CI is running on this head.

@callmeYe
callmeYe enabled auto-merge September 8, 2026 05:47
@callmeYe

callmeYe commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the reproducible history viewport failure from Smoke runs 34191710804 and 34197997866.

The 200-record paging test was consistently losing the reading position by 286px. When scrolling outran virtual row rendering, the initial DOM anchor capture found no row. The response later prepended a page with no anchor to restore, leaving the virtualizer's estimated scroll offset in place. The viewport now refreshes the reading anchor immediately before admitting a current boundary response, including retries; canceled requests are checked again before admission.

Reproduction and verification:

  • Reproduced the original 286px drift locally with Chromium CPU throttling at 6x. A new regression also failed before the fix when rows became visible after paging started without another scroll event.

  • After synchronizing the latest remote branch, full build, typecheck and bundle passed, along with 93 focused viewport/store tests. Added cases cover canceled responses and cancellation from the pre-admission callback.

  • All three original history browser scenarios passed against built static assets at CPU6, with zero retries. The 200-record scenario completed all eight wheel steps; the formerly failing second older-page step retained the message at offset 12px with scrollTop 18914.

  • The synchronized source-based Smoke suite passed all 53 tests with 9 workers and zero retries (1.9m), including the 600px attachment/layout scenario. No browser page errors were recorded.

The existing browser assertions and timeouts are unchanged. The separate pre-fix composer startup failure was at the initial application-root mount assertion, before attachment layout checks; it is not attributed to the anchor fix.

@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Main's split-view navigation rework (#11250) made a boundary load wait
for the virtualized rows to mount, retrying the anchor capture over a
few frames and giving up rather than loading without a reading position
to restore. This branch solved the same missing-rows problem from the
other end: it hands the viewport an anchor-refresh callback that the
store invokes immediately before admitting the new page, while the old
rows are still mounted.

Keep both. The deferred loop now passes that callback through to the
boundary load, so the anchor is captured before the request starts and
refreshed at the last moment it can still be observed.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Root cause

#11250 "Improve split-view session navigation" (70cf363395) rewrote the same load() in TranscriptViewport.tsx this PR edits. Both fix one bug from opposite ends: a boundary load can start before the virtualized rows mount, so capture() returns undefined and no reading position survives. #11250 defers the load (retry capture over 8 frames, give up rather than load unanchored); this PR loads immediately and passes refreshAnchor as a beforeAdmit callback the store fires just before admitBoundary — the last moment old rows are mounted.

Semantic, not textual

Both sides edited one statement, void viewport.load(...). Kept #11250's loop, threaded this PR's callback through; only the last line differs from main:

if (!saved) {
  if (--remaining > 0)
    loadFrame.current = requestAnimationFrame(loadWhenVisible);
  return;              // never load without an anchor
}
anchor.current = saved;
void viewport.load(direction, refreshAnchor);

Load-bearing

  • load() must keep writing loadFrame.current: handleScrollIntent() and the viewKey cleanup (both feat(web-shell): Improve split-view session navigation #11250, merged outside the conflict) cancelAnimationFrame it — this PR's original three-line load() would break deferred-load cancellation.
  • The intent check must stay before capture(): it lets a pointerdown mid-retry abandon the load.
  • refreshAnchor is not redundant — the loop anchors at request start, the callback re-anchors at admit. It must read via captureRef, not capture, or the long-lived callback freezes a stale toolSources/pin.

Could not verify: a NON-conflicted test contradicts this

TranscriptViewport.scroll.test.tsx auto-merged cleanly (both sides added an identical hideRows mock flag) but now holds mutually exclusive getTranscriptPage counts while rows are hidden: L286/L314 (main) expect 1, L377 (this PR) expects 2 immediately. No load() satisfies both. I kept main's behaviour, so this PR's own L377 test will fail at toHaveBeenCalledTimes(2) — it observes 1, the load being deferred rather than fired. Rework it to main's timing (start the request with rows visible, hide only while in flight) or drop it as covered by L286. Not edited: it did not conflict.

No build/lint/tests were run, per instructions.

中文说明

根因:主干 #11250(70cf363395)重写了本 PR 同样修改的 TranscriptViewport.tsx 中的 load()。两边修同一个 bug,方向相反:边界加载可能在虚拟化行挂载前发起,capture() 返回 undefined,阅读位置无从恢复。#11250 推迟加载(重试 8 帧,捕不到锚点就放弃);本 PR 立即加载,并把 refreshAnchor 作为 beforeAdmit 回调传入,由 store 在 admitBoundary 前调用。这是语义冲突:两边改的是同一条语句 void viewport.load(...),我保留 #11250 的循环并穿入本 PR 的回调,相对 main 只有最后一行不同。

关键约束:load() 必须继续写 loadFrame.current,否则 handleScrollIntent() 与 viewKey 清理里的 cancelAnimationFrame 成为空转;intent 判断须留在 capture() 之前;refreshAnchor 并非冗余(开始定锚 vs 接纳时重定锚),且须经 captureRef 读取以免冻结过期闭包。

无法验证:TranscriptViewport.scroll.test.tsx 干净自动合并(两边加了相同的 hideRows 标志),但现含互斥断言:main 的 L286/L314 期望隐藏行期间 getTranscriptPage 只调 1 次,本 PR 的 L377 期望立即 2 次。我保留 main 行为,故本 PR 的 L377 会失败于 toHaveBeenCalledTimes(2)。需按 main 时序改写或删除;该文件未冲突,未修改。按要求未运行 build/lint/测试。

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

@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 ghost 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:a9be7a0a6 → fe5ebb17a(本轮只走查该区间内属于本 PR 的提交,合并 main 的 merge commit 不计;引用的每一行都在 head fe5ebb17a 的树上重新核对过)。

Baseline → head: a9be7a0a6 → fe5ebb17a. 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)、Lint、Desktop Shell、Capture web-shell visuals、web-shell E2E Smoke 全绿(仅 bot 的 review-pr 还 pending)。提交 a2b12d047 补齐了 packages/cli/src/i18n/locales/{zh,zh-TW,en}.js、serve/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.aborted 且 removePendingPrompt 成功时,先 removeUploadedAttachments 再 return {removedAfterAbort:true}(:1343-1344),从而跳过 :1363 的 registerAcceptedAttachmentSources;另一条准入路径也在 :1107 先 removeUploadedAttachments+throwIfAborted 再登记(:1174)。「acceptance 前取消 → 什么都不登记」的契约现在成立。
  • I-3(来源 URL 的 <a href> 未走 isSafeHref)已解决。 components/artifacts/ArtifactPanel.tsx:3054 现在用 {isSafeHref(locator.url) && (...)} 包住链接,:3057 的 href 与 :3060 的 openExternal 仅在通过校验时渲染,与兄弟路径 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.ts 的 catch {} 里保留 cause(new 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() 以零参覆盖了基类签名,忽略了 AbortSignal(tools.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.tsx 与 SourcePreview.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.

@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 build、npm 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 13;uploaded-diagram.png 只出现一次,显示为显式标题「Architecture diagram (explicit title)」;收起 3 → 展开 13 → 再收起 3
工作区文件与 HTTP(S) 登记:身份/创建时间保留、相同重试不涨 revision、编辑不改排序、非法定位被拒、200 上限只约束元数据 ✅ 创建 → revision 1 / created;相同重试 → revision 1 / unchanged;改标题 → revision 2 / updated 且 createdAt 不变。被拒:绝对路径(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:true、truncatedCount: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 上限,只有手工编辑或损坏的快照才能到达(写入路径先行截断);M8 把 workspaceCwd 从 sessionSourceId 里去掉,哈希确实变了,但我构造不出活体差异,因为一个会话的项目根在其生命周期内是固定的。各补一个用例即可,不值得阻断。

本 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(server、error-response、telemetry、telemetry-catalog、web-shell-static、multi-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
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.

5 participants