Skip to content

feat(transcript): add cross-host contract prevalidation - #9355

Closed
water-in-stone wants to merge 3 commits into
QwenLM:mainfrom
water-in-stone:feat/web-shell-adapter
Closed

feat(transcript): add cross-host contract prevalidation#9355
water-in-stone wants to merge 3 commits into
QwenLM:mainfrom
water-in-stone:feat/web-shell-adapter

Conversation

@water-in-stone

Copy link
Copy Markdown
Collaborator

What this PR does

This PR establishes and validates a minimum read-only transcript contract for Web Shell, Qwen Tauri Desktop, VS Code, and future HTML export while continuing to use the existing daemon transcript blocks instead of introducing a second public message model.

It adds versioned synthetic fixtures, schemas, hashes, a capability matrix, stable identity and render/action probes, and machine-generated gates for shared transcript semantics. Both the direct-daemon and ACP candidates pass append, partial-prepend, replay, scope, render, and action identity validation; ACP remains the selected path for a later VS Code migration because it is the current production transport.

It also introduces a versioned, allowlisted export document boundary and an opt-in document rendering mode with typed tool/result projections, resource budgets, CSP and zero-network checks, path and metadata sanitization, and document-only Mermaid limits. The default interactive and readonly adapters retain their existing raw input/output behavior, including complete write_file Turn Output diffs.

This is contract prevalidation and supporting infrastructure. It does not migrate the production VS Code timeline, replace the current HTML export renderer, change composer or permission workflows, or introduce a shared ChatPanel package.

Why it's needed

The four target hosts currently obtain and render conversation history through different data paths. Before sharing more presentation code, they need repeatable evidence that the existing transcript representation preserves the same visible semantics, stable identity, and host-action targets across replay, reconnect, pagination, and protocol boundaries.

HTML export also requires a stricter trust boundary than an interactive runtime: raw tool payloads, credentials, internal metadata, absolute paths, and active remote resources must not leak into an exported document. Establishing these identity, compatibility, and export-safety gates first provides a measurable foundation for later VS Code and HTML migrations without causing a Web Shell behavior break.

Reviewer Test Plan

How to verify

  • Confirm that normal interactive and readonly Web Shell transcripts still use raw tool input/output and that a write_file call still produces the complete file diff and line statistics.
  • Confirm that document mode renders tool results, plans, permission history, nested and parallel tools, Markdown, and Mermaid from the safe typed projection without falling back to raw payloads.
  • Confirm that identical direct-daemon and ACP source events retain stable block, rendered-item, and action identity after append, partial prepend, replay, and scope rebinding, while inputs without stable native identity fail closed.
  • Confirm that the maximum export document supports full-text search, copy, and print with no unexpected network requests or CSP violations, and that unsafe metadata, paths, URLs, images, credentials, and canary values are rejected or safely projected.
  • Confirm that the generated gate report records both VS Code candidates as passing, selects ACP for the later migration phase, and contains no blockers.

Evidence (Before & After)

Tested on

OS Status
🍏 macOS
🪟 Windows
🐧 Linux

Environment (optional)

macOS 26.0, Node.js v22.22.3, npm 10.9.8, and the repository-pinned Playwright Chromium. Validation completed with the repository build and typecheck, 644 CLI Session/identity tests, and 7 cross-host contract/document integration tests. The generated report recorded overall: pass, direct-daemon and ACP as pass, selectedVscodePath: acp, zero blockers, zero unexpected requests, and zero CSP violations.

Risk & Scope

  • Main risk or tradeoff: This is a broad cross-package contract change; incorrect source identity projection or export sanitization could cause replay identity drift, incomplete document rendering, or sensitive-data exposure. The deterministic fixtures, schema allowlists, compatibility regressions, and browser gates are intended to detect those failures.
  • Not validated / out of scope: Windows and Linux were not tested. Production VS Code timeline integration, production HTML renderer replacement, installed Tauri desktop smoke testing, composer and queue unification, active permission handling, host side effects, and a shared ChatPanel package remain follow-up work.
  • Breaking changes / migration notes: No intentional breaking change or migration is introduced. Interactive and readonly raw payload semantics and default runtime ordinal IDs remain unchanged; the safe projection and Mermaid limits apply only to document/export paths, and the new identity metadata is additive.

Linked Issues

Closes #9354

中文说明

Establish cross-host chat transcript contract prevalidation

What this PR does

本 MR 为 Web Shell、Qwen Tauri Desktop、VS Code 和未来的 HTML 导出建立并验证最小只读 transcript 契约,同时继续复用现有 daemon transcript blocks,不引入第二套公共消息模型。

它增加了版本化合成 fixtures、schemas、hashes、capability matrix、稳定 identity 与 render/action probes,以及共享 transcript 语义的机器生成门禁。direct-daemon 与 ACP 两条候选路径均通过 append、partial prepend、replay、scope、render 和 action identity 验证;由于 ACP 是当前生产传输路径,后续 VS Code 迁移仍选择 ACP。

它还引入了版本化、逐字段 allowlist 的导出文档边界,以及显式启用的 document rendering mode,包括类型化工具与结果投影、资源预算、CSP 与零网络检查、路径和 metadata 脱敏,以及仅作用于 document mode 的 Mermaid 限制。默认 interactive 和 readonly adapters 保持原有 raw input/output 行为,包括完整的 write_file Turn Output diff。

本 MR 的范围是契约预验证和配套基础设施。它不迁移生产 VS Code 时间线、不替换当前 HTML 导出 renderer、不改变 composer 或权限工作流,也不引入共享 ChatPanel package。

Why it's needed

四个目标宿主目前通过不同数据路径获取和渲染会话历史。在进一步共享展示代码之前,需要用可重复的证据证明现有 transcript 表达能够在 replay、reconnect、pagination 和协议边界下保持一致的可见语义、稳定 identity 与宿主动作目标。

HTML 导出还需要比交互式运行时更严格的信任边界:raw tool payload、credential、内部 metadata、绝对路径和主动远程资源都不能泄漏到导出文档。先建立这些 identity、兼容性和导出安全门禁,可以为后续 VS Code 与 HTML 迁移提供可度量的基础,同时避免造成 Web Shell 行为回归。

Reviewer Test Plan

How to verify

  • 确认正常 interactive 和 readonly Web Shell transcript 仍使用 raw tool input/output,并且 write_file 调用仍能生成完整文件 diff 与行数统计。
  • 确认 document mode 只使用安全类型化投影渲染工具结果、计划、权限历史、嵌套与并行工具、Markdown 和 Mermaid,且不会回退到 raw payload。
  • 确认相同 direct-daemon 与 ACP 源事件在 append、partial prepend、replay 和 scope 重新绑定后仍保持稳定的 block、rendered-item 与 action identity;缺少稳定原生 identity 的输入应 fail closed。
  • 确认最大规格导出文档支持全文搜索、复制和打印,不产生非预期网络请求或 CSP 违规;不安全的 metadata、路径、URL、图片、credential 和 canary 值应被拒绝或安全投影。
  • 确认生成的 gate report 将两条 VS Code 候选路径都记录为通过,为后续迁移阶段选择 ACP,并且 blockers 为空。

Evidence (Before & After)

N/A — 这是契约、安全和重构基础设施,没有刻意引入用户可见 UI 变化,因此未生成截图或录屏。

Tested on

OS Status
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

Environment (optional)

macOS 26.0、Node.js v22.22.3、npm 10.9.8,以及仓库锁定的 Playwright Chromium。已完成仓库 build 与 typecheck、644 个 CLI Session/identity tests,以及 7 个跨宿主 contract/document integration tests。生成的报告记录为 overall: pass,direct-daemon 和 ACP 均为 passselectedVscodePath: acp,blockers 为空,非预期请求为零,CSP 违规为零。

Risk & Scope

  • Main risk or tradeoff: 这是一次跨多个 package 的契约变更;错误的源 identity 投影或导出脱敏可能导致 replay identity 漂移、文档渲染不完整或敏感数据泄漏。确定性 fixtures、schema allowlists、兼容性回归测试和浏览器门禁用于检测这些失败。
  • Not validated / out of scope: 未在 Windows 和 Linux 上测试。生产 VS Code 时间线接入、生产 HTML renderer 替换、安装后 Tauri desktop smoke test、composer 与队列统一、活动权限处理、宿主副作用以及共享 ChatPanel package 均属于后续工作。
  • Breaking changes / migration notes: 未引入有意的 breaking change 或迁移要求。interactive 与 readonly 的 raw payload 语义和默认 runtime ordinal IDs 保持不变;安全投影与 Mermaid 限制只作用于 document/export 路径,新增 identity metadata 为增量字段。

Linked Issues

Closes #9354


@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 17, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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

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

Establish a shared read-only transcript boundary for Web Shell, VS Code,
Desktop, and future HTML export without changing interactive adapter
semantics.

- Add versioned fixtures, schemas, capability gates, and identity probes
- Preserve raw runtime payloads while adding safe document projections
- Add source-stable ACP identity and read-only VS Code ID projection
- Add document rendering, export allowlists, and network guards
- Cover direct-daemon, ACP, HTML export, and Web Shell regressions

Note: Production VS Code and HTML renderer migrations remain follow-ups.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a big one, so a few honest gate observations before code review.

  • Template: complete ✓
  • Problem: no observed defect here. The linked issue (Establish cross-host chat transcript contract prevalidation #9354) was filed by the PR author a few minutes before this PR, is still status/needs-triage, and has no maintainer sign-off. The motivation is forward-looking — "before sharing more presentation code we need repeatable evidence" — not a user-visible break that has actually happened. That doesn't disqualify a feat PR, but it does mean the direction question carries the weight.
  • Direction: cross-host transcript consistency and hardened HTML export are plausible areas, and the author has a solid record of merged web-shell work. But this PR builds the validation machinery and a brand-new rendering path before the migrations it protects (VS Code timeline migration, HTML renderer replacement, shared ChatPanel) have been designed or approved — those are all listed as follow-up work. Claude Code's CHANGELOG shows no direct reference for this kind of contract prevalidation, so no external direction signal either way. Because this is 5,700+ production lines across five packages (core-breadth paths), it is escalated to a maintainer for awareness regardless — the direction call ("do we want prevalidation infrastructure before the migration itself is scoped?") belongs to them.
  • Size: 77 files, +11,032/−350. Breakdown excluding tests/fixtures/schemas: 5,722 production-logic lines (5,406 additions) vs 5,660 test/fixture/schema lines (incl. a 775-line JSON schema and a 777-line design doc). This is a feat touching core-breadth paths, so no hard block — but it is well past the 1,000-line advisory: if any part of this can be split, please consider it (see Approach).
  • Approach: two separable concerns are bundled. (a) Contract/identity validation — fixtures, probes, gates — tests the existing transcript path. (b) A whole new document-mode export pipeline: a 2,586-line export-transcript-document.ts that runs parallel to the existing packages/cli/src/ui/utils/export module (which production actually uses, and which this PR by its own description does not replace), plus SDK additions and an ACP wire change (segmentId stamped onto every session update via a new projector in Session.sendUpdate). Have you considered landing (a) alone first — contract tests against the path we already ship — and letting the document-mode renderer and the wire changes arrive with the migration PR that actually consumes them? That would cut most of the new surface and keep the hot path untouched until something needs it. Raised as a genuine question, not a block.
  • Risk: Stage 1e signal — packages/cli/src/acp-integration/session/** is correlated with post-merge reverts in this repo's history, and Session.sendUpdate is on every ACP update's hot path. Full review depth and CI evidence will be required in Stage 2 before anyone should consider merging.

Flagging these for maintainer attention, and moving on to code review. 🔍

中文说明

感谢贡献!这个 PR 体量很大,先说几点门禁阶段的观察。

  • 模板:完整 ✓
  • 问题:没有观测到的缺陷。关联 issue(Establish cross-host chat transcript contract prevalidation #9354)是 PR 作者在 PR 前几分钟自己创建的,目前仍是 status/needs-triage,没有 maintainer 确认。动机是前瞻性的——"在共享更多展示代码之前需要可重复的证据"——而不是已经发生的用户可见问题。这对 feat PR 不构成否决,但意味着方向性问题成为关键。
  • 方向:跨宿主 transcript 一致性和加固的 HTML 导出是合理方向,作者在 web-shell 也有大量已合并的工作。但本 PR 在它要保护的迁移(VS Code timeline 迁移、HTML 渲染器替换、共享 ChatPanel)尚未设计或批准之前,就构建了验证机制和一条全新渲染路径——这些都列为后续工作。Claude Code 的 CHANGELOG 中没有此类契约预验证的直接参考。由于这是跨五个包、5,700+ 生产行的改动(核心广度路径),无论如何都要升级给 maintainer 知悉——方向判断("我们是否要在迁移本身定稿前先建预验证基础设施?")应由 maintainer 做出。
  • 规模:77 个文件,+11,032/−350。剔除测试/fixture/schema 后:5,722 行生产逻辑(其中 5,406 为新增),测试/fixture/schema 共 5,660 行(含 775 行 JSON schema 和 777 行设计文档)。这是触及核心广度路径的 feat,不硬拦截——但已远超 1,000 行建议线:如果可以拆分,请考虑(见"方案")。
  • 方案:PR 打包了两件可分离的事。(a) 契约/身份验证——fixtures、probes、gates——测试的是现有 transcript 路径。(b) 一整套全新的 document-mode 导出管线:2,586 行的 export-transcript-document.ts,与生产实际使用的现有 packages/cli/src/ui/utils/export 模块并行存在(且 PR 自述并不替换它),外加 SDK 新增和 ACP 协议变更(通过 Session.sendUpdate 中的新 projector 给每条 session update 打上 segmentId)。有没有考虑过先只落地 (a)——针对现有路径的契约测试——让 document-mode 渲染器和协议变更随真正消费它们的迁移 PR 一起进来?这样可以砍掉大部分新增面,并在有东西需要之前不碰热路径。这是认真提出的问题,不是拦截。
  • 风险:Stage 1e 信号——packages/cli/src/acp-integration/session/** 与本仓库历史上的 merge 后 revert 相关,且 Session.sendUpdate 位于每条 ACP 更新的热路径上。Stage 2 将要求完整 review 深度和 CI 证据,之后才应考虑合并。

以上提请 maintainer 关注,同时进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline first: for "repeatable evidence that the existing transcript path preserves semantics and identity across hosts", I'd have landed a thin fixture-based equivalence suite against the path we already ship (fixtures + replay/render assertions, ~1-2k lines including tests), verified identity with the IDs the protocol already carries, and left the export hardening to the migration PR that needs it — no wire changes, no new production surface.

What this PR does instead is broader. The good parts, verified statically: the default-path-preservation claim holds up in the diff. safeToolProjection and includeSourceIdentity default to false; renderMode defaults to 'readonly'; every shared component change (Plan/Tasks/ToolGroup/SubAgentPanel/Thinking/UserMessage/Markdown) is gated on documentMode; and the −117 in MessageList is a pure code move of the grouping logic into an adapter, not a behavior change. The sizeable test/fixture effort is real and mostly well-shaped.

Findings worth a maintainer's attention:

  1. The new machinery has no production consumer. export-transcript-document.ts (2,586 lines), chatTranscriptContractProbe.ts (VS Code, 184 lines) and transcriptRenderProbe.ts (web-shell, 297 lines) are referenced only by this PR's own tests and the design doc. Nothing routes /export through the new document builder, the VS Code extension doesn't call the probe, and nothing ever renders web-shell in document mode. The PR says so itself ("does not migrate… does not replace…"). That means merging this lands ~3.1k lines of production-shaped code whose only user is the gate suite — speculative surface per the repo's simplicity-first convention, and surface that a follow-up migration may reshape anyway. This is the question I'd want answered before merge: can the contract tests land first, with the document pipeline arriving alongside the migration that consumes it?
  2. One ungated cross-mode change. extractDiff in ToolGroup gains a fallback to tool.args.patch / args.newText+args.oldText that is not gated on document mode. In interactive/readonly mode args is the raw tool input, so any edit-kind tool whose raw input carries a string patch or newText key (an MCP or custom tool is plausible) would render that value as its diff. Low probability, but this one should be gated to the safe projection or scoped to preview-projected tools.
  3. Hot-path change in revert-prone territory. Session.sendUpdate now routes every ACP update through TranscriptUpdateIdentityProjector (AsyncLocalStorage read + sha256 chain), and replay stamps segmentId on every emission. The implementation reads defensively (additive _meta only; passthrough without a promptId), but acp-integration is exactly where this repo's history says to want executed evidence — see the CI section below, which currently has none.
  4. Minor, non-blocking: mermaid rendering is now serialized through a global queue in all modes (previously parallel per block) — presumably deliberate, but it is an interactive-mode change; and the two identity-hash implementations (sha256 chain in CLI vs 64-bit FNV-style in the web-shell probe) are different by design today but worth keeping an eye on if this becomes a shared contract.

Not verified: runtime behavior of any of the above — no CI has run on this commit (below), so the "default behavior unchanged" and "segmentId is inert" claims rest on static reading alone. The author's reported results (macOS only, 644 CLI tests + 7 integration tests passing) are the author's claim, not evidence this review can cite.

Files changed (30 of 77 shown)
File What changed
packages/cli/src/ui/utils/export/export-transcript-document.ts New v1 export document builder: budgets, opaque IDs, sanitized projections; test-only consumers
packages/cli/src/ui/utils/export/index.ts Barrel exports for the new module
packages/cli/src/acp-integration/session/transcript-update-identity.ts New projector stamping segmentId onto ACP updates
packages/cli/src/acp-integration/session/Session.ts sendUpdate routed through the projector (hot path)
packages/acp-bridge/src/transcript-replay.ts Replay emissions stamped with segmentId; resultPreviewText meta, 100KB cap
packages/sdk-typescript/src/daemon/ui/normalizer.ts Extracts segmentId and resultPreviewText from event meta
packages/sdk-typescript/src/daemon/ui/toolPreview.ts Tool result previews and a todo_list preview detector
packages/sdk-typescript/src/daemon/ui/transcript.ts segmentId and resultPreview propagated onto blocks
packages/sdk-typescript/src/daemon/ui/types.ts New preview and segmentId types (additive)
packages/sdk-typescript/src/daemon/ui/render.ts Markdown/plain rendering for todo_list previews
packages/vscode-ide-companion/src/services/chatTranscriptContractProbe.ts Contract probe for VS Code candidates; test-only consumers
packages/web-shell/client/adapters/transcriptToMessages.ts Opt-in source identity and safe tool projection; defaults unchanged
packages/web-shell/client/adapters/transcriptRenderProbe.ts Render identity probe; test-only consumers
packages/web-shell/client/adapters/parallelAgentGrouping.ts Grouping logic extracted from MessageList, unchanged semantics
packages/web-shell/client/components/MessageList.tsx Grouping moved out; re-export keeps the public shape
packages/web-shell/client/components/WebShellTranscript.tsx renderMode prop; readonly default keeps current behavior
packages/web-shell/client/components/messages/Markdown.tsx Document-mode mermaid limits, render timeout, strict image allowlist
packages/web-shell/client/components/messages/ToolGroup.tsx Document-mode gating; plus the ungated extractDiff fallback (finding 2)
packages/web-shell/client/components/messages/PlanMessage.tsx Document mode renders expanded, non-interactive
packages/web-shell/client/components/messages/TasksStatusMessage.tsx Document mode disables refresh timers and actions
packages/web-shell/client/components/messages/AssistantMessage.tsx Thinking always expanded in document mode
packages/web-shell/client/components/messages/UserMessage.tsx No collapse toggle in document mode
packages/web-shell/client/components/messages/tools/SubAgentPanel.tsx Document-mode default expansion
packages/web-shell/client/components/messages/tools/ParallelAgentsGroup.tsx Document-mode expansion, interaction disabled
packages/web-shell/client/hooks/useMessages.ts Threads the safeToolProjection flag
packages/web-shell/client/transcriptRenderMode.ts Adds the 'document' render mode
integration-tests/chat-transcript-contract.test.ts Cross-host identity, replay, and scope gates
integration-tests/chat-transcript-document.test.ts Browser gates for document mode: CSP, zero network, sanitization
integration-tests/fixtures/chat-transcript-contract/v1/** Versioned fixtures, JSON schemas, capability matrix
docs/design/web-shell/chat-transcript-contract-prevalidation.md Design doc for the whole effort

…and 47 more files — unit tests for the modules above, small CSS-module tweaks for the new mode, and fixture case files.

Test evidence (this PR's own CI)

PR CI has not run on this commit at all: there are zero pull_request-event workflow runs for the head SHA — only the pull_request_target bot orchestration jobs (label, triage, review-pr). Fork PR workflow runs need maintainer approval to start, so until one of you approves and the suite runs, there is no executed evidence for any claim in this PR, green or red. The table below reflects that; the finalize job will rewrite it once CI lands.

Check Conclusion
PR CI suite (lint, typecheck, build, unit, integration — macOS/Windows/Linux) not started — no pull_request workflow run exists for this commit
precheck-pr / precheck success
PR self-report label success

Sandboxed verification would settle the behavioural claims: @qwen-code /verify — that the default interactive/readonly rendering is truly unchanged and that the segmentId projector is observably inert on the ACP hot path cannot be proven from the diff, and there is no CI signal yet either. This is a fork author, so /tmux is unavailable; /verify works as a sponsored run — a maintainer's @qwen-code /verify comment approves the head it was written against, and the run carries a pre-execution risk screen plus a full workspace wipe. Read the resulting report with the same skepticism as the fork's own CI logs: the code under verification is adversarial input.

中文说明

代码审查

独立基线:要证明"现有 transcript 路径跨宿主保持语义与身份一致",我会先落地一套精简的 fixture 等价性测试(fixture + replay/render 断言,含测试约 1-2k 行),用协议已有的 ID 验证身份,把导出加固留给真正需要它的迁移 PR——不改协议、不新增生产面。

本 PR 做得更多。静态验证过的好消息:默认路径保持不变的声明在 diff 中成立。safeToolProjectionincludeSourceIdentity 默认 falserenderMode 默认 'readonly';所有共享组件改动(Plan/Tasks/ToolGroup/SubAgentPanel/Thinking/UserMessage/Markdown)都以 documentMode 门控;MessageList 的 −117 行是分组逻辑搬到 adapter 的纯移动,不是行为变化。测试/fixture 的投入是真实的,结构大体良好。

提请 maintainer 关注的发现:

  1. 新机制没有生产消费者。 export-transcript-document.ts(2,586 行)、chatTranscriptContractProbe.ts(VS Code,184 行)、transcriptRenderProbe.ts(web-shell,297 行)只被本 PR 自己的测试和设计文档引用。没有任何东西把 /export 接到新的文档构建器,VS Code 扩展不调用 probe,也没有任何地方以 document 模式渲染 web-shell。PR 自己也这么说("不迁移……不替换……")。这意味着合并会落地约 3.1k 行生产形态代码,其唯一用户是门禁套件——按仓库的"简单优先"约定这是投机性表面,而且后续迁移可能还会重塑它。合并前我想先得到答案:能否先落地契约测试,让 document 管线随消费它的迁移 PR 一起进来?
  2. 一处未门控的跨模式改动。 ToolGroup 的 extractDiff 新增了对 tool.args.patch / args.newText+args.oldText 的回退,且用 document 模式门控。在 interactive/readonly 模式下 args 是原始工具输入,因此任何 raw input 中带字符串 patchnewText 键的 edit 类工具(MCP 或自定义工具完全可能)都会把该值当作 diff 渲染。概率低,但这一处应当限定在安全投影内,或只对来自 preview 投影的工具生效。
  3. 易 revert 区域的热路径改动。 Session.sendUpdate 现在把每条 ACP 更新都经过 TranscriptUpdateIdentityProjector(AsyncLocalStorage 读取 + sha256 链),replay 也给每条发射打上 segmentId。实现读起来是防御性的(只加 _meta;无 promptId 时直通),但 acp-integration 恰恰是本仓库历史上最需要执行证据的地方——见下文 CI 部分,目前没有任何执行证据。
  4. 小的、非阻塞:mermaid 渲染现在在所有模式下都走全局队列串行化(此前按块并行)——应该是有意的,但这是 interactive 模式的变化;两套身份哈希实现(CLI 的 sha256 链 vs web-shell probe 的 64 位 FNV 风格)目前设计上就不同,但若将来成为共享契约值得留意。

未验证:以上所有点的运行时行为——本 commit 没有任何 CI 运行,因此"默认行为不变"与"segmentId 无副作用"目前只有静态阅读支撑。作者自报的结果(仅 macOS,644 个 CLI 测试 + 7 个集成测试通过)是作者声明,不是本审查可引用的证据。

测试证据(本 PR 自己的 CI)

本 commit 完全没有跑过 PR CI:head SHA 上没有任何 pull_request 事件的 workflow run——只有 pull_request_target 的机器人编排作业(label、triage、review-pr)。fork PR 的 workflow 需要 maintainer 批准才会启动,因此在批准并跑完套件之前,本 PR 的任何声明都没有执行证据,无论红绿。下表如实反映这一点;CI 落地后 finalize 作业会改写它。

沙箱验证可以落定行为性声明:@qwen-code /verify——默认 interactive/readonly 渲染确实不变、segmentId projector 在 ACP 热路径上确实无副作用,这两点无法从 diff 证明,目前也没有任何 CI 信号。作者是 fork 用户,/tmux 不可用;/verify 可作为赞助运行——maintainer 的 @qwen-code /verify 评论批准其写下的 head,运行前有预执行风险筛查和完整工作区清理。请以对 fork CI 日志同样的怀疑态度阅读产出的报告:被验证的代码是对抗性输入。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — competent, careful engineering, but the Stage 0 size escalation caps the verdict at a maintainer decision, and there are real open questions underneath the cap anyway.

Stepping back: the code I reviewed is mostly good. Defaults are preserved, the document-mode gating is consistent across a dozen components, budgets and caps fail closed, and the author clearly knows this codebase — their merged track record in web-shell and core shows. Nobody should read this as sloppy work.

But the gate's job is to ask whether this should ship in this shape, and here I have genuine reservations:

  • The stated goal is "repeatable evidence" about the transcript path we already ship. That goal is reachable with a small fraction of this diff — fixtures and equivalence tests against the existing path. What lands instead is ~3,100 lines of production-shaped code (the export document builder, the VS Code probe, the web-shell render probe) whose only consumers are this PR's own gate tests, plus wire-format additions (segmentId on every ACP update and replay emission) that nothing downstream reads yet. If a future migration reshapes the contract, all of that gets reworked or deleted.
  • One real cross-mode defect candidate slipped through anyway (extractDiff's ungated args.patch/newText fallback) — which is what happens when 5,700 production lines land at once.
  • There is zero executed evidence for any claim: no PR CI has run on the commit (fork workflows need maintainer approval), and the author's validation is self-reported on macOS only. On the plus side, the hot-path change is additive _meta only and reads defensively.
  • The direction itself was never signed off: the linked issue was self-filed minutes before the PR and is still untriaged.

So this is not a reject — it's a "this needs a human decision before it goes further".

⏸️ Deferring to @wenshao @pomelo-nwu — escalating per the core-breadth size rule (5,722 production lines across five packages; feat type, so no hard block, but policy requires maintainer awareness and withholds auto-approval). Specifically, this needs a maintainer call on:

  1. Direction: do we want contract-prevalidation infrastructure landed before the VS Code timeline / HTML export migrations it protects are scoped and approved? The motivating issue (Establish cross-host chat transcript contract prevalidation #9354) has no maintainer sign-off yet.
  2. Shape: consider having the author split this — contract/identity tests against the existing path first (the part with clear value today), and the document-mode pipeline, SDK additions, and ACP segmentId projection arriving with the migration PR that consumes them. That also takes the hot-path change out of a PR nobody can behaviorally verify until CI runs.
  3. Evidence: approving the fork's workflow runs would give this its first real test signal; @qwen-code /verify is available as a sponsored run if behavioural A/B proof on the default-path-unchanged claim is wanted.

The two concrete code findings from Stage 2 (test-only consumers; ungated extractDiff fallback) stand regardless of the direction call.

中文说明

置信度:3/5 —— 工程上是胜任且细致的,但 Stage 0 的规模升级把结论封顶在"由 maintainer 决定",而且封顶之下本来也有真实的疑问。

退一步看:我审过的代码大部分是好的。默认行为被保留,document 模式门控在十几个组件间保持一致,预算与上限都是 fail-closed,作者显然了解这个代码库——他们在 web-shell 和 core 的合并记录可以证明。不应把这里的评价读成"工作粗糙"。

但门禁的职责是问"这个应不应该以这种形态合入",在这一点上我有真实的保留:

  • 声明的目标是对现有 transcript 路径的"可重复证据"。这个目标用本 diff 的一小部分就能达到——针对现有路径的 fixture 与等价性测试。实际落地的却是约 3,100 行生产形态代码(导出文档构建器、VS Code probe、web-shell render probe),其唯一消费者是本 PR 自己的门禁测试,外加协议字段新增(每条 ACP 更新和 replay 发射上的 segmentId)而下游尚无人读取。如果未来的迁移重塑了契约,这些都要重写或删除。
  • 即便如此,仍有一处真实的跨模式缺陷候选溜了进来(extractDiff 未门控的 args.patch/newText 回退)——一次落地 5,700 行生产代码时就会发生这种事。
  • 任何声明都没有执行证据:本 commit 没有跑过 PR CI(fork 的 workflow 需要 maintainer 批准),作者的验证是自报的、且仅在 macOS。往好的方面说,热路径改动只是附加 _meta,读起来是防御性的。
  • 方向本身从未被确认:关联 issue 是 PR 前几分钟自己创建的,至今未分诊。

所以这不是拒绝——而是"在继续之前需要一个人类决定"。

⏸️ 移交 @wenshao @pomelo-nwu —— 按核心广度规模规则升级(5,722 行生产代码横跨五个包;feat 类型故不硬拦截,但政策要求 maintainer 知悉并禁止自动批准)。具体需要 maintainer 判断:

  1. 方向:在 VS Code timeline / HTML 导出迁移本身定稿之前,我们是否要先落地契约预验证基础设施?动机 issue(Establish cross-host chat transcript contract prevalidation #9354)尚无 maintainer 确认。
  2. 形态:建议让作者拆分——先落地针对现有路径的契约/身份测试(今天就有明确价值的部分),让 document-mode 管线、SDK 新增与 ACP segmentId 投影随消费它们的迁移 PR 一起进来。这也把热路径改动从"在 CI 跑起来之前谁都做不了行为验证"的 PR 里拿出去。
  3. 证据:批准 fork 的 workflow 运行将给它第一个真实测试信号;如果想要"默认路径不变"声明的行为性 A/B 证明,@qwen-code /verify 可作为赞助运行使用。

无论方向结论如何,Stage 2 的两个具体代码发现(仅测试消费者;未门控的 extractDiff 回退)都成立。

Qwen Code · qwen3.8-max

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

@water-in-stone
water-in-stone force-pushed the feat/web-shell-adapter branch from 1287487 to b6e691c Compare August 17, 2026 12:14
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 17 render-shaping files:

  • packages/web-shell/client/components/MessageList.module.css
  • packages/web-shell/client/components/MessageList.tsx
  • packages/web-shell/client/components/WebShellTranscript.tsx
  • packages/web-shell/client/components/messages/AssistantMessage.tsx
  • packages/web-shell/client/components/messages/GoalStatusMessage.tsx
  • packages/web-shell/client/components/messages/Markdown.tsx
  • packages/web-shell/client/components/messages/PlanExecutionView.tsx
  • packages/web-shell/client/components/messages/PlanMessage.tsx
  • …and 9 more.

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

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

Qwen Code · web-shell visuals

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not explored to full depth (tool budget reached): chunk 8: run integration-tests/chat-transcript-contract.test.ts end-to-end (worktree has no node_modules/build; install+build+bundle exceeds this chunk's tool budget — a….

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

中文说明

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

未探索到全部深度(达到工具调用预算):chunk 8:run integration-tests/chat-transcript-contract.test.ts end-to-end (worktree has no node_modules/build; install+build+bundle exceeds this chunk's tool budget — a…

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

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

const params: SessionNotification = {
sessionId: this.sessionId,
update: projectedUpdate,
update: this.transcriptUpdateIdentity.project(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The PR breaks the previously-green packages/cli suite: sendUpdate now stamps _meta.qwenTranscript.segmentId onto every update — including replayed history — but history-replayer.test.ts's exact-match expectations were not updated. — Failure scenario: any npm test / CI run of packages/cli: HistoryReplayer replays records through Session.sendUpdate, the projector injects segmentId, and 10 toHaveBeenCalledWith assertions fail on the extra field — the workspace test command exits non-zero in every environment (the failing Test (ubuntu-latest, Node 22.x) CI job is consistent with this).

Witness (executed):

PR tree: Tests 10 failed | 37 passed (47); merge base (121f247c0): 47/47 passed — measured on both trees

Suggested fix: Update history-replayer.test.ts expectations to include the projected identity metadata (as was done for the Session.test.ts refresh), or skip projection on the history-replay path if segment identity is meant to be live-session-only.

中文说明

sendUpdate 现在对每条 update(包括回放的历史记录)都注入 _meta.qwenTranscript.segmentId,导致 packages/cli 既有绿套件 history-replayer.test.ts 挂掉 10/47(merge base 上 47/47 全绿,双树实测)。The PR breaks the previously-green packages/cli suite: sendUpdate stamps segmentId onto every update including replayed history, and history-replayer.test.ts's exact-match expectations were not updated.

失败场景: 任何对该 PR 运行 packages/cli 测试/CI 的场合:HistoryReplayer 通过 Session.sendUpdate 回放 assistant/system 记录,projector 注入 "segmentId": "assistant-uuid:0",10 个 toHaveBeenCalledWith 精确匹配断言因多出字段而失败,workspace 测试命令在所有环境退出码非零。

建议修复: 更新 history-replayer.test.ts 的期望以包含投影的 identity metadata(如同 21k 行 Session.test.ts 的刷新方式),或者如果 segment identity 只应作用于 live session,则在 history-replay 路径跳过投影。

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

Comment on lines +2385 to +2386
value === '[link omitted]' ||
value === '[content omitted: export text budget exceeded]'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Class finding — the export producer can emit values the module's own final validator rejects, so legitimate transcripts hard-fail (invalid_block / envelope_budget_exceeded) instead of degrading like every other unsafe input in this module. Five executed symptoms: (1) safePath yields a bare drive letter 'C:' for Windows drive-root cwds that isSafeExportPath explicitly rejects; (2) once the visible-text budget is exhausted past the 46-byte sentinel window, applyTextBudget returns '' and web_fetch url='' fails the validator; (3) file_read.range passes through unsanitized — a recorded non-integer/negative range throws invalid_block; (4) the text budget accounts raw utf8 bytes while the envelope gate measures JSON-serialized bytes, so escaping inflation lets a document inside every component budget exceed the 32 MB envelope (measured 38.7 MB → throw); (5) a linked inline data-image [![img](data:…)](https://…) is spliced into a new remote image by sanitizeMarkdownNavigableUrls' label fallback, and the self-assert throws invalid_markdown_image. — Failure scenario: transcripts with a Windows drive-root tool cwd, long sessions (>8 MB visible text), JSON-escape-heavy tool output, or badge-style linked images fail export outright instead of degrading with diagnostics.

Witness (executed):

safePath('C:\\') -> 'C:' -> isSafeExportPath=false; boundary sweep remaining=47…0 -> THREW invalid_block (url='' arm); range [1.5,10] -> invalid_block; envelope probe serialized=38756037 > 33554432 -> envelope_budget_exceeded; linked data-image -> threw invalid_markdown_image (two independent runs)

Suggested fix: Make producer output always validator-safe: safePath returns '[path]' for bare drive letters; degrade url/labels to the omission markers when the budget would produce ''; sanitize or omit range; account the envelope in the same units as the component budgets (leave headroom for serialization inflation); stop the label-fallback splice from creating new images.

中文说明

类级发现:导出模块的生产者可以产出被模块自身最终校验器拒绝的值,导致合法 transcript 的导出直接抛错(invalid_block / envelope_budget_exceeded)而不是按模块其余部分的设计降级为诊断信息。五个已执行验证的症状:(1) safePath 对 Windows 盘符路径产出裸 'C:',isSafeExportPath 显式拒绝(command preview cwd 路径已实测触发);(2) 可见文本预算耗尽越过 46 字节哨兵窗口后 applyTextBudget 返回 '',web_fetch 的 url='' 无法通过校验器;(3) file_read.range 原样透传,录制的非整数/负数 range 触发 invalid_block;(4) 文本预算按未转义 utf8Bytes 计量而 envelope 门禁按 JSON.stringify 序列化字节计量,转义膨胀使各项分预算全合规的文档仍可能突破 32MB envelope 上限(实测 38.7MB 抛 envelope_budget_exceeded);(5) 链接包裹的内联 data 图片经 sanitizeMarkdownNavigableUrls 的 label 回退拼接生成新的远程图片,最终自校验抛 invalid_markdown_image。

失败场景: Windows 盘符工作目录的会话、长会话(>8MB 可见文本)、含 JSON 转义密集工具输出或含 img 徽章式 markdown 的 transcript,导出时硬失败而非降级 — 与模块对其余不安全输入的降级语义(占位符+诊断+truncated)矛盾。

建议修复: 让生产者输出永远满足自身校验器:safePath 对裸盘符返回 '[path]';optionalLabel/url 在预算会产生 '' 时降级为省略占位(如 '[link omitted]');range 经 safeCount 规整或省略;预算计量与 envelope 采用同一单位(预留序列化膨胀余量);link-label 拼接不再制造新图片。

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

Comment on lines +2109 to +2110
const indentedCode =
fence === undefined && marker === undefined && /^(?: {4}|\t)/.test(line);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Class finding — the export gate hand-rolls a markdown mini-parser (splitMarkdownFenceSegments + link/image/autolink regexes) to re-derive what a renderer will render over untrusted cross-host documents; CommonMark + inline HTML is an unbounded entrance space. Executed bypasses: (1) a 4-space-indented line is classified indentedCode and skipped, but CommonMark forbids an indented code block from interrupting a paragraph — the lazy-continuation javascript: link / animated GIF is accepted while the identical flat shape is rejected; (2) whitespace around a link/image destination (legal in CommonMark) bypasses the URL gate and the image approval/animation/raster checks; (3) an escaped bang \! falls through both the link model ((?<!!)) and the image model (isEscapedMarkdownSyntax) yet renders as a literal ! followed by an active link; (4) <javascript:…> autolinks, raw HTML (a/iframe/svg) and backtick-in-fence-info shapes likewise pass. The bundled web-shell renderer neutralizes javascript: hrefs itself (defense in depth holds), but the proven defect is this gate, which the PR certifies in place of renderer judgment. — Failure scenario: a cross-host document carrying hello\n [click](javascript:alert(1)), [x]( javascript:alert(1) ), \![x](javascript:alert(1)), or a whitespace-padded animated-GIF data URI passes exportDocumentToTranscriptBlocks/assertExportTranscriptDocumentV1 with empty diagnostics and complete: true — the gate admits exactly the payloads its own invalid_markdown_url/invalid_markdown_image rules exist to reject.

Witness (executed):

F5-GATE-INDENTED: ACCEPTED vs F5-GATE-FLAT: REJECTED invalid_markdown_url (same semantics); N8 padded javascript: link and padded animated GIF: preserved=true, diagnostics=[], complete=true; N9 escaped-bang payload: preserved=true, diagnostics=[], complete=true; marked 15.0.12 renders the lazy continuation as <a href="javascript:alert(1)">

Suggested fix: Stop re-deriving rendering semantics with regexes: parse each markdown text field with the authoritative grammar consumers already depend on (the micromark/remark AST in the dependency tree) and validate structurally — every link/image/autolink destination must pass normalizeNavigableUrl/parseApprovedImageDataUrl, html nodes fail closed, fence metadata respects the rich-render budget. That converts the unbounded enumeration into a closed check over finite node types.

中文说明

类级发现:导出契约门禁用手写 markdown 迷你解析器(splitMarkdownFenceSegments + 链接/图片/autolink 正则)重新推导"渲染器会渲染什么",而 CommonMark+内联 HTML 是无界入口面,已执行验证出多个绕过:(1) 4 空格缩进行被当作 indented code 跳过检查,但 CommonMark 规定缩进代码块不能打断段落——惰性续行中的 javascript: 链接/动画 GIF 被门禁接受(对照的非缩进形态被正确拒绝);(2) 链接/图片目标与括号间的空白(CommonMark 允许)同时绕过 URL 门禁与图片审批/动画拒绝/字节计量;(3) 转义感叹号 ! 同时躲过链接模型((?<!!) 环视)与图片模型(isEscapedMarkdownSyntax),CommonMark 渲染为字面 ! 加活跃链接;(4) javascript:… autolink、原始 HTML(a/iframe/svg)与围栏 info 含反引号等形态同样绕过。捆绑的 web-shell 渲染器自身会中和 javascript: href(纵深防御成立),但被验证的缺陷是这个 PR 用以替代渲染器判断的契约门禁本身。

失败场景: 跨宿主文档携带 hello\n [click](javascript:alert(1))[x]( javascript:alert(1) )\![x](javascript:alert(1)) 或空白包裹的动画 GIF data URI 时,通过 exportDocumentToTranscriptBlocks/assertExportTranscriptDocumentV1 且诊断为空、complete:true——门禁放行了它自己的 invalid_markdown_url/invalid_markdown_image 规则意图拦截的载荷。

建议修复: 停止用正则重推导渲染语义:用消费方已依赖的权威语法(依赖树中的 micromark/remark AST)解析每个 markdown 文本字段并做结构化校验——遍历 AST,要求所有 link/image/autolink 目标通过 normalizeNavigableUrl/parseApprovedImageDataUrl,html 节点 fail-closed,围栏元数据遵循富渲染预算。无界枚举即被转换为对有限节点类型的封闭检查。

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

});
})
.join('');
const richTaskSafeValue = resourceSafeValue.replace(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The rich-render budget pass re-parses the rejoined text with its own fence regex instead of reusing splitMarkdownFenceSegments, and its counting disagrees with both the validator and the renderer: (1) fence-looking lines that are literal content inside an already-open code block are counted as render tasks and, over budget, rewritten — injecting [source fallback: js] text that was never in the conversation while truncated stays false; (2) the producer counts ```text/```plain/```plaintext fences against maxRichRenderTasks, while the validator's recount and the renderer (which gives those languages no rich processing) both exclude them — real rich fences (js/mermaid) are demoted up to 100 tasks early. — Failure scenario: a document with >100 info-string fences (e.g. a transcript quoting js examples inside a md block): user-visible [source fallback: …] garbage inside literal code samples, a spurious rich_render_budget_exceeded warning, and premature demotion of genuinely rich fences.

Witness (executed):

probe: 100 ```text fences + 1 ```js fence -> the js fence demoted to "```text [source fallback: js]" (observed verbatim in the exported block text) with rich_render_budget_exceeded emitted, while the document still passed its own self-assert (validator counts only truly-rich fences)

Suggested fix: Make splitMarkdownFenceSegments the single authoritative fence parse: record which lines are genuine fence openers (marker seen while no fence is open) and run the rich-render count/demotion over that record — rewriting only real opening lines' info strings — excluding text/plain/plaintext consistently with the validator and renderer.

中文说明

富渲染预算通道用自己的围栏正则重新解析拼接后的文本,而不是复用 splitMarkdownFenceSegments,且计数口径与校验器/渲染器不一致:(1) 已开启代码块内部的"围栏样"字面内容行被当作渲染任务计数、超预算时被改写——向导出文本注入从未存在过的 "text [source fallback: js]" 内容而 truncated 保持 false;(2) 生产者把 text/plain/plaintext 围栏计入 maxRichRenderTasks,而校验器的重算与渲染器(resolveFenceLanguage 对其不做富处理)都排除这三种语言——两把预算尺不一致,真实的富围栏(js/mermaid)被提前降级。

失败场景: 含 100+ 信息串围栏(含文本中引用 ```js 代码示例的 markdown 教程类转录)的文档:字面代码示例中被注入 [source fallback: …] 垃圾文本、发出虚假的 rich_render_budget_exceeded 警告,且 js/mermaid 围栏最多多提前 100 个任务被降级。

建议修复: 让 splitMarkdownFenceSegments 成为唯一权威围栏解析:记录真正的围栏开启行(在 fence === undefined 时见到 marker),富渲染计数/降级只在该记录上进行;并在计数中排除 text/plain/plaintext(与校验器和渲染器一致)。

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

Comment on lines +389 to +391
const VISIBLE_SYSTEM_RECORD_SUBTYPES = new Set([
'notification',
'cron',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] VISIBLE_SYSTEM_RECORD_SUBTYPES omits 'slash_command', but the replay machine (projectSystemRecord, transcript-replay.ts:967-1008) does project slash_command records as visible assistant messages (/about, /help output and goal cards recorded via restoreGoal.ts:235). The policy rejects those records before projection with only an info-severity diagnostic and complete unaffected — contradicting the design doc §8.2 "若原本用户可见则门禁失败". — Failure scenario: every export containing slash-command output silently drops it: probe shows the tail slash record absent from the export with diagnostics record_internal_excluded (info) and complete: true, truncated: false; with a later record parented on it, a misattributed causal_record_excluded error instead — while the canonical projection with the record accepted renders the content as an assistant block.

Witness (executed):

N3(tail): contains ABOUT_VISIBLE_OUTPUT=false | diagnostics=[{code:record_internal_excluded,severity:info}] | complete=true; N3b canonical projection: contains=true; N3(later record): causal_record_excluded error

Suggested fix: Add 'slash_command' to VISIBLE_SYSTEM_RECORD_SUBTYPES, or emit an affectsCompleteness diagnostic whenever a projection-capable subtype is excluded.

中文说明

VISIBLE_SYSTEM_RECORD_SUBTYPES 遗漏 'slash_command',但回放机(transcript-replay.ts projectSystemRecord)确实把 slash_command 记录投影为可见 assistant 消息(/about、/help 输出与 restoreGoal.ts 记录的 goal 卡片)。策略在投影前以 info 级诊断拒绝这些记录且 complete 不受影响——与设计文档 §8.2 "若原本用户可见则门禁失败" 直接矛盾。

失败场景: 任何含斜杠命令输出的 transcript 导出时静默丢失这些内容:探针实测尾部 slash 记录 -> 输出缺失、diagnostics 仅 record_internal_excluded(info)、complete:true truncated:false;若其后还有记录则误报 causal_record_excluded(error)。而同一记录走规范投影时确实渲染为 assistant 块(contains ABOUT_VISIBLE_OUTPUT: true)。

建议修复: 将 'slash_command' 加入 VISIBLE_SYSTEM_RECORD_SUBTYPES;或对任何可投影但被排除的 subtype 发出影响完整性(affectsCompleteness)的诊断。

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

"complete": { "type": "boolean" },
"truncated": { "type": "boolean" },
"projectName": { "type": "string", "maxLength": 400 },
"repository": { "type": "string", "maxLength": 200 },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The pinned schema caps metadata.repository at 200 chars, but the runtime import gate (assertMetadataisSafeRepositoryisSafeDisplayUrl) applies no length cap to http(s) URLs — the machine-readable contract is stricter than the gate it mirrors, so schema-conformant documents are rejected on import. (The producer side is already safe: over-long gitRepo degrades to '[link omitted]'; the drift direction is gate-accepts/schema-forbids.) — Failure scenario: a cross-host document whose repository is a 248-char https URL: schema validation rejects (maxLength 200) while the runtime gate accepts — consumers implementing against the schema disagree with runtime behavior.

Witness (executed):

probe: 248-char repository -> unmodified PR accepts (runtimeError=none) while ajv against the pinned schema rejects; adding isSafeLabel(value,200) to assertMetadata flips to invalid_metadata (reverted)

Suggested fix: Add a length cap to isSafeRepository's URL branch (e.g. isSafeLabel(value, 200)) to align with the schema.

中文说明

固定 schema 把 metadata.repository 上限设为 200 字符,但运行时导入门禁(assertMetadata -> isSafeRepository -> isSafeDisplayUrl)对 http(s) URL 无长度上限——机器可读契约比它镜像的运行时门禁更严格:schema 合规文档在导入时被拒绝。(生产者侧已有防护:createExportTranscriptDocumentV1 对超长 gitRepo 输出 '[link omitted]',漂移方向是门禁接受/schema 拒绝。)

失败场景: repository 为 248 字符 https URL 的跨宿主文档:schema 校验拒绝(maxLength 200),运行时门禁接受——契约两面不一致,消费方按 schema 实现会与运行时行为冲突。

建议修复: 在 isSafeRepository 的 URL 分支加长度上限(如 isSafeLabel(value, 200)),与 schema 对齐。

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

Comment on lines +1809 to +1811
block.kind === 'tool' &&
(block.status === 'completed' || block.status === 'failed') &&
block.resultPreview === undefined

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The "completed/failed tool without resultPreview" branch of hasExplicitContentLoss in assertDocumentConsistency is unreachable: assertExportBlock (line 310 loop) hard-rejects that exact shape before assertDocumentConsistency (line 328) runs — the two validation layers encode contradictory semantics for one shape (layer 1: invalid, throw; layer 2: degradable explicit content loss), and only the "invalid" layer can fire. (The serializer independently never emits the shape — it substitutes '[tool result omitted from export]' — which is why the contradiction stayed latent.) — Failure scenario: if a future change relaxes assertExportBlock for that shape (e.g. to allow degradation), the contradiction becomes live immediately; today the dead branch masks the design disagreement.

Suggested fix: Delete the unreachable branch, or relax assertExportBlock for the shape and let assertDocumentConsistency enforce complete: false/truncated: true (if degradable content loss is the intent).

中文说明

assertDocumentConsistency 的 hasExplicitContentLoss 中 "completed/failed 工具无 resultPreview" 分支不可达:同一 assertExportTranscriptDocumentV1 管线中 assertExportBlock(第 310 行循环)先于 assertDocumentConsistency(第 328 行)对该形状硬抛 invalid_block——两个验证层对同一形状编码矛盾语义(一层:非法,抛错;另一层:可降级的显式内容丢失),只有"非法"层可能触发。(序列化器独立地从不产出该形状——替换为 '[tool result omitted from export]'——因此矛盾一直潜伏。)

失败场景: 维护者若使 assertExportBlock 放宽该形状(例如允许降级),assertDocumentConsistency 的语义与其冲突将立即显形;当前该死分支掩盖了两层设计矛盾。

建议修复: 删除不可达分支,或让 assertExportBlock 放宽该形状并由 assertDocumentConsistency 强制 complete:false/truncated:true(若其本意是可降级内容丢失)。

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

Comment on lines 1066 to 1070
function appendSubTool(
parent: DaemonMessageToolCall,
toolCall: DaemonMessageToolCall,
includeSourceIdentity: boolean,
): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] A parent (sub-agent) tool's sourceBlockIds closure is only populated at appendSubTool time; block ids later added to a subtool by mergeToolCall(..., {collectSourceIdentity}) never propagate back to the parent tool, and synchronizeToolGroupSourceIdentity repairs only the message level — so the parallel-agents probe evidence (transcriptRenderProbe.ts:72-77 reads the agent tool's sourceBlockIds directly) under-attributes rendered content. — Failure scenario: a permission block merged into a subtool by callId has its rendered effect (subtool status/permission history inside the agent card) present in the UI but absent from the probe evidence's sourceBlockIds — the exact-coverage contract fails (or must be loosened) for such transcripts.

Witness (executed):

probe: messageLevel includes permission-block; parentToolLevel misses it; probeEvidence (parallel_agents item) misses it — a parent-propagation fix arm makes all three levels agree (144/144 green, fix reverted)

Suggested fix: Track the parent at appendSubTool and re-union subtool ids into the parent's sourceBlockIds after each mergeToolCall (or have the probe read the message-level union).

中文说明

父(sub-agent)工具的 sourceBlockIds 闭包只在 appendSubTool 时填充;之后经 mergeToolCall(..., {collectSourceIdentity}) 加到子工具上的块 id 从不回传父工具,synchronizeToolGroupSourceIdentity 只修复消息层——并行代理探针证据(transcriptRenderProbe.ts:72-77 直接读代理工具的 sourceBlockIds)对已渲染内容归属不足。

失败场景: 权限块经 callId 并入子工具后,其渲染效果(代理卡片内的子工具状态/权限历史)出现在界面却不在探针证据的 sourceBlockIds 中——精确覆盖契约对这类转录失败或被迫放宽。

建议修复: 在 appendSubTool 记录父引用,并在每次 mergeToolCall 后把子工具新增 id 回并入父工具 sourceBlockIds(或让探针改读消息层并集)。

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

Comment on lines +677 to +679
expect(renderedSourceBlockIds).toEqual(
new Set(exportDocument.blocks.map((block) => block.id)),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The exact-coverage contract (every source block id must appear in some rendered item's sourceBlockIds) has no exclusion mechanism, but several block shapes are deliberately never rendered in document/safe mode: empty assistant blocks without usage (if (!textBlock.text && !textBlock.usage) break;), unresolved permission blocks (pending-permission UI is live-only; WebShellTranscript passes pendingApproval={null}), and AskUserQuestion permission blocks (permissionBlockToToolCall returns undefined; rememberPermissionToolInfo never ledgers the permission block's own id). The export schema explicitly supports permission blocks, so real exports can carry these shapes. (One correction: thought blocks are NOT dropped — an empty thought still yields a thinking message carrying sourceBlockIds.) — Failure scenario: any fixture or real transcript containing one of these shapes fails the gate with no way to express "deliberately unrendered"; today only fixtures avoiding these shapes pass.

Witness (executed):

probe: 5 blocks including all three shapes -> rendered ids miss [empty-assistant, pending-permission, ask-user-permission]

Suggested fix: Ledger deliberately-unrendered blocks onto an explicit evidence field (e.g. unrenderedBlockIds) the gate subtracts, or scope the assertion to renderable blocks — then add fixture cases for each skipped shape.

中文说明

精确覆盖契约(每个源块 id 必须出现在某个渲染项的 sourceBlockIds)没有排除机制,但若干块形状在 document/safe 模式下刻意不渲染:无 usage 的空 assistant 块(if (!textBlock.text && !textBlock.usage) break;)、未解决权限块(pending 权限 UI 仅 live;WebShellTranscript 传 pendingApproval={null})、AskUserQuestion 权限块(permissionBlockToToolCall 返回 undefined,rememberPermissionToolInfo 从不记录权限块自身 id)。导出 schema 明确支持 permission 块,真实导出可携带这些形状。(一处更正:thought 块不被丢弃——空 thought 仍产生带 sourceBlockIds 的 thinking 消息。)

失败场景: 任何含上述形状的夹具/真实转录使门禁失败且无法表达"刻意不渲染";当前只有避开这些形状的夹具能通过。

建议修复: 把刻意不渲染的块记入显式证据字段(如 unrenderedBlockIds)供门禁扣除,或把断言限定到可渲染块;并为每种被跳过形状增加夹具用例。

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

Comment on lines +16 to +19
:global([data-transcript-render-mode='document']) .message {
max-height: none;
overflow: visible;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The document-mode override un-clips the shell-message container (.message) but leaves its .command child with overflow: hidden; text-overflow: ellipsis; white-space: nowrap — long shell commands are truncated to one ellipsized line in exported documents while the output below is fully expanded and every sibling surface (DiffView .lines, SubAgentPanel .stream/.scrollWindow, ToolChrome .expandedOutput, the MessageList viewport) is deliberately un-clipped by this same change; the command's only render site is this span (no title or alternate full-text path). — Failure scenario: a transcript document containing a long command (docker/npm/git with many flags — routinely 100+ chars at document widths) renders npm install --save pkg-a pkg-b … with an ellipsis; the full command is unrecoverable from the document.

Suggested change
:global([data-transcript-render-mode='document']) .message {
max-height: none;
overflow: visible;
}
:global([data-transcript-render-mode='document']) .message {
max-height: none;
overflow: visible;
}
:global([data-transcript-render-mode='document']) .command {
overflow: visible;
text-overflow: unset;
white-space: pre-wrap;
}
中文说明

document 模式覆盖解开了 shell 消息容器(.message),但其 .command 子元素仍保持 white-space:nowrap; overflow:hidden; text-overflow:ellipsis 且文件内无任何覆盖——长 shell 命令在导出文档中被截断为单行带省略号,而同一改动把下方 .output(pre-wrap)及所有兄弟表面(DiffView .lines、SubAgentPanel .stream/.scrollWindow、ToolChrome .expandedOutput、MessageList viewport)完全展开;命令的唯一渲染点就是该 span(无 title 或全文替代路径)。

失败场景: 含长命令(docker/npm/git 多参数,文档宽度下常见 100+ 字符)的转录文档:头部渲染 'npm install --save pkg-a pkg-b …' 带省略号,完整命令在文档中不可恢复。

建议修复: 扩展同一 document 覆盖块::global([data-transcript-render-mode='document']) .command { overflow: visible; text-overflow: unset; white-space: pre-wrap; }

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

@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

1 similar comment
@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code attempted to resolve merge conflicts but the run did not complete successfully.

Check the workflow run for full logs.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Establish cross-host chat transcript contract prevalidation

4 participants