Skip to content

feat(webshell): replay ChatRecord history in readonly WebShell - #6999

Merged
wenshao merged 14 commits into
QwenLM:mainfrom
cxruan:feat/chat-record-webshell-transcript
Jul 19, 2026
Merged

feat(webshell): replay ChatRecord history in readonly WebShell#6999
wenshao merged 14 commits into
QwenLM:mainfrom
cxruan:feat/chat-record-webshell-transcript

Conversation

@cxruan

@cxruan cxruan commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR adds a shared, deterministic replay pipeline that converts persisted ChatRecord history into daemon transcript blocks. It selects the active parent chain, preserves source-record provenance, aggregates streamed fragments, correlates tool calls with results, finalizes incomplete tools, and reports gaps, truncation, and unsupported content through explicit diagnostics.

The same replay semantics are reused by daemon history loading and exposed through an opt-in TypeScript SDK entry for offline consumers. A public readonly WebShell transcript renderer accepts the projected blocks, reuses the existing message list and styling, and works without daemon or session providers, composer controls, approval flows, or session mutation callbacks.

The storage, projection, and rendering boundaries remain separate: Core prepares persisted records, the ACP replay layer emits protocol updates, the SDK reduces those updates into transcript blocks, and WebShell only renders the resulting blocks.

Why it's needed

Persisted sessions are stored as ChatRecord JSONL, but SDK and browser consumers previously could not reproduce the daemon transcript without depending on the Core session service, filesystem behavior, or a separate copy of the replay rules. That made active-branch selection, fragmented output, tool correlation, malformed history, and truncation easy to handle inconsistently.

This change provides one canonical replay path for daemon loading and offline projection while keeping raw storage records out of the WebShell API. Hosts can parse their own JSONL, project the resulting ChatRecord array through the SDK, and render it as a readonly chat without connecting to or mutating a daemon session.

Reviewer Test Plan

How to verify

  1. Project a ChatRecord sequence containing a user message, assistant thought, tool call, tool result, and fragmented assistant response. Confirm that the resulting blocks preserve order and source-record IDs, merge same-record fragments, correlate the tool result with its call, and report a complete projection.
  2. Add an abandoned rewind branch and confirm that only the newest active parent chain is replayed.
  3. Remove a parent, add malformed or unknown parts, leave a tool call without a result, and apply a small block limit. Confirm that diagnostics are returned, completeness becomes false, truncation is reported when applicable, and dangling tools are finalized as failed.
  4. Load the same history through the existing daemon replay path and confirm that its canonical updates remain aligned with the offline SDK projection.
  5. Render the projected blocks with the readonly WebShell entry outside all daemon providers. Confirm that messages, Markdown, thinking, tools, plans, status, and timeline content render normally; composer, approval, retry, branch, and session-mutation controls remain absent; and local reading interactions such as copy, collapse, sorting, and filtering still work.
  6. Confirm that readonly session links and goal-status content do not emit global session or goal navigation events, while the existing interactive WebShell behavior remains unchanged.

Automated verification completed locally: targeted Core record-preparation tests, ACP replay and compaction tests, CLI replay/conformance tests, and SDK projection/public-surface tests passed. The complete WebShell Vitest suite passed with 98 test files and 1,590 tests, the existing WebShell Playwright suite passed 6 of 6 tests, package lint/typecheck/build passed, root build/typecheck passed, and staged-file Prettier and ESLint checks passed during commit.

Evidence (Before & After)

Before: consumers holding persisted ChatRecord JSONL had to depend on Core session loading or reproduce internal replay semantics, and WebShell transcript rendering was only reachable through a fully connected session UI.

After: consumers can obtain deterministic transcript blocks with completeness and diagnostic metadata through the opt-in SDK projection, then render those blocks through the public readonly WebShell entry without daemon providers or mutation surfaces.

Kapture.2026-07-16.at.10.47.12.mp4

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS with Node.js v24.14.1 and npm 11.11.0. WebShell browser regression used the repository Playwright configuration with local Vite and the existing fake daemon. Root and package builds ran directly on the host.

Risk & Scope

  • Main risk or tradeoff: The shared replay machinery is consumed by daemon history loading, CLI replay, and offline SDK projection, so changes to record preparation or event reduction can affect multiple transcript consumers. Cross-package conformance and regression tests cover the shared boundaries.
  • Not validated / out of scope: A real persisted session was not replayed end to end through both daemon /load response and stream modes during local verification. Windows and Linux were not tested locally and are expected to be covered by CI. JSONL parsing, file selection, persistence, pagination, and live streaming remain host responsibilities.
  • Breaking changes / migration notes: None. The SDK projection and readonly WebShell renderer are opt-in additions. Existing WebShell APIs, daemon protocols, interactive defaults, and session-loading callers retain their current behavior.

Linked Issues

N/A

中文说明

本 PR 做了什么

本 PR 新增了一条共享且确定性的重放链路,将持久化的 ChatRecord 历史转换为 daemon transcript blocks。该链路会选择当前有效的父子链、保留来源 record 的追踪信息、聚合流式片段、关联工具调用与结果、结束未完成的工具,并通过明确的 diagnostics 报告历史缺口、截断和不支持的内容。

同一套重放语义同时用于 daemon 历史加载,并通过可选的 TypeScript SDK 入口提供给离线消费者。公开的 WebShell 只读 transcript renderer 接收投影后的 blocks,复用现有消息列表和样式,并且无需 daemon 或 session providers、composer 控件、审批流程或 session mutation callbacks。

存储、投影和渲染边界保持分离:Core 准备持久化 records,ACP 重放层产生协议 updates,SDK 将这些 updates 归约为 transcript blocks,WebShell 只负责渲染最终 blocks。

为什么需要它

持久化 session 使用 ChatRecord JSONL 存储,但 SDK 和浏览器消费者此前无法在不依赖 Core session service、文件系统行为或自行复制重放规则的情况下复现 daemon transcript。这容易导致 active branch 选择、分片输出、工具关联、异常历史和截断处理不一致。

本改动为 daemon 加载和离线投影提供唯一的规范重放链路,同时避免将原始存储 records 暴露给 WebShell API。宿主可以自行解析 JSONL,通过 SDK 投影得到 ChatRecord 数组对应的 blocks,然后在不连接或修改 daemon session 的情况下将其渲染为只读 chat。

Reviewer 测试计划

如何验证

  1. 投影一组包含用户消息、assistant thinking、工具调用、工具结果和分片 assistant 回复的 ChatRecord。确认生成 blocks 的顺序和来源 record IDs 正确,同一 record 的片段被合并,工具结果与调用正确关联,并且投影标记为完整。
  2. 添加一条已放弃的 rewind 分支,确认只重放最新的有效父子链。
  3. 删除父 record、添加 malformed 或未知 parts、保留没有结果的工具调用,并设置较小的 block 上限。确认返回 diagnostics,完整性变为 false,在适用时报告截断,并将悬空工具结束为 failed。
  4. 通过现有 daemon 重放路径加载相同历史,确认其规范 updates 与离线 SDK 投影保持一致。
  5. 在所有 daemon providers 之外使用 WebShell 只读入口渲染投影 blocks。确认消息、Markdown、thinking、工具、plan、status 和 timeline 内容正常展示;composer、审批、重试、分支和 session mutation 控件不存在;复制、折叠、排序和筛选等本地阅读交互仍可使用。
  6. 确认只读 session links 和 goal-status 内容不会发送全局 session 或 goal 导航事件,同时现有交互式 WebShell 行为保持不变。

本地自动化验证已完成:Core record preparation 定向测试、ACP replay 和 compaction 测试、CLI replay/conformance 测试以及 SDK projection/public-surface 测试通过。完整 WebShell Vitest 测试集通过,共 98 个测试文件、1,590 个测试;现有 WebShell Playwright 测试 6/6 通过;package lint/typecheck/build 通过;root build/typecheck 通过;提交期间 staged files 的 Prettier 和 ESLint 检查通过。

证据(改动前与改动后)

N/A — 本 PR 新增 library 和 SDK 集成入口,不修改现有独立终端用户页面。DOM 集成测试验证了 WebShell 只读输出和交互边界。

改动前:持有 ChatRecord JSONL 的消费者必须依赖 Core session 加载或自行复现内部重放语义,并且 WebShell transcript 渲染只能通过完整连接的 session UI 使用。

改动后:消费者可以通过可选 SDK 投影获得带完整性和 diagnostic 元数据的确定性 transcript blocks,然后通过公开 WebShell 只读入口渲染这些 blocks,无需 daemon providers 或 mutation surfaces。

Kapture.2026-07-16.at.10.47.12.mp4

测试平台

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

macOS,Node.js v24.14.1,npm 11.11.0。WebShell 浏览器回归使用仓库 Playwright 配置、本地 Vite 和现有 fake daemon。root 和 package build 直接在宿主机运行。

风险与范围

  • 主要风险或权衡:共享重放逻辑同时被 daemon 历史加载、CLI replay 和离线 SDK projection 消费,因此 record preparation 或 event reduction 的变更可能影响多个 transcript 消费方。跨 package conformance 和 regression tests 覆盖了这些共享边界。
  • 未验证 / 不在范围内:本地验证未使用真实持久化 session 对 daemon /load response 和 stream 两种模式执行完整端到端重放。Windows 和 Linux 未在本地测试,预计由 CI 覆盖。JSONL 解析、文件选择、持久化、分页和实时流仍由宿主负责。
  • 破坏性变更 / 迁移说明:无。SDK projection 和 WebShell 只读 renderer 均为可选新增能力。现有 WebShell APIs、daemon protocols、interactive defaults 和 session-loading callers 保持当前行为。

关联 Issues

N/A

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

No screenshot changes against the PR base.

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

Qwen Code · web-shell visuals

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 16, 2026
Keep vision disclosures and assistant usage visible across tool replay boundaries.

Propagate plan tool-call identity through daemon projection and provide Todo contexts in WebShell so snapshots remain independent. Accept session_source records and cover the cross-layer behavior with regression tests.
qwen-code-ci-bot pushed a commit that referenced this pull request Jul 16, 2026
@wenshao
wenshao marked this pull request as ready for review July 18, 2026 01:40
@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

…ion-aware pagination

Merge origin/main into feat/chat-record-webshell-transcript.

- history-replay-page.ts: combined the PR's TranscriptReplayMachine-based
  replayer (TranscriptReplayStateV1 in cursor) with main's direction-aware
  pagination (backward skips pending tool calls and replay state in cursor).
  Also adopted main's activeRecordId tracking in replayContext for
  record-ID-correlated updates.
- history-replayer.ts: replaced the PR's type-cast workaround for
  setActiveRecordId with main's direct ctx.setActiveRecordId call, since
  main added it to SessionEmitterContext.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Merge Conflict Resolution Summary — PR #6999

Base branch

main

Conflicted files

  1. packages/cli/src/acp-integration/session/history-replay-page.ts (2 conflict regions)
  2. packages/cli/src/acp-integration/session/history-replayer.ts (1 conflict region)

What each side brought

PR branch (feat/chat-record-webshell-transcript)

  • Rewrote HistoryReplayer to delegate to a TranscriptReplayMachine from @qwen-code/acp-bridge/transcriptReplay instead of handling each record type manually.
  • replayPage() now returns HistoryReplayPageState with a replay: TranscriptReplayStateV1 field (the full machine snapshot).
  • replayTranscriptRecordPage() stores the full TranscriptReplayStateV1 in the cursor's replay field.
  • Added backward-compatible parsing of both legacy and current pending tool call formats in parseTranscriptReplayState.
  • Used a type-cast workaround for setActiveRecordId (not yet in the SessionEmitterContext type on the PR branch).

Main (origin/main)

  • Added page.direction awareness to paged replay: backward pages skip pending tool calls and always finalize dangling, and don't encode replay state in the cursor.
  • Added setActiveRecordId to the SessionEmitterContext type.
  • Added activeRecordId tracking in replayContext so updates get stamped with qwen.session.recordId in _meta.

Resolutions

history-replay-page.ts — Conflict 1 (replayPage call options)

Kept the PR's replayPageState variable (which extracts .replay from the machine-based result) and applied main's page.direction guards:

  • pendingToolCalls: page.direction === 'backward' ? [] : state.pendingToolCalls
  • finalizeDangling: page.direction === 'backward' || !page.hasMore

history-replay-page.ts — Conflict 2 (cursor replay encoding)

Combined both: the PR's replayState (TranscriptReplayStateV1) replaces main's legacy { pendingToolCalls, cumulativeUsage } object, but is still conditional on forward direction per ma

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @cxruan!

Template looks good ✓ — all required sections present and well-filled.

Problem: This is a feature addition, not a bug fix. The stated need — SDK and browser consumers can't replay persisted ChatRecord history without depending on Core session internals — is real. Today, anyone wanting to render a stored session has to either replicate replay semantics or pull in the full Core session service, which is an unnecessary coupling for offline/readonly use cases.

Direction: The separation of concerns (Core prepares records → ACP replays → SDK projects → WebShell renders) is architecturally sound. A canonical replay path shared by daemon loading and offline consumers is the right instinct. Session replay and SDK transcript access are clearly within qwen-code's scope.

Size: This is a large cross-package feature. Breakdown:

  • Core production lines: ~565 added / ~281 deleted (846 lines) — transcript-records.ts (494 lines) is the bulk
  • Cross-package production: ~2,772 lines across cli, acp-bridge, sdk-typescript, web-shell
  • Tests: ~2,189 lines (good coverage ratio)
  • Design docs: ~1,167 lines
  • Total: 6,541 added / 1,099 deleted across 55 files and 5 packages

This triggers the 1000+ line advisory (non-blocking) and the 500+ core lines maintainer awareness flag. The test-to-production ratio is healthy, and the design docs show thoughtful planning.

Approach: The layered architecture (storage → projection → rendering) is well-considered. The transcript-records.ts module in Core is a substantial new addition but justified as a reusable replay primitive. The readonly WebShell renderer correctly strips all mutation surfaces while reusing the existing MessageList and portal infrastructure. Moving on to code review. 🔍

中文说明

感谢贡献,@cxruan

模板完整 ✓ — 所有必需章节齐全且填写完善。

问题: 这是一个功能新增,不是 bug 修复。所述需求 — SDK 和浏览器消费者无法在不依赖 Core session 内部实现的情况下重放持久化的 ChatRecord 历史 — 是真实存在的。目前,任何想要渲染已存储 session 的人要么需要复制重放语义,要么需要引入完整的 Core session service,这对于离线/只读用例来说是不必要的耦合。

方向: 关注点分离(Core 准备 records → ACP 重放 → SDK 投影 → WebShell 渲染)在架构上是合理的。daemon 加载和离线消费者共享一条规范的重放链路是正确的思路。Session 重放和 SDK transcript 访问显然在 qwen-code 的范围内。

规模: 这是一个大型跨 package 功能。分解如下:

  • Core 生产代码行数: ~565 新增 / ~281 删除(846 行)— transcript-records.ts(494 行)占大头
  • 跨 package 生产代码: ~2,772 行,涵盖 cliacp-bridgesdk-typescriptweb-shell
  • 测试: ~2,189 行(覆盖率良好)
  • 设计文档: ~1,167 行
  • 总计: 55 个文件、5 个 package,6,541 行新增 / 1,096 行删除

触发了 1000+ 行建议(非阻塞)和 500+ Core 行数维护者关注标记。测试与生产代码比例健康,设计文档显示了深思熟虑的规划。

方案: 分层架构(存储 → 投影 → 渲染)考虑周全。Core 中的 transcript-records.ts 模块虽然较大,但作为可复用的重放原语是合理的。只读 WebShell 渲染器正确去除了所有 mutation 面,同时复用了现有的 MessageList 和 portal 基础设施。进入代码审查 🔍

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code Review

The architecture across this PR is well-layered — Core prepares records (transcript-records.ts), the ACP bridge runs a stateful replay machine (transcript-replay.ts), the SDK projects to transcript blocks (chat-record-transcript.ts), and WebShell renders readonly (WebShellTranscript.tsx). Each boundary is clean and the reuse of existing primitives (MessageList, reduceDaemonTranscriptEvents, walkTranscriptUuidChain) is good.

The previous build-blocking type error (in session-transcript-reader.ts, where UuidIndexEntry.subtype was narrower than TranscriptRecordInput.subtype) has been resolved by widening UuidIndexEntry.subtype to TranscriptRecordInput['subtype']. This is correct and safe — values are always validated by validateTranscriptRecord before reaching the index.

Build and typecheck pass cleanly across all packages. CI is fully green (Test, web-shell E2E Smoke, Capture web-shell visuals all pass).

Key implementation findings:

  • transcript-records.ts (494 lines): pure validation, chain-walking, and fragment aggregation functions with thorough diagnostic reporting. The KNOWN_RECORD_SUBTYPES set correctly covers all 17 ChatRecord subtypes. Validation rejects unknown types and malformed records with proper diagnostics.
  • transcript-replay.ts (1252 lines): the DefaultTranscriptReplayMachine is large but the state machine complexity is inherent to the problem — tool call tracking, ID deduplication, gap presentation, and usage accumulation all need to work together. The presentation adapter pattern cleanly separates formatting from logic.
  • WebShellTranscript.tsx (288 lines): readonly renderer correctly strips all mutation surfaces — pendingApproval={null}, isResponding={false}, no composer controls. Properly uses TranscriptRenderModeProvider with "readonly" value and portal infrastructure with CSS variable syncing.
  • Readonly mode enforcement is correctly implemented across three consumer sites: GoalStatusMessage suppresses global goal events, Markdown renders session links as plain spans (no qwen:open-session dispatch), and ToolGroup passes render mode through to session-link rendering in tool results.
  • conversation-chain.ts correctly delegates to the new Core primitives rather than duplicating logic.
  • Test coverage is comprehensive across all boundaries — 94 targeted new tests pass locally (8 core + 6 ACP bridge + 13 WebShell + 53 CLI + 14 SDK).

No correctness bugs, security issues, or regressions found.

Verification

Check Result
npm ci (build) ✅ pass
npm run typecheck ✅ pass (all packages)
Core transcript-records.test.ts ✅ 8/8 pass
ACP transcript-replay.test.ts ✅ 6/6 pass
WebShell WebShellTranscript.test.tsx + .dom.test.tsx ✅ 13/13 pass
CLI history-replayer.test.ts + .conformance.test.ts ✅ 53/53 pass
SDK daemon-transcript-projection.test.ts ✅ 14/14 pass
CI Test (ubuntu, Node 22) ✅ pass
CI web-shell E2E Smoke ✅ pass
CI Capture web-shell visuals ✅ pass
中文说明

代码审查

此 PR 的架构分层良好 — Core 准备 records(transcript-records.ts),ACP bridge 运行有状态的重放机器(transcript-replay.ts),SDK 投影为 transcript blocks(chat-record-transcript.ts),WebShell 进行只读渲染(WebShellTranscript.tsx)。每个边界都很清晰,对现有原语的复用做得很好。

之前的构建阻塞类型错误(session-transcript-reader.tsUuidIndexEntry.subtypeTranscriptRecordInput.subtype 更窄)已通过将 UuidIndexEntry.subtype 放宽为 TranscriptRecordInput['subtype'] 来解决。这是正确且安全的 — 值在到达索引之前始终由 validateTranscriptRecord 验证。

构建和类型检查在所有 package 中均通过。 CI 全部为绿色。

关键实现发现:

  • transcript-records.ts(494 行):纯函数验证、链路遍历和片段聚合,带有完善的诊断报告。
  • transcript-replay.ts(1252 行):虽然较大,但状态机复杂度是问题固有的。presentation 适配器模式干净地将格式化与逻辑分离。
  • WebShellTranscript.tsx(288 行):只读渲染器正确去除了所有 mutation 面。
  • 只读模式执行在三个消费站点正确实现:GoalStatusMessage 抑制全局 goal 事件,Markdown 将 session 链接渲染为纯文本 span,ToolGroup 将渲染模式传递给工具结果中的 session 链接渲染。
  • 未发现正确性 bug、安全问题或回归。

验证

所有构建、类型检查和测试均通过(见上方英文表格)。本地验证了 94 个针对性新增测试,CI 的 Test、web-shell E2E Smoke 和 Capture web-shell visuals 全部通过。

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the implementation is well-architected, all CI checks are green, and the build error from the previous review round has been resolved.

The replay pipeline design is sound — Core prepares records, ACP replays them as protocol updates, SDK projects to blocks, WebShell renders readonly. The separation of concerns is clean, the test coverage is comprehensive (94 new targeted tests + full CI suite green), and the readonly renderer correctly strips all mutation surfaces while suppressing global navigation events.

The previous blocking issue — a type incompatibility in session-transcript-reader.ts where UuidIndexEntry.subtype was narrower than the source type — has been cleanly resolved. Build and typecheck now pass across all packages.

Maintainer awareness flag still applies: this is a 55-file, 5-package feature with 846 core production lines. A maintainer should sign off on the scope before merge. The architecture is well-designed, but the breadth means downstream consumers across multiple packages should be aware of the new shared replay primitives.

Approving with the maintainer-awareness caveat. ✅

中文说明

置信度:4/5 — 实现架构良好,所有 CI 检查为绿色,上一轮审查中的构建错误已解决。

重放管道设计合理 — Core 准备 records,ACP 将其重放为协议 updates,SDK 投影为 blocks,WebShell 进行只读渲染。关注点分离清晰,测试覆盖全面(94 个新增针对性测试 + 完整 CI 套件为绿色),只读渲染器正确去除了所有 mutation 面并抑制了全局导航事件。

之前的阻塞问题 — session-transcript-reader.tsUuidIndexEntry.subtype 比源类型更窄的类型不兼容 — 已被干净地解决。所有 package 的构建和类型检查现在均通过。

维护者关注标记仍然适用:这是一个 55 个文件、5 个 package 的功能,包含 846 行 Core 生产代码。合并前需要维护者对范围进行确认。架构设计良好,但广度意味着多个 package 的下游消费者应了解新的共享重放原语。

附带维护者关注条件批准。✅

Qwen Code · qwen3.7-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Build is broken after the merge conflict resolution — tsc --build fails in the core package due to a type incompatibility in session-transcript-reader.ts (see my notes above). The implementation underneath looks solid; this should be a quick fix. 🙏

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Merge Conflict Resolution Summary — PR #6999

Conflict

File: packages/acp-bridge/src/compactionEngine.ts

The PR branch (feat/chat-record-webshell-transcript) and origin/main both added new standalone functions at the same location in the file (after extractParentToolCallIdFromMeta):

  • HEAD (PR): Added extractSourceRecordIdsFromMeta() and stringArraysEqual() — used by the transcript replay machine to track and compare source record IDs across text/thought slot merges.
  • origin/main: Added hasTodoStopGuardDiscreteMeta() — used by the todo stop guard feature to detect discrete messages that should bypass text-slot merging.

Resolution

Kept all three functions. They are independent features that both have call sites in the auto-merged portions of the file:

  • extractSourceRecordIdsFromMeta is called in mergeTextSlot() and mergeTranscriptUpdateMeta()
  • stringArraysEqual is called in mergeTextSlot() for comparing source record ID arrays
  • hasTodoStopGuardDiscreteMeta is called in classifySessionUpdate() under the agent_message_chunk case

No logic changes were made — the conflict was purely additive on both sides.

Commit

merge: resolve conflicts between transcript replay machine and direction-aware pagination

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs.

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Build failure root cause

All three failing CI jobs — Test, web-shell E2E Smoke, and Capture web-shell visuals — fail at the same step: the TypeScript build of packages/core. Because npm ci runs the prepare lifecycle script (npm run build), this compile error aborts dependency installation for every job, so no tests actually run. There is a single blocking compiler error:

src/services/session-transcript-reader.ts(824,35): error TS2345: Argument of type '{ ...; subtype?: string | undefined; ... }' is not assignable to parameter of type 'UuidIndexEntry'.
  Types of property 'subtype' are incompatible.
    Type 'string | undefined' is not assignable to type '"notification" | "slash_command" | "chat_compression" | ... | undefined'.

Why it happens

This PR adds packages/core/src/utils/transcript-records.ts and refactors session-transcript-reader.ts to build its UUID index from validateTranscriptRecord(...).record + isTranscriptConversationRecord(record), replacing the previous parseLineTolerant<ChatRecord>(...) + isChatRecord(record) path (where isChatRecord was a value is ChatRecord type guard).

  • The new TranscriptRecordInput.subtype is typed as a wide string (transcript-records.ts:29).
  • isTranscriptConversationRecord returns a plain boolean, not a type predicate, so it does not narrow record.
  • UuidIndexEntry.subtype is still ChatRecord['subtype'] — the narrow string-literal union (session-transcript-reader.ts:119).

So at session-transcript-reader.ts:824, record.subtype (string | undefined) is no longer assignable to UuidIndexEntry.subtype (ChatRecord['subtype']). On main this compiled because record was narrowed to ChatRecord.

Suggested fix (any one of these)

  1. Narrow the input type — change TranscriptRecordInput.subtype from string to ChatRecord['subtype']. This is sound: validateTranscriptRecord already rejects any subtype not in KNOWN_RECORD_SUBTYPES, and that set is exactly the 17 members of the ChatRecord['subtype'] union.
  2. Make isTranscriptConversationRecord a real type guard (e.g. record is TranscriptRecordInput & { subtype?: ChatRecord['subtype'] }).
  3. Cast at the call site: subtype: record.subtype as ChatRecord['subtype'].

Option 1 is the cleanest and keeps the type meaningful. Once packages/core compiles, all three checks should go green.

中文说明(点击展开)

构建失败根因

三个失败的 CI 任务(Testweb-shell E2E SmokeCapture web-shell visuals)都卡在同一步骤:packages/core 的 TypeScript 构建。由于 npm ci 会执行 prepare 生命周期脚本(npm run build),这个编译错误会在每个任务安装依赖阶段就中断,因此测试根本没有运行起来。阻塞性的编译错误只有一条:

src/services/session-transcript-reader.ts(824,35): error TS2345: Argument of type '{ ...; subtype?: string | undefined; ... }' is not assignable to parameter of type 'UuidIndexEntry'.
  Types of property 'subtype' are incompatible.
    Type 'string | undefined' is not assignable to type '"notification" | "slash_command" | "chat_compression" | ... | undefined'.

原因分析

本 PR 新增了 packages/core/src/utils/transcript-records.ts,并将 session-transcript-reader.ts 中构建 UUID 索引的来源从原来的 parseLineTolerant<ChatRecord>(...) + isChatRecord(record)isChatRecordvalue is ChatRecord 类型守卫)改为 validateTranscriptRecord(...).record + isTranscriptConversationRecord(record)

  • 新的 TranscriptRecordInput.subtype 被定义为宽泛的 stringtranscript-records.ts:29)。
  • isTranscriptConversationRecord 返回的是普通 boolean不是类型谓词,因此不会收窄 record 的类型。
  • UuidIndexEntry.subtype 仍然是 ChatRecord['subtype'],即窄的字符串字面量联合类型(session-transcript-reader.ts:119)。

所以在 session-transcript-reader.ts:824 处,record.subtypestring | undefined)无法再赋值给 UuidIndexEntry.subtypeChatRecord['subtype'])。在 main 上之所以能编译通过,是因为 record 被收窄成了 ChatRecord

修复建议(任选其一)

  1. 收窄输入类型——把 TranscriptRecordInput.subtypestring 改为 ChatRecord['subtype']。这是安全的:validateTranscriptRecord 已经会拒绝不在 KNOWN_RECORD_SUBTYPES 中的 subtype,而该集合恰好就是 ChatRecord['subtype'] 联合类型的 17 个成员。
  2. isTranscriptConversationRecord 改成真正的类型守卫(例如 record is TranscriptRecordInput & { subtype?: ChatRecord['subtype'] })。
  3. 在调用处强制转换subtype: record.subtype as ChatRecord['subtype']

推荐方案 1,最简洁且能保留类型的实际意义。只要 packages/core 能编译通过,三个 CI 检查应该都会变绿。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline. Not reviewed: invariant-a (tool-call-emitter.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: invariant-b (tool-call-emitter.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: invariant-c (tool-call-emitter.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: invariant-a (history-replayer.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: invariant-b (history-replayer.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: invariant-c (history-replayer.ts) — harness did not match launched agent to CLI-built prompt; the agent ran substantively and found no issues. Not reviewed: reverse audit — 8 per-chunk auditors ran substantively with no new findings; harness did not match CLI-built prompts. Not reviewed: verification — 2 Suggestions verified against source code; harness did not match CLI-built verify prompt. Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/history-replayer.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/history-replayer.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/history-replayer.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies. Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.

— qwen3.7-max via Qwen Code /review

Comment on lines +928 to +931
const first = makeTextChunkWithParent(1, 'first', 'task::x');
const second = makeTextChunkWithParent(2, 'second', 'task::x');
engine.ingest(withSources(first, ['a::b', 'c']));
engine.ingest(withSources(second, ['a', 'b::c']));

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] withSources replaces _meta entirely (update['_meta'] = { qwenTranscript: { sourceRecordIds } }), which destroys the parentToolCallId that makeTextChunkWithParent set. The test labelled "uses structured source identity for interleaved subagent chunks" therefore never reaches the subagent path — it falls through to the top-level adjacency check instead.

Failure scenario: a bug in the subagent entries.find/sourceRecordIds-based index lookup would ship undetected because no test actually exercises that branch with a parent tool call ID present.

Suggested change
const first = makeTextChunkWithParent(1, 'first', 'task::x');
const second = makeTextChunkWithParent(2, 'second', 'task::x');
engine.ingest(withSources(first, ['a::b', 'c']));
engine.ingest(withSources(second, ['a', 'b::c']));
const first = makeTextChunkWithParent(1, 'first', 'task::x');
const second = makeTextChunkWithParent(2, 'second', 'task::x');
engine.ingest(withSources(first, ['a::b', 'c'], 'task::x'));
engine.ingest(withSources(second, ['a', 'b::c'], 'task::x'));

— qwen3.7-max via Qwen Code /review

const renderMode = useTranscriptRenderMode();
if (href && QWEN_SESSION_SCHEME.test(href.trim())) {
if (renderMode === 'readonly') {
return <span className={styles.link}>{children}</span>;

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] Readonly-mode session reference reuses styles.link, which includes a :hover { text-decoration: underline } rule — the <span> visually mimics a clickable link but does nothing on click.

Failure scenario: in a readonly transcript (exported or shared view), a user sees blue text that underlines on hover. They click expecting to navigate to the referenced session; nothing happens. The visual affordance contradicts the non-interactive intent stated by the test name "renders qwen session references as inert text in readonly mode."

Suggested fix: use a separate styles.inertLink class that has the color but not the hover rule, or add a CSS override to suppress hover on non-anchor elements.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM — build error resolved, CI green, architecture is solid. Maintainer awareness flag for scope still applies. ✅

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

🔍 Maintainer Local Verification Report

Verified on branch feat/chat-record-webshell-transcript @ b355bc8e1b (macOS, Node.js v22.22.2)

Test Report

Build

All 6 affected packages built successfully: core, acp-bridge, sdk-typescript, cli, webui, web-shell.

PR-Specific Unit Tests — 17 files, 607 tests, ALL PASSED ✅

Package Test File Tests Status
core transcript-records.test.ts 8
core session-transcript-reader.test.ts 46
acp-bridge transcript-replay.test.ts 6
acp-bridge compactionEngine.test.ts 57
cli history-replayer.test.ts 52
cli history-replayer.conformance.test.ts 1
cli history-replay-page.test.ts 5
cli acpAgent.test.ts 275
sdk-typescript daemon-transcript-projection.test.ts 14
sdk-typescript daemon-public-surface.test.ts 13
web-shell WebShellTranscript.dom.test.tsx 9
web-shell WebShellTranscript.test.tsx 4
web-shell useMessages.test.ts 1
web-shell index.test.tsx 16
web-shell Markdown.test.ts 64
web-shell SystemMessage.test.tsx 5
web-shell ToolGroup.test.tsx 31

WebShell Full Vitest Suite — 109 files, 1,739 tests, ALL PASSED ✅

Playwright E2E (Chromium) — 16/16 PASSED ✅

Typecheck

Package Status Notes
core 2 pre-existing errors unrelated to this PR
acp-bridge
sdk-typescript
web-shell

CI Status

All CI checks passed: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke, Capture web-shell visuals.

Notes

  • The full npm run build (all packages in one pass) hits a pre-existing TS5055 conflict in the local worktree environment when tsc --build follows CLI project references into an already-built core/dist. Individual package builds and CI are unaffected. This is not introduced by this PR.
  • No regressions observed in existing WebShell interactive behavior (full suite + Playwright smoke).

🇨🇳 中文验证报告

🔍 维护者本地验证报告

验证分支:feat/chat-record-webshell-transcript @ b355bc8e1b(macOS,Node.js v22.22.2)

测试报告

构建

6 个受影响包全部构建成功:coreacp-bridgesdk-typescriptcliwebuiweb-shell

PR 相关单元测试 — 17 个文件,607 个测试,全部通过 ✅

测试文件 测试数 状态
core transcript-records.test.ts 8
core session-transcript-reader.test.ts 46
acp-bridge transcript-replay.test.ts 6
acp-bridge compactionEngine.test.ts 57
cli history-replayer.test.ts 52
cli history-replayer.conformance.test.ts 1
cli history-replay-page.test.ts 5
cli acpAgent.test.ts 275
sdk-typescript daemon-transcript-projection.test.ts 14
sdk-typescript daemon-public-surface.test.ts 13
web-shell WebShellTranscript.dom.test.tsx 9
web-shell WebShellTranscript.test.tsx 4
web-shell useMessages.test.ts 1
web-shell index.test.tsx 16
web-shell Markdown.test.ts 64
web-shell SystemMessage.test.tsx 5
web-shell ToolGroup.test.tsx 31

WebShell 完整 Vitest 测试集 — 109 个文件,1,739 个测试,全部通过 ✅

Playwright E2E(Chromium)— 16/16 通过 ✅

类型检查

状态 备注
core 2 个与本 PR 无关的预存错误
acp-bridge
sdk-typescript
web-shell

CI 状态

所有 CI 检查通过:Test(ubuntu-latest, Node 22.x)、web-shell E2E Smoke、Capture web-shell visuals。

备注

  • 全量 npm run build(一次性构建所有包)在本地 worktree 环境下会触发预存的 TS5055 冲突——tsc --build 沿 CLI 项目引用写入已构建的 core/dist。逐包构建和 CI 不受影响,此问题非本 PR 引入。
  • 现有 WebShell 交互行为未观察到回归(完整测试集 + Playwright 冒烟测试)。

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real local build & end‑to‑end run

Verified at head b355bc8e1 on macOS (Node v22.23.1). I went beyond re‑running the PR's own suites: I built the real artifacts, exercised the actual published consumer entry on hand‑authored realistic histories, cross‑checked the daemon replay path against the offline projection, and rendered the projected blocks through the public readonly WebShellTranscript with screenshots. Net result: the "one canonical replay path" claim holds, and both the offline SDK projection and the readonly renderer work end‑to‑end. One small, non‑blocking observation below.

1. Their suites — green baseline (202 targeted tests)

Package Files Tests
core transcript-records, session-transcript-reader 54 ✅
acp-bridge transcript-replay, compactionEngine 63 ✅
sdk daemon-transcript-projection, daemon-public-surface 27 ✅
cli history-replayer, history-replayer.conformance, history-replay-page 58 ✅

packages/core, acp-bridge, and sdk all build cleanly. The SDK build's own guards passed too: the dist/daemon/transcript.* bundle stays under budget, is browser‑safe (no Node builtins), and its .d.ts does not leak @qwen-code/qwen-code-core / @qwen-code/acp-bridge.

2. Real published consumer entry (reviewer steps 1–3)

Resolved require('@qwen-code/sdk/daemon/transcript') → the built dist/daemon/transcript.cjs (self‑contained, no @qwen-code source on the runtime path) and projected four hand‑authored ChatRecord histories:

  • Rich multi‑turn (user → thought → read_file tool+result → thought → markdown answer → todo_write plan → follow‑up → answer): order & kinds exact, provenance sourceRecordIds union the call+result records (['a1','t1']), same‑record fragments merge, tool correlates to completed, assistant usage preserved, projection deterministic, complete: true.
  • Diagnostics/degradation: emits invalid_record, history_gap, malformed_part, unknown_record_or_part; the dangling tool is finalized failed; a synthetic "history unavailable" notice block is injected; complete: false.
  • Rewind/branch selection: only the newest active parent chain is replayed — the abandoned branch is dropped; complete: true.
  • maxBlocks truncationtruncated: true, complete: false, transcript_blocks_truncated diagnostic; and input validation throws TranscriptProjectionInputError with invalid_max_blocks / invalid_records / leaf_not_found.

3. Daemon ↔ SDK alignment on rich histories (reviewer step 4 — the PR's flagged gap)

The shipped conformance test proves alignment for one gap‑free case. The PR's own Risk & Scope notes that a real rich session was not replayed E2E through the daemon path. I closed that: I drove the actual CLI HistoryReplayer, reduced its emitted SessionUpdates with the SDK reducer, and diffed against the offline projection.

  • Rich multi‑turn (thought + read_file + todo + usage): block‑for‑block equal
  • Rewind/branch selection: block‑for‑block equal
  • Gapped/malformed/dangling: structurally identical (kinds, provenance, positions, dangling→failed) — see note below.

(Confirmed production forwards gaps: Session.ts:1626 and history-replay-page.ts:194 both call replay(records, gaps).)

4. Readonly WebShellTranscript render + screenshots (reviewer steps 5–6)

Fed the offline‑projected blocks straight into the public WebShellTranscript in a standalone Vite page — no daemon, no session provider, no composer/approval wiring. Asserted the readonly invariants and content, across both themes, with zero console/page errors on every variant:

  • 0 <textarea>, 0 contenteditable, 0 send/stop/approve/reject buttons (readonly ✔)
  • Messages, thinking, tools, markdown + fenced code (syntax‑highlighted, Copy), and the todo plan all render; theme‑aware (dark + light).
  • The diagnostics history renders gracefully: the gap notice, the kept post‑gap text, and the dangling tool as a red Failed / Shell.
Happy path — dark Happy path — light
happy dark happy light

Diagnostics/degradation (gap notice + finalized‑failed tool), readonly:

diagnostics

🔎 One non‑blocking observation

For gapped histories, the two "canonical" paths emit a different gap‑notice sentence (the block position/kind/provenance are identical; only the human text differs, because only the CLI supplies a custom formatHistoryGap):

  • daemon replay → "⚠️ History gap: earlier conversation was lost before this point (storage interruption) and could not be recovered."
  • offline projection (what WebShell renders) → "Some earlier messages are unavailable because the saved history is incomplete."

So the same interrupted session shows one wording live via the daemon and another when rendered offline. This is cosmetic and the presentation adapter is intentional, so it's not a blocker — just flagging in case a consistent notice across daemon‑ and offline‑rendered transcripts is desired (the shipped conformance test uses a gap‑free fixture, so it never surfaces this).

Verdict

LGTM from a verification standpoint — merge‑ready. Storage → projection → daemon‑alignment → readonly render all hold on realistic data through the real public entry points; the only nuance is the cosmetic gap‑notice wording above.

Note: unrelated to this PR, my checkout's node_modules was missing turndown (a core dependency); I supplied it so the build type‑checks. Not a PR issue.

🇨🇳 中文版本(点击展开)

✅ 维护者验证 —— 本地真实构建与端到端运行

在 head b355bc8e1、macOS(Node v22.23.1)上验证。我没有只是重跑 PR 自带的测试,而是构建了真实产物,用手写的真实历史数据调用实际发布的消费者入口,将 daemon 重放路径与离线投影做了逐块对比,并通过公开的只读 WebShellTranscript 渲染投影得到的 blocks 并截图。结论:「唯一规范重放链路」的说法成立,离线 SDK 投影与只读渲染均可端到端工作。下面有一条不阻塞的小观察。

1. 现有测试 —— 绿色基线(202 个定向测试)

文件 测试数
core transcript-recordssession-transcript-reader 54 ✅
acp-bridge transcript-replaycompactionEngine 63 ✅
sdk daemon-transcript-projectiondaemon-public-surface 27 ✅
cli history-replayerhistory-replayer.conformancehistory-replay-page 58 ✅

coreacp-bridgesdk 均干净构建。SDK 构建自带的校验也通过:dist/daemon/transcript.* 未超体积预算、浏览器安全(无 Node 内建)、且其 .d.ts 未泄漏内部依赖。

2. 真实发布的消费者入口(Reviewer 步骤 1–3)

require('@qwen-code/sdk/daemon/transcript') 解析到构建产物 dist/daemon/transcript.cjs(自包含,运行期不依赖 @qwen-code 源码),对四组手写 ChatRecord 历史做投影:

  • 多轮富历史(user → thinking → read_file 调用+结果 → thinking → markdown 回答 → todo_write plan → 追问 → 回答):顺序与 kind 精确;provenance sourceRecordIds 合并调用+结果记录(['a1','t1']);同一 record 片段合并;工具关联为 completed;assistant usage 保留;投影确定性complete: true
  • 诊断/降级:产出 invalid_recordhistory_gapmalformed_partunknown_record_or_part;悬空工具结束为 failed;注入「历史不可用」提示块;complete: false
  • rewind/分支选择:只重放最新有效父子链,放弃分支被丢弃;complete: true
  • maxBlocks 截断truncated: truecomplete: falsetranscript_blocks_truncated;非法参数抛出 TranscriptProjectionInputErrorinvalid_max_blocks / invalid_records / leaf_not_found)。

3. Daemon 与 SDK 在富历史上的一致性(Reviewer 步骤 4 —— PR 自述的缺口)

现有 conformance 测试只覆盖了一个无 gap 的场景。PR 的风险说明也提到真实富 session 未通过 daemon 路径做完整端到端重放。我补上了:驱动真实的 CLI HistoryReplayer,用 SDK reducer 归约其发出的 SessionUpdate,再与离线投影对比。

  • 多轮富历史(thinking + read_file + todo + usage):逐块相等
  • rewind/分支选择:逐块相等
  • 有 gap/异常/悬空:结构完全一致(kind、provenance、位置、悬空→failed)—— 见下方观察。

(已确认生产路径会传入 gaps:Session.ts:1626history-replay-page.ts:194 都调用 replay(records, gaps)。)

4. 只读 WebShellTranscript 渲染 + 截图(Reviewer 步骤 5–6)

将离线投影得到的 blocks 直接喂给公开的 WebShellTranscript,运行在独立 Vite 页面 —— 无 daemon、无 session provider、无 composer/审批。在明暗两套主题下断言只读不变量与内容,且每个变体零 console/page 错误

  • 0<textarea>0contenteditable0 个 send/stop/approve/reject 按钮(只读 ✔)
  • 消息、thinking、工具、markdown + 代码块(语法高亮、Copy)、todo plan 均正常渲染;主题自适应(暗+亮)。
  • 诊断历史优雅降级:gap 提示、gap 后保留的文本,以及悬空工具渲染为红色 Failed / Shell

截图见上方英文部分(happy 暗/亮 + 诊断)。

🔎 一条不阻塞的观察

对于有 gap 的历史,两条「规范」路径给出的 gap 提示文案不同(block 的位置/kind/provenance 完全一致,仅人类可读文本不同,因为只有 CLI 提供了自定义 formatHistoryGap):

  • daemon 重放 → "⚠️ History gap: earlier conversation was lost before this point (storage interruption) and could not be recovered."
  • 离线投影(WebShell 渲染的就是它)→ "Some earlier messages are unavailable because the saved history is incomplete."

即同一个被中断的 session,在线(daemon)与离线渲染会显示不同措辞。这是纯展示层差异且 presentation adapter 本就是可插拔的,不阻塞合并——仅提示:如果希望 daemon 与离线渲染的 gap 提示保持一致,可以统一(现有 conformance 用的是无 gap 数据,因此从未触发到这点)。

结论

从验证角度 LGTM —— 可合并。 存储 → 投影 → daemon 一致性 → 只读渲染,在真实数据上、通过真实公开入口全部成立;唯一细节是上面这条纯展示层的 gap 文案差异。

注:与本 PR 无关——我本地 node_modules 缺少 turndowncore 的依赖),我补装后才能通过类型检查;非 PR 问题。

@wenshao
wenshao added this pull request to the merge queue Jul 19, 2026
Merged via the queue into QwenLM:main with commit 9b6da8e Jul 19, 2026
68 checks passed

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Found two correctness issues in the public offline-projection/readonly-rendering contracts. Details are inline.

The large cross-package feat also remains subject to the repository's maintainer-awareness advisory.

if (this.finalized) return;
this.finalized = true;
let ordinal = 0;
for (const pending of [...this.pendingToolCalls.values()]) {

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] Mark dangling tool history as incomplete. The finalizer synthesizes a failed result for every pending call, but it never sends an affectsCompleteness diagnostic through onDiagnostic. Consequently projectChatRecordsToDaemonTranscript() returns a failed tool block with diagnostics: [] and complete: true for an assistant tool call whose result is missing. That contradicts the PR's stated incomplete-tool diagnostics contract and can make offline consumers trust a partial projection. Please report a dedicated completeness diagnostic here and extend the dangling-tool test to assert both complete === false and the diagnostic.

<div
className={`${styles.content} ${styles.contentHasMessages}`}
>
<MessageList

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] Keep tasks-status rendering independent of daemon actions. A valid status block containing the serialized /tasks payload reaches SystemMessage → TasksStatusMessage, which unconditionally calls useActions(). This component intentionally installs no DaemonSessionProvider, so that hook throws and the entire readonly transcript falls into the root error boundary. The task component also polls and exposes cancellation, which would violate readonly behavior even if a provider were added. Please give the readonly path a static tasks renderer (or optional actions with all refresh/cancel/global-active side effects disabled) and cover a tasks-status block in the no-provider DOM test.

@yiliang114

Copy link
Copy Markdown
Collaborator

@cxruan @wenshao I did a post-merge audit of this PR and confirmed that the two unresolved Critical findings are reproducible:

  1. Dangling tool calls are synthesized as failed, but the projection still returns complete: true with no diagnostic.
  2. A serialized tasks-status block renders through TasksStatusMessage, which requires DaemonSessionProvider, polls live tasks, and exposes cancellation in the readonly transcript.

I also found three related gaps:

  • MCP status has the same readonly isolation problem: McpStatusMessage requires DaemonWorkspaceProvider and exposes reconnect/enable/disable/auth actions. Without the provider, rendering throws.
  • Legacy tool-result records with an error only in functionResponse.response.error are projected as status: completed, complete: true, with no diagnostic.
  • Duplicate UUID fragments with conflicting parentUuid values emit a diagnostic but are still aggregated, so content from the conflicting branch enters the projection.

I reproduced the provider failures and projection outputs locally. The targeted Core/ACP/SDK/WebShell tests pass, but these cases are currently uncovered. Since this has shipped in v0.20.0, could we track the correctness fixes in a follow-up PR or issue?

@cxruan

cxruan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@yiliang114 @ytahdn Thanks for the audit. I confirmed the reported projection edge cases, with a few impact and reachability nuances:

  • Dangling tools already render as failed; only diagnostics and complete are incorrect. The opt-in SDK projection currently has no in-repo production callers, so the impact is limited.
  • Legacy functionResponse.response.error records are incorrectly rendered as completed.
  • Conflicting-parent fragments are diagnosed but still aggregated into the transcript.

The tasks/MCP provider issues are not reachable through the real ChatRecord → projection → WebShellTranscript path. Those commands are not persisted, and slash-command records become assistant blocks rather than status messages. Manually supplied status blocks can trigger the issue, but failures are isolated per message.

I’ll open a follow-up PR to fix the issues, and add regression tests.

cxruan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@yiliang114 @ytahdn Follow-up after tracing each finding against the pre-#6999 code: I opened Draft PR #7340 to address the issue introduced by #6999—the dangling-tool finalizer synthesizes a failed result but does not emit an affectsCompleteness diagnostic, so the offline projection reports complete: true.

The PR body also documents why the other audit findings are out of scope: task/MCP status blocks are not produced by the real persisted ChatRecord projection path, while the legacy nested-error status mapping and conflicting-parent UUID aggregation both predate #6999.

Thanks again for the audit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants