Repository navigation
feat(web-shell): add unified session sources - #11262
Conversation
Local implementation and E2E acceptanceImplementation commit: 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.
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 |
ytahdn
left a comment
There was a problem hiding this comment.
本 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-488copyFrom用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.
|
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 Validation:
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. |
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.
|
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. |
|
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:
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. |
|
@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 resolved the merge conflicts and pushed the branch update. Root cause#11250 "Improve split-view session navigation" ( Semantic, not textualBoth sides edited one statement, 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
Could not verify: a NON-conflicted test contradicts this
No build/lint/tests were run, per instructions. 中文说明根因:主干 #11250( 关键约束: 无法验证: |
|
Closed the current source security/bootstrap review findings and the CI regressions in 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. |
|
Resolved the conflict with main 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. |
|
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
left a comment
There was a problem hiding this comment.
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.
|
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. |
|
Following up on the convergence concern with concrete work on 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:
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. |
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
left a comment
There was a problem hiding this comment.
增量复审 / 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:489copyFrom仍以commit(copied)(只含父列表)整体替换目标快照而非合并;当前唯一调用方有「目标必须是本会话 fork」的守卫(acpAgent.ts:10343)且 fork 初始为空,故实际安全,但若将来对已自行登记过来源的 fork 再次 copy,会静默丢弃子会话自己的来源。(上一轮已提,未变,留作潜在项。)tools/record-source.ts:36RecordSourceInvocation.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.
|
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.
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. |
Local real-environment verification (maintainer pass) — head
|
| 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. |
| 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.
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 |
|---|---|
![]() |
![]() |
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.
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 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 toqwen/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 underlyingEACCES. - R2-2 (full transcript parse). Measured rather than argued:
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
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_reachedis 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 |
|---|---|
![]() |
![]() |
| Workspace PDF — body actually renders | Workspace HTML — stays literal text, script never runs |
|---|---|
![]() |
![]() |
| Binary attachment — no preview, download offered | Source list down, attachments healthy — independent errors |
|---|---|
![]() |
![]() |
Older daemon (capability removed) — no Add-source affordance, uploads still listed:
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
结论 / 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: inacpAgent.ts, any non-SessionSourceErroris collapsed tosource_persistence_unavailable, mapped byerror-response.tsto 503, with the internal cause only going todebugLoggerand 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 thesourceErroronce to produce a copy warning and does not retry the invalid target, while public invalid-source input already maps toinvalid_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 —
readSessionSourcesmaterializes the full transcript: first source access parses the entire transcript viareadLinesWithIntegrity(..., 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.















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
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.
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
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
Linked Issues
None.
中文说明
本 PR 的内容
增加统一的“来源”清单,展示上传文件、工作区文件引用和 HTTP(S) 链接。历史附件不需要迁移元数据就能找到;有登记信息的附件优先使用该信息,同一文件不会重复展示。列表统一单行,默认三条,支持“查看全部 / 收起”。
增加 Agent/客户端显式来源登记、按会话归属路由的接口、持久化元数据快照和列表失效通知。登记只保存元数据,不读取文件、不抓取 URL、不把资源内容加入 prompt,也不创建产物。文件预览复用现有会话/工作区访问检查;来源 HTML 保持文本预览,二进制附件支持下载。
为什么需要
现有附件列表只覆盖上传文件,工作区文档和链接没有统一的会话参考资料清单。用户需要一个重新打开资料的入口,无须区分上传存储和可选的引用元数据。登记与消息投递分开后,元数据写入失败可以独立重试,不会重发消息。
审阅验证计划
如何验证
证据(前后对比)
之前:上传文件显示在“附件”,工作区引用与链接缺少统一登记清单。之后:一个“来源”清单覆盖这些资料,保留历史上传文件,以单行展示,并支持三条与全部之间的往返切换。
全仓 build 和 typecheck 通过。Core、bridge、daemon、SDK 与 Web Shell 的定向回归通过,包括最新 33 项环境面板检查。实际本地验收覆盖 daemon、ACP child、工具调度、录制文件、REST/SSE 和 Chrome:统一流程 20 项、容量/长文件名 3 项、最终视觉检查 2 项通过。最后的单行与 3 → 全部 → 3 交互也在 Chrome 中实际验证。独立 PR 评论记录 E2E 结果和验证边界。
验证系统
环境
Node 22.17.0、Chrome 152.0.7977.82。模型回复使用带测试占位凭据的本机确定性 fixture;产品运行时、调度、存储和浏览器交互均真实执行。不声称已验证外部模型端点。
风险与范围
关联 Issue
无。