feat(webshell): replay ChatRecord history in readonly WebShell - #6999
Conversation
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head 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 |
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 /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 resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution Summary — PR #6999Base branch
Conflicted files
What each side broughtPR branch (
|
|
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:
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 中文说明感谢贡献,@cxruan! 模板完整 ✓ — 所有必需章节齐全且填写完善。 问题: 这是一个功能新增,不是 bug 修复。所述需求 — SDK 和浏览器消费者无法在不依赖 Core session 内部实现的情况下重放持久化的 ChatRecord 历史 — 是真实存在的。目前,任何想要渲染已存储 session 的人要么需要复制重放语义,要么需要引入完整的 Core session service,这对于离线/只读用例来说是不必要的耦合。 方向: 关注点分离(Core 准备 records → ACP 重放 → SDK 投影 → WebShell 渲染)在架构上是合理的。daemon 加载和离线消费者共享一条规范的重放链路是正确的思路。Session 重放和 SDK transcript 访问显然在 qwen-code 的范围内。 规模: 这是一个大型跨 package 功能。分解如下:
触发了 1000+ 行建议(非阻塞)和 500+ Core 行数维护者关注标记。测试与生产代码比例健康,设计文档显示了深思熟虑的规划。 方案: 分层架构(存储 → 投影 → 渲染)考虑周全。Core 中的 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewThe architecture across this PR is well-layered — Core prepares records ( The previous build-blocking type error (in 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:
No correctness bugs, security issues, or regressions found. Verification
中文说明代码审查此 PR 的架构分层良好 — Core 准备 records( 之前的构建阻塞类型错误( 构建和类型检查在所有 package 中均通过。 CI 全部为绿色。 关键实现发现:
验证所有构建、类型检查和测试均通过(见上方英文表格)。本地验证了 94 个针对性新增测试,CI 的 Test、web-shell E2E Smoke 和 Capture web-shell visuals 全部通过。 — Qwen Code · qwen3.7-max Reviewed at |
|
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 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 面并抑制了全局导航事件。 之前的阻塞问题 — 维护者关注标记仍然适用:这是一个 55 个文件、5 个 package 的功能,包含 846 行 Core 生产代码。合并前需要维护者对范围进行确认。架构设计良好,但广度意味着多个 package 的下游消费者应了解新的共享重放原语。 附带维护者关注条件批准。✅ — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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. 🙏
|
@qwen-code /resolve |
…ion-aware pagination
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution Summary — PR #6999ConflictFile: The PR branch (feat/chat-record-webshell-transcript) and origin/main both added new standalone functions at the same location in the file (after
ResolutionKept all three functions. They are independent features that both have call sites in the auto-merged portions of the file:
No logic changes were made — the conflict was purely additive on both sides. Commit
|
|
Qwen Code review did not complete successfully. Qwen review exited with status 1. See workflow logs. |
Build failure root causeAll three failing CI jobs — Why it happensThis PR adds
So at Suggested fix (any one of these)
Option 1 is the cleanest and keeps the type meaningful. Once 中文说明(点击展开)构建失败根因三个失败的 CI 任务( 原因分析本 PR 新增了
所以在 修复建议(任选其一)
推荐方案 1,最简洁且能保留类型的实际意义。只要 |
…chat-record-webshell-transcript
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
| 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'])); |
There was a problem hiding this comment.
[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.
| 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>; |
There was a problem hiding this comment.
[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
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM — build error resolved, CI green, architecture is solid. Maintainer awareness flag for scope still applies. ✅
🔍 Maintainer Local Verification ReportVerified on branch BuildAll 6 affected packages built successfully: PR-Specific Unit Tests — 17 files, 607 tests, ALL PASSED ✅
WebShell Full Vitest Suite — 109 files, 1,739 tests, ALL PASSED ✅Playwright E2E (Chromium) — 16/16 PASSED ✅Typecheck
CI StatusAll CI checks passed: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke, Capture web-shell visuals. Notes
🇨🇳 中文验证报告🔍 维护者本地验证报告验证分支: 构建6 个受影响包全部构建成功: PR 相关单元测试 — 17 个文件,607 个测试,全部通过 ✅
WebShell 完整 Vitest 测试集 — 109 个文件,1,739 个测试,全部通过 ✅Playwright E2E(Chromium)— 16/16 通过 ✅类型检查
CI 状态所有 CI 检查通过:Test(ubuntu-latest, Node 22.x)、web-shell E2E Smoke、Capture web-shell visuals。 备注
|
ytahdn
left a comment
There was a problem hiding this comment.
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()]) { |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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.
|
@cxruan @wenshao I did a post-merge audit of this PR and confirmed that the two unresolved Critical findings are reproducible:
I also found three related gaps:
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? |
|
@yiliang114 @ytahdn Thanks for the audit. I confirmed the reported projection edge cases, with a few impact and reachability nuances:
The tasks/MCP provider issues are not reachable through the real I’ll open a follow-up PR to fix the issues, and add regression tests. |
|
@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 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 Thanks again for the audit. |




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
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
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
/loadresponse 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.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 测试计划
如何验证
本地自动化验证已完成: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,Node.js v24.14.1,npm 11.11.0。WebShell 浏览器回归使用仓库 Playwright 配置、本地 Vite 和现有 fake daemon。root 和 package build 直接在宿主机运行。
风险与范围
/loadresponse 和 stream 两种模式执行完整端到端重放。Windows 和 Linux 未在本地测试,预计由 CI 覆盖。JSONL 解析、文件选择、持久化、分页和实时流仍由宿主负责。关联 Issues
N/A