feat(serve): add workspace persisted transcript reader - #6740
Conversation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Thanks for the PR! Re-run with updated review. Template looks good ✓ Problem: This is a real capability gap, not theoretical hardening. Untrusted secondary workspaces can already list persisted sessions via the daemon catalog, but reading transcript content forces the ACP path — which may start workspace-controlled execution, load workspace settings, and create cursor-key files. The PR closes that read-only gap. Related to #6378. Direction: Aligned. The daemon already serves persisted-only catalogs; adding a persisted-only transcript read path is the natural next step. No direct CHANGELOG reference, but the daemon workspace API has been growing in this direction across recent releases. Size: ~662 production logic lines, ~216 test lines, ~47 design doc lines. Core touch is small (76 lines in Approach: Scope feels right. The new REST route, the in-memory cursor codec (HKDF-derived per workspace, no file creation), the replay-page module, and the SDK method are all needed for the stated goal. The kebab-case renames for Moving on to code review. 🔍 中文说明感谢贡献!Re-run 更新审查。 模板完整 ✓ 问题:这是一个真实的能力缺口,而非理论性加固。Untrusted secondary workspace 已经可以通过 daemon catalog 列出已持久化的 session,但读取 transcript 内容会强制进入 ACP 路径——可能启动 workspace-controlled execution、加载 workspace settings、并创建 cursor-key 文件。本 PR 补齐了这个只读缺口。关联 #6378。 方向:对齐。Daemon 已经提供 persisted-only 的 catalog 服务;新增 persisted-only transcript 读取路径是自然的下一步。CHANGELOG 无直接引用,但 daemon workspace API 在近几个版本中一直在朝这个方向发展。 规模:约 662 行生产逻辑代码、约 216 行测试代码、约 47 行设计文档。Core 触及较小( 方案:范围合理。新 REST 路由、内存 cursor codec(HKDF 按 workspace 派生,不创建文件)、replay-page 模块和 SDK 方法都是实现目标所需的。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): I'd add a workspace-qualified GET route that reads only the persisted JSONL transcript file, uses HMAC-signed cursors with daemon-lifetime keys (no file creation), and converts raw ChatRecords to SessionUpdate events using the existing replay logic. SDK client method should force REST transport. The PR's approach matches this proposal closely and exceeds it in several ways:
No critical blockers found. The code is clean, well-tested, and follows project conventions. The shared replay logic between ACP and direct paths is a conscious design tradeoff (noted in Stage 1), and existing tests cover the boundary. Reuse check: Test ResultsThis is a daemon REST API + SDK change with no user-visible CLI behavior to demonstrate in tmux before/after. The PR author explicitly marked visual evidence as N/A. Unit test results serve as the primary verification evidence. Total: 992 tests passing across all affected packages. Key test coverage:
中文说明代码审查独立方案(阅读 diff 之前):我会添加一个 workspace-qualified 的 GET 路由,只读取已持久化的 JSONL transcript 文件,使用 daemon 生命周期的 HMAC 签名 cursor(不创建文件),并使用现有 replay 逻辑将原始 ChatRecord 转换为 SessionUpdate 事件。SDK 客户端方法应强制使用 REST transport。 PR 的方案与此高度吻合,并在多处超越:
未发现关键阻塞问题。代码干净、测试充分、遵循项目约定。ACP 和 direct 路径之间的共享 replay 逻辑是有意识的设计权衡(Stage 1 已说明),现有测试覆盖了该边界。 复用检查: 测试结果这是一个 daemon REST API + SDK 变更,无用户可见的 CLI 行为可在 tmux 中 before/after 演示。PR 作者已明确将视觉证据标为 N/A。单元测试结果是主要验证证据。 总计:992 个测试在所有受影响包中通过。 — Qwen Code · qwen3.7-max |
|
This PR ships a well-designed, well-tested feature. The motivation is clear (untrusted workspaces can list sessions but not read transcripts), the implementation is clean, and the test coverage is comprehensive at 992 passing tests across core, CLI, and SDK. The cursor security model is thoughtful: HKDF-derived per-workspace keys from a daemon-lifetime master key means cursors expire on restart (by design) and no key file is written for the new path. The shared replay logic between the legacy ACP pager and the new direct pager is a reasonable coupling — both paths convert the same persisted ChatRecords into the same SessionUpdate format, so sharing the conversion avoids drift. Every change in the diff serves the stated goal. The kebab-case renames are per repo convention and isolated in doc references. No drive-by refactors, no scope creep. The design doc ( If I had to maintain this in six months, I'd thank the author — the code is straightforward, the error paths are well-named, and the security boundary is enforced at every layer (workspace trust check, archive coordinator lock, session ID validation on records, debug log suppression). My independent proposal matched the PR's approach. No simpler path was missed. LGTM — approving. ✅ 中文说明本 PR 交付了一个设计良好、测试充分的功能。动机清晰(untrusted workspace 可以列出 session 但不能读取 transcript),实现干净,测试覆盖全面——core、CLI 和 SDK 共 992 个测试通过。 Cursor 安全模型考虑周到:从 daemon 生命周期的 master key 按 workspace HKDF 派生 key,意味着 cursor 在重启后失效(按设计),且新路径不写入 key 文件。旧 ACP pager 和新 direct pager 之间的共享 replay 逻辑是合理的耦合——两条路径都将相同的已持久化 ChatRecord 转换为相同的 SessionUpdate 格式,共享转换逻辑可避免偏差。 Diff 中的每个变更都服务于既定目标。Kebab-case 重命名符合仓库约定且限于文档引用。无顺手重构、无范围蔓延。设计文档清晰地说明了范围外的部分(archived 读取、live follow、Web Shell 查看器)并明确延后。 如果六个月后需要维护这段代码,我会感谢作者——代码直截了当,错误路径命名清晰,安全边界在每一层都有执行(workspace trust check、archive coordinator 锁、record 的 session ID 验证、debug log 抑制)。 我的独立方案与 PR 方案吻合。未发现更简路径。 LGTM — 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
CI Failure AnalysisFailing job: Failing test: Root cause: The PR adds The test has a hardcoded expected features list (76 entries). The new capability makes it 77, so the assertion fails: Fix: Add 'workspace_qualified_rest_core',
+ 'workspace_persisted_transcript',
'voice_transcribe',中文分析CI 失败分析失败的 Job: 失败的测试: 根本原因: PR 在 该测试有一个硬编码的预期 features 列表(76 项)。新增的 capability 使实际返回变为 77 项,导致断言失败: 修复方法: 在 'workspace_qualified_rest_core',
+ 'workspace_persisted_transcript',
'voice_transcribe', |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
✅ Local build & real end-to-end validation — recommend mergeI validated this on a clean, isolated checkout of the PR head ( Environment: macOS (darwin 24.6) · Node v22.23.1 · npm 10.9.8 · fresh 1. Real end-to-end against a live
|
|
@qwen-code /triage |
| sessionShellCommandEnabled, | ||
| } = deps; | ||
| const LANGUAGE_CODES = deps.languageCodes; | ||
| const transcriptCursorMasterKey = crypto.randomBytes(32); |
There was a problem hiding this comment.
[Suggestion] The cursor-signing master key is crypto.randomBytes(32) at route-registration time, so every nextCursor handed to clients dies with the daemon process. Failure scenario: a client pages a long transcript, the daemon restarts (upgrade, crash, laptop sleep-restart of the service), the client follows its stored nextCursor → MAC mismatch → the request fails mid-pagination and the client must restart from page one. The pre-existing encodeSessionTranscriptCursor path derives from a persisted per-workspace key, so this is a behavior choice the route makes silently. If process-scoped cursors are intentional (forgery containment), a line in docs/design/untrusted-persisted-transcript.md saying so — and a distinct error body telling clients to restart pagination — would make the contract explicit.
— posted from the /review dogfooding session (hand-verified against this head)
| const cursor = | ||
| options.cursor !== undefined | ||
| ? decodeSessionTranscriptCursor(options.cursor, this.workspaceCwd) | ||
| ? (this.cursorCodec?.decode(options.cursor) ?? |
There was a problem hiding this comment.
[Suggestion] This ?? reads like a graceful fallback but can never act as one: SessionTranscriptCursorCodec.decode throws on a bad MAC (and on malformed input) rather than returning undefined, so when cursorCodec is present a decode failure propagates and decodeSessionTranscriptCursor is unreachable. The ?? only selects the legacy path when the codec itself is absent. Concrete cost: the next reader (or a future error-handling change) assumes bad cursors fall back to the workspace-key path, which would quietly accept a cursor signed under a different key domain. Suggest this.cursorCodec ? this.cursorCodec.decode(options.cursor) : decodeSessionTranscriptCursor(...) to say what actually happens.
— posted from the /review dogfooding session (hand-verified against this head)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅



What this PR does
This PR adds a workspace-qualified, daemon-local REST pager for active persisted transcripts. Trusted workspaces and registered untrusted secondary workspaces can read the existing visible transcript projection without attaching to a session, starting ACP, consulting live bridge state, loading settings, or creating the legacy persisted cursor-key file.
The new
workspace_persisted_transcriptcapability advertises the contract. Cursors use daemon-lifetime in-memory signing keys derived per workspace, the replay path can operate without a runtimeConfig, and the TypeScriptWorkspaceDaemonClientmethod always uses native REST even when a replaceable transport is installed. Existing singular ACP-backed transcript behavior remains unchanged.The touched legacy replay modules and their consumers are also mechanically renamed to kebab-case in a separate commit, as required by the repository filename convention.
Why it's needed
Untrusted secondary workspaces can already expose persisted session catalogs, but users and clients cannot inspect transcript content without entering the ACP-backed path that may start workspace-controlled execution and load workspace configuration. This closes that read-only gap while preserving the trust boundary and keeping all mutation, attach, resume, and live-runtime surfaces blocked.
Reviewer Test Plan
How to verify
GET /workspaces/:workspace/session/:id/transcript?limit=1. Expect a persisted-only page and a cursor without any ACP channel startup or cursor-key file creation.400 invalid_transcript_cursor.409 session_archived, an untrusted primary returns403 untrusted_workspace, unknown workspace selectors return400 workspace_mismatch, and record/session mismatches fail closed with409 transcript_snapshot_unavailable.workspaceById(...).getSessionTranscriptPage(...). Expect the native REST fetch to succeed and the replaceable transport to remain unused.git diff --checkalso pass.Evidence (Before & After)
N/A — backend, SDK, protocol, and refactor changes only; the Web Shell viewer is intentionally deferred to a follow-up PR.
Tested on
Environment (optional)
macOS, Node.js v22.22.3, npm 10.9.8, local loopback daemon test harness.
Risk & Scope
workspace_persisted_transcript; its cursors intentionally expire on daemon restart.Linked Issues
Related to #6378
中文说明
本 PR 做了什么
本 PR 新增 workspace-qualified、daemon-local 的 active persisted transcript REST 分页读取能力。Trusted workspace 和已注册的 untrusted secondary workspace 可以读取现有可见 transcript 投影,过程中不会 attach session、启动 ACP、查询 live bridge、加载 settings,也不会创建旧的持久化 cursor-key 文件。
新增的
workspace_persisted_transcriptcapability 用于声明该 contract。Cursor 使用 daemon 生命周期内、按 workspace 派生的内存签名 key;replay 路径可在没有 runtimeConfig的情况下工作;TypeScriptWorkspaceDaemonClient方法即使安装了可替换 transport 也始终强制使用原生 REST。现有 singular、ACP-backed transcript 行为保持不变。按照仓库文件命名约定,本次触及的 legacy replay 模块及其消费者也在独立提交中机械改为 kebab-case。
为什么需要
Untrusted secondary workspace 已经可以暴露 persisted session catalog,但用户和客户端若要查看 transcript 内容,仍需进入可能启动 workspace-controlled execution 并加载 workspace 配置的 ACP-backed 路径。本 PR 在不放开 mutation、attach、resume 和 live-runtime surface 的前提下补齐该只读能力。
Reviewer 测试计划
如何验证
GET /workspaces/:workspace/session/:id/transcript?limit=1。预期返回 persisted-only page 和 cursor,且不会启动 ACP channel 或创建 cursor-key 文件。400 invalid_transcript_cursor。409 session_archived,untrusted primary 返回403 untrusted_workspace,未知 workspace selector 返回400 workspace_mismatch,record/session 不匹配时 fail closed 为409 transcript_snapshot_unavailable。workspaceById(...).getSessionTranscriptPage(...)。预期原生 REST fetch 成功,可替换 transport 未被使用。git diff --check也已通过。证据(Before & After)
N/A——本 PR 仅包含 backend、SDK、protocol 和 refactor 变更;Web Shell 只读查看器明确延后到后续 PR。
已测试平台
环境(可选)
macOS、Node.js v22.22.3、npm 10.9.8、本地 loopback daemon 测试环境。
风险与范围
workspace_persisted_transcriptgate 新路由;其 cursor 按设计会在 daemon 重启后失效。关联 Issue
Related to #6378